Add the activity capability seam: ctx.activities is a pure observation
plane where long-running work (background bash/pwsh, workflow runs) opens a
handle, appends transient output into a byte-offset ring buffer, and settles
exactly once. Nothing model-visible changes: reads are non-consuming and
lossy-by-design (absolute UTF-8 offsets survive eviction and report gaps),
correlation carries {callId, jobId}, and visibility is owner-fenced like jobs.
The seam is strictly optional. Producers tap it through ctx.get('activities')
and contain every observation failure, so an unregistered or broken registry
never affects the underlying job (proven by broken-registry tests).
Ship the full path in one change:
- @deepseek-ai/dsh-activity: Service Definition, pumpActivityOutput pull
substrate, invariant companion; dsh-activity-local: ring-buffer provider
with retention config and owner cleanup.
- shell: ShellProcess.observed exposes non-consuming stdout/stderr readers;
tool-bash/tool-pwsh mirror background jobs into activities.
- tool-workflow: phase/log/agent lines mirrored as a workflow activity.
- api-activity-controller: Typert Remote control (roster baseline+buckets)
and observe (opened/output/status frames, coalesced and byte-capped)
streams plus the client feed service with reference-counted observation.
- ui-activity: session-header action rendering live output through
TerminalBlock, which now shows output while a command is still running.
Docs: activity subsystem page, architecture row, regenerated catalogs, and
the Agent Note (2026-08-24-activity-observation-seam), all bilingual. A
keyless web e2e replays a real run_in_background stream to ARIA goldens.
2.3 KiB
@deepseek-ai/dsh-api-activity-controller
English | 中文
Activity Remote streams over the optional ctx.activities registry, plus their React-free client model. A pure read surface: it starts nothing, cancels nothing, and never touches the model-facing consuming cursors — the wire path reads only the registry's non-consuming offsets.
The Host ActivityController (namespace activity) exposes two @Remote({ mode: 'stream' }) methods:
control(signal)— one complete roster baseline, then whole-bucket replacement frames per owner session on every registry change (the self-healing shape the session control stream uses for jobs). A composition without the registry serves an empty baseline.observe({ activityId, from? }, signal)— oneopenedanchor, coalescedoutputframes (flushMswindow,maxFrameBytessoft budget; one larger chunk ships whole), then one terminalstatusafter the settled activity is drained, after which the generation closes normally. Reconnecting callers resume by passing the last frame'snext; a resume behind the retained window arrives flaggedlossy. Status rides the same stream as output, so settlement can never race a still-open output channel.
| Config | Default | Meaning |
|---|---|---|
flushMs |
100 |
Coalescing window after new output before an observation read. |
maxFrameBytes |
65536 |
Soft byte budget per observation output frame. |
The client half (./client) installs ctx.activityFeed: ClientActivityModel mirrors the roster (last-wins buckets, generation replace) and accumulates each observed activity's bounded render tail, and ClientActivityFeed.observe(id) opens reference-counted observation streams that resume from the model's cursor across carrier generations.
Model Experience
None, as activity transport is browser and Host observation state and registers no prompt, tool, or session event.
KV Cache effect
None; the package never assembles or alters provider requests.
Known Limitations and Deferred Work
- Registry presence is sampled at construction — a registry mounted after the controller stays unobserved until the controller reloads.
- The roster names no watermark — an observation stream anchors itself; correlating roster rows against observation offsets is client policy.