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.

  • Predecessor Phase 2 — Memory & Skills
  • ADRs 0007 · 0016
  • Status Approved

View source markdown ↗ generated by claude-opus-4-8 · diagrams mermaid

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.

  • Theme Manager visibility from team-chat signal
  • Unit Per channel binding
  • Delivery Scheduled + on-demand · in-app notify
  • Compute Hybrid: structured facts + one gated LLM pass

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_worker flag, respecting existing privacy and retention boundaries.

Out of scope

Posting to external chat

deferred
  • An unsolicited consequential action; can be added later behind the ChatSender port + 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

Selected

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.

Rejected

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.

Rejected

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

Generation flow — scheduled cron and on-demand both enter the same use case.
Generation flow — scheduled cron and on-demand both enter the same use case; only 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.

ColumnTypeNotes
iduuid PK
org_idtext NOT NULLtenant scope
channel_binding_iduuid NOT NULLFK → dw_channel_bindings
role_agent_iduuid NOT NULLpersona that generated it — the actor identity used everywhere
period_starttimestamptz NOT NULL= period_end of this binding's last successful digest (the watermark)
period_endtimestamptz NOT NULLrun time; becomes the next period_start
triggertext NOT NULLCHECK ('scheduled','on_demand')
statustext NOT NULLCHECK ('generated','empty','failed')empty = no activity, no notify
sectionsjsonb NOT NULLrendered sections (progress, blockers, commitment_health, follow_ups); each an array of items with source_refs
summary_mdtextoptional one-line headline
generated_bytext NOT NULLdw_worker:<role_agent_id> (scheduled) or user_id (on-demand)
created_attimestamptz 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 columnTypeNotes
digest_enabledbool NOT NULL DEFAULT falseopt-in per binding
digest_cadencetext NOT NULL DEFAULT 'daily'CHECK ('daily','weekly')
digest_send_hourint NOT NULL DEFAULT 8org-local hour 0–23
digest_weekdayint0–6, only used for weekly

Generation pipeline (hybrid)

One use case, GenerateChannelDigest(binding, trigger, actor), runs identically for scheduled and on-demand triggers.

  1. Window

    Read [period_start, period_end): the dw_chat_events in range plus structured rows (dw_inbox_items and 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.

  2. Structured pass (no AI)

    Build Commitment health deterministically: overdue/stale (commitment_type set, due_date < now(), not promoted/dismissed/completed) and unclear ownership (suggested_owner_user_id IS NULL). Precise and source-linked — no hallucination risk.

  3. 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.

  4. Validation (Phase-1 rules reused)

    Drop items whose source_message_ids fall outside the window; any owner reference must resolve to a verified dw_external_identities row or it is nulled. Schema failure → drop bad fields, retry once, then status='failed'.

  5. 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_worker feature 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.

#ops · Daily digest 18 Jun · 08:00 · IT Agent
Progress
  • Staging deploy unblocked; client demo build shipped.
Blockers & risks
  • HighCert renewal on the VPN gateway still pending Network Infra.
  • MedNo owner confirmed for the migration runbook.
Commitment health
  • Overdue"Run staging deploy" — due last night, not promoted.
  • "Document switch-port checklist" — no owner resolved.
Suggested follow-ups
  • Nudge Network Infra on the cert; assign the migration runbook owner.
Manager-facing digest detail — the four sections with severity chips and source-linked items. Mockup only; real chrome follows the panel-ui-style guide.

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 the panel-ui-style guide.
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 the digital_worker flag; 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

CaseBehavior
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 outputDrop invalid fields, retry once, then status='failed'
No chat & no structured signalsstatus='empty', no LLM call, no notification
Overdue commitments but no chat eventsStructured-only digest (generated); narrative sections empty
Notification delivery failsDigest persists; logged; still visible in the panel
Org timezone missingUTC 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_worker flag.
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

  1. Schema + structured-only

    Add dw_digests and binding columns; generate Commitment health with no LLM call; readable in the panel.

  2. LLM narrative pass

    Add the gated digital_worker:progress_digest call for Progress, Blockers, and Follow-ups.

  3. Scheduling

    The system_cron loop with the single-owner lease and org-local due-calculation.

  4. Delivery

    The digital_worker:digest_ready notification fan-out reusing inbox-notification targeting.

  5. 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.