Digital Worker Phase 3 — Progress & Risk Digest Design
Phase 3 turns what the Digital Worker already observes into a scheduled Progress & Risk Digest: a per-channel-binding summary of progress, blockers, stale commitments, and follow-ups, delivered via the in-app notifications module and read in the SolidStart panel. It is the lowest-risk next step, composed almost entirely of Phase 1/2 primitives, with no new external-posting surface.
Phase 3 turns what the Digital Worker already observes into a scheduled Progress & Risk Digest — a per-channel-binding summary of what moved, what is blocked, which commitments are stale, and what a manager should follow up on. It is delivered through the existing in-app notifications module and read in the SolidStart panel, with no new external-posting surface.
Goals & scope
Phase 3 is deliberately the lowest-risk, highest-leverage next step. It composes primitives already shipped in Phases 1 and 2 — chat-event ingest, task extraction, the system_cron actor precedent from Fieldforce briefings, the notifications module, AccessGate billing, the watermark recovery pattern, and the feature-flagged panel nav — and introduces no new way for the worker to act on the outside world.
Success criteria
- A manager who owns a team channel receives a digest on a configurable cadence (daily default, weekly option) without having to remember to look.
- Four sections: Progress summary, Blockers & risks, Commitment health (overdue/stale + unclear ownership), and Suggested follow-ups.
- Risk facts are computed from already-extracted structured data, so they are precise and source-linked; narrative comes from a single bounded LLM pass.
- Managers can regenerate on demand from the panel; empty periods produce no notification.
- Every digest is persisted with provenance and readable as history, behind the two-tier
digital_workerflag, respecting existing privacy and retention boundaries.
Out of scope
Posting to external chat
deferred- An unsolicited consequential action; can be added later behind the
ChatSenderport + an approval/auto-post policy.
Performance scoring / KPI
separate module- Digests describe work and process, not individuals. KPI stays a governed future module.
Org-wide rollups
later- Per-channel-binding is the v1 unit; cross-binding rollups are a later enhancement.
New data collection
none- Reads only events already ingested for explicitly-configured channels. Never modifies memory or skills.
Approach selected — mirror the Fieldforce briefings engine
Cron use case + dw_digests store
- New tables
- 1
- History
- Queryable
- Triggers
- One code path
Maximal reuse of cron, notifications, AccessGate, audit, and watermark patterns. Scheduled and on-demand share one GenerateChannelDigest use case.
Reuse dw_inbox_items as carrier
- New tables
- 0
- Semantics
- Mismatch
Inbox items are approvable drafts with promote/dismiss semantics; a digest is read-only reporting. Overloading muddies inbox, audit, and the notification event.
Compute on-read, no persistence
- New tables
- 0
- History
- Lost
A scheduled notification has to generate anyway. This just loses history and re-spends AI on every page view. No upside.
Architecture
generated digests notify.The generation use case lives in the Go digitalworker module alongside the Phase 1/2 use cases. The AccessGate actor is the synthetic dw_worker:<role_agent_id> for both triggers, keeping the triggering human out of the org AI-spend ledger.
Data model
All tables are dw_-prefixed and expressed as raw SQL in migrator.go (CHECK constraints + JSONB), per the schema convention in CONTEXT.md.
dw_digests
One row per generated digest — the source of truth for both the rendered content and the per-binding watermark.
| Column | Type | Notes |
|---|---|---|
id | uuid PK | |
org_id | text NOT NULL | tenant scope |
channel_binding_id | uuid NOT NULL | FK → dw_channel_bindings |
role_agent_id | uuid NOT NULL | persona that generated it — the actor identity used everywhere |
period_start | timestamptz NOT NULL | = period_end of this binding's last successful digest (the watermark) |
period_end | timestamptz NOT NULL | run time; becomes the next period_start |
trigger | text NOT NULL | CHECK ('scheduled','on_demand') |
status | text NOT NULL | CHECK ('generated','empty','failed') — empty = no activity, no notify |
sections | jsonb NOT NULL | rendered sections (progress, blockers, commitment_health, follow_ups); each an array of items with source_refs |
summary_md | text | optional one-line headline |
generated_by | text NOT NULL | dw_worker:<role_agent_id> (scheduled) or user_id (on-demand) |
created_at | timestamptz NOT NULL |
Index: (org_id, channel_binding_id, created_at DESC) for the panel history list.
dw_channel_bindings (ALTER)
Cadence config lives as new columns on the existing binding row, matching where proactive-mode, capabilities, and approver rules already live (no extra join).
| Added column | Type | Notes |
|---|---|---|
digest_enabled | bool NOT NULL DEFAULT false | opt-in per binding |
digest_cadence | text NOT NULL DEFAULT 'daily' | CHECK ('daily','weekly') |
digest_send_hour | int NOT NULL DEFAULT 8 | org-local hour 0–23 |
digest_weekday | int | 0–6, only used for weekly |
Generation pipeline (hybrid)
One use case, GenerateChannelDigest(binding, trigger, actor), runs identically for scheduled and on-demand triggers.
-
Window
Read
[period_start, period_end): thedw_chat_eventsin range plus structured rows (dw_inbox_itemsand their states, Fieldforce task states for promoted items). The window is capped (max messages / chars, same idea as the Phase-1 triage window) so token cost is bounded. -
Structured pass (no AI)
Build Commitment health deterministically: overdue/stale (
commitment_typeset,due_date < now(), not promoted/dismissed/completed) and unclear ownership (suggested_owner_user_id IS NULL). Precise and source-linked — no hallucination risk. -
LLM pass (one gated call)
Send the bounded chat window plus the already-computed structured facts to the remote model via AccessGate
digital_worker:progress_digest(DB-managed, versioned prompt template). It returns strict JSON for the three narrative sections. -
Validation (Phase-1 rules reused)
Drop items whose
source_message_idsfall outside the window; any owner reference must resolve to a verifieddw_external_identitiesrow or it is nulled. Schema failure → drop bad fields, retry once, thenstatus='failed'. -
Empty & cost guards
No chat and no structured signals →
status='empty', skip the LLM call and the notification. Overdue commitments but no chat → structured-only digest (narrative empty). Scheduled cadence bounds volume, so no local-triage pre-filter is needed.
The narrative LLM call returns strict JSON:
{
"progress": [ { "text": "...", "source_message_ids": ["..."] } ],
"blockers": [ { "text": "...", "severity": "low|medium|high", "source_message_ids": ["..."] } ],
"follow_ups": [ { "text": "...", "refers_to": "commitment|blocker|none", "ref_id": "...", "source_message_ids": ["..."] } ]
}
Passing the structured facts into the prompt lets follow-ups reference real overdue commitments without the LLM recomputing them.
Scheduling, on-demand trigger & delivery
Scheduled path (cron)
A periodic loop under the system_cron actor, same precedent as Fieldforce briefings. Each tick, for every binding with digest_enabled = true, it checks whether the binding is due (org-local digest_send_hour reached, weekly matches digest_weekday) and that no successful/empty digest already exists for the current period (idempotent).
- Multi-replica safety: reuse the existing lease / advisory-lock pattern (cf. the Discord gateway owner lease) so exactly one replica generates per binding per period.
- Kill-switch: the whole feature sits behind the two-tier
digital_workerfeature flag (ADR-0016); flag off ⇒ no digests.
On-demand path
POST .../channel-bindings/:id/digest calls the same use case with trigger='on_demand', actor = the requesting user. Authorization = that binding's approver rules ∩ role scope (or owner/admin) — the same gate as viewing it. A light minimum-interval guard prevents AI-spend abuse on top of AccessGate caps.
Delivery / recipients
A new notification event digital_worker:digest_ready flows through the existing notifications module, deep-linking to the binding's digest page. Recipients = the binding's approver rules ∩ role scope — the exact targeting Phase 1 already uses for digital_worker:inbox_pending. One notification per generated digest; empty and failed are not notified to managers (failed surfaces in worker health / audit for admins).
Panel experience
Two surfaces, splitting reading (managers) from configuring (admins), following the Phase-2 pattern. The manager reader lives at /app/digital-workers/digests (the notification deep-link target); the admin config block lives on the channel-binding editor under /app/settings/digital-workers.
- Staging deploy unblocked; client demo build shipped.
- HighCert renewal on the VPN gateway still pending Network Infra.
- MedNo owner confirmed for the migration runbook.
- Overdue"Run staging deploy" — due last night, not promoted.
- "Document switch-port checklist" — no owner resolved.
- Nudge Network Infra on the cert; assign the migration runbook owner.
Components, nav & i18n
- Components
- New under
src/features/digital-workers/digests/, reusing existing panel primitives (cards, chips/badges, section list, markdown renderer, filter bar) per thepanel-ui-styleguide. - Query hooks
- digests-by-binding list, digest detail, generate-on-demand mutation, binding digest-config update (TanStack Query).
- Nav
- A "Digests" link in the sidebar (
apps/panel/src/app.tsx) behind thedigital_workerflag; field-team hidden. - i18n
- New Paraglide
digitalWorkerDigest.*namespace (en/zh/ms) for chrome. LLM-authored section text is server-supplied and not re-translated client-side.
API
GET /api/organizations/:org_id/digital-workers/channel-bindings/:id/digests # history list
GET /api/organizations/:org_id/digital-workers/digests/:digest_id # detail
POST /api/organizations/:org_id/digital-workers/channel-bindings/:id/digest # on-demand generate
# digest_enabled / cadence / send_hour / weekday folded into the existing binding-update endpoint
Internal use-case API: GenerateChannelDigest(binding, trigger, actor) -> Digest.
Safety & privacy
Error handling
| Case | Behavior |
|---|---|
| AI cap/denied (402) or BYOK out of credits (502) | status='failed', watermark not advanced, retried next tick; on-demand returns an error |
| Invalid AI output | Drop invalid fields, retry once, then status='failed' |
| No chat & no structured signals | status='empty', no LLM call, no notification |
| Overdue commitments but no chat events | Structured-only digest (generated); narrative sections empty |
| Notification delivery fails | Digest persists; logged; still visible in the panel |
| Org timezone missing | UTC fallback |
Testing strategy
Backend
- Watermark advance-on-success / hold-on-failure; first-run lookback cap.
- Structured commitment-health queries (overdue, unclear ownership).
- LLM schema validation: out-of-window source-ref drop, unverified-owner coercion, retry-once-then-fail.
- Empty-skip (no notification) and structured-only-generate paths.
- Cron due-calculation: daily, weekly, org-local hour/weekday, UTC fallback; idempotent per-period guard.
- Lease single-owner generation across replicas; feature-flag gating and off-window watermark skip.
- On-demand authorization and minimum-interval guard.
Frontend
- List and detail render of all four sections; severity chips; empty state.
- Config block (enable, cadence, send hour, weekday) and generate-now mutation.
- Nav link gated by the
digital_workerflag.
E2E
- Enable digest on a binding → cron generates → manager notified → opens panel → sees sections with source links.
- On-demand generate from the panel; an overdue commitment surfaces in Commitment health.
- A failed gate holds the watermark and retries on the next tick.
Rollout plan
-
Schema + structured-only
Add
dw_digestsand binding columns; generate Commitment health with no LLM call; readable in the panel. -
LLM narrative pass
Add the gated
digital_worker:progress_digestcall for Progress, Blockers, and Follow-ups. -
Scheduling
The
system_cronloop with the single-owner lease and org-local due-calculation. -
Delivery
The
digital_worker:digest_readynotification fan-out reusing inbox-notification targeting. -
On-demand + config UI
The panel config block and "Generate now" endpoint.
This order yields a useful, fully-auditable digest before any scheduling or AI narrative is switched on, and keeps every step reversible.