Просмотр исходного кода

docs: describe the workflow engine as worker-thread first

The outer ring catches up with the engine swap (the package's own
README/JSDoc rode the port commit):

- Seam module doc and README name the worker-thread engine as THE
  implementation, with isolated-vm/separate-process sandboxing as the
  deferred hardening; the seam service doc states the holder-owned-runs
  contract (engine-fiber disposal deliberately leaves live runs to
  their holders).
- Seam contract precision: agentsStarted documents the termination-path
  degradation to the host-observed count; the events section scopes the
  agent-start/agent-end pair to calls that STARTED a child run;
  WorkflowRun wording drops the vm-era abandonment language.
- The dynamic-workflows RFC is rewritten in place to the shipped
  mechanism (implemented-RFC rule): why worker threads, the thread's
  concrete buys, the in-process node:vm first cut recorded under
  alternatives considered; the tool section describes the usage policy
  as the tool's own prompt section.
- gen-doc-graphs: six workflow/* DYNAMIC_EVENT_DISPATCHERS entries (the
  catalog no longer claims nothing dispatches them) and the seam-note
  wording; core-data-structures gains its workflow.md index row;
  packages/README + AGENTS.md layout line + example cordis.yml comments
  say worker-thread; catalogs regenerated.
imccyu 2 месяцев назад
Родитель
Сommit
d5c65e2b4c

+ 1 - 1
AGENTS.md

@@ -18,7 +18,7 @@ packages/    Harness packages at packages/<group>/<pkg>/, all named @deepseek-ai
   web/         web seam + search/fetch providers + model-facing web tools
   compact/     compaction seam + basic backend
   subagent/    subagent seam + spawn/fork/ACP backends + delegation tool
-  workflow/    workflow seam + node:vm script engine + the workflow tool
+  workflow/    workflow seam + worker-thread script engine + the workflow tool
   todo/        the todo_write tool
   hooks/       Claude Code / Codex hook bridges + shared wire-protocol library
   session-persistence/  persistence seam + JSONL/SQLite backends

+ 1 - 1
docs/capability-seams.md

@@ -145,6 +145,6 @@ flowchart LR
 | `ctx.compact` | `seam` | [`compact`](../packages/compact/compact) | [`compact-basic`](../packages/compact/compact-basic) | [`compact-basic`](../packages/compact/compact-basic) | - | The basic backend currently consumes the pre-step event directly; a model-facing compact tool remains deferred. |
 | `ctx.subagents` | `seam` | [`subagent`](../packages/subagent/subagent) | [`subagent-spawn`](../packages/subagent/subagent-spawn), [`subagent-fork`](../packages/subagent/subagent-fork), [`subagent-acp`](../packages/subagent/subagent-acp), [`subagent-mock`](../packages/support/subagent-mock) | [`tool-subagent`](../packages/subagent/tool-subagent) | - | Providers implement transports; tool-subagent exposes one configured provider as a model-facing tool name. |
 | `ctx.web` | `seam` | [`web`](../packages/web/web) | [`web-search-exa`](../packages/web/web-search-exa), [`web-search-perplexity`](../packages/web/web-search-perplexity), [`web-search-deepseek`](../packages/web/web-search-deepseek), [`web-fetch-local`](../packages/web/web-fetch-local) | [`tool-web`](../packages/web/tool-web) | - | Search and fetch providers register into one ctx.web seam; tool-web owns the stable model-facing names. |
-| `ctx.workflows` | `seam` | [`workflow`](../packages/workflow/workflow) | [`workflow-vm`](../packages/workflow/workflow-vm) | [`tool-workflow`](../packages/workflow/tool-workflow) | - | One engine per context (bash shape, no named-provider registry); the vm engine fans agent() calls out through ctx.subagents. |
+| `ctx.workflows` | `seam` | [`workflow`](../packages/workflow/workflow) | [`workflow-vm`](../packages/workflow/workflow-vm) | [`tool-workflow`](../packages/workflow/tool-workflow) | - | One engine per context (bash shape, no named-provider registry); the worker-thread engine fans agent() calls out through ctx.subagents. |
 
 Maintenance mode: hybrid: services are discovered from Cordis declarations; interface/implementation/consumer roles are classified in `scripts/gen-doc-graphs.ts` with a completeness guard.

+ 4 - 4
docs/config-catalog.md

@@ -815,18 +815,18 @@ export interface Config {
   maxTotalAgents?: number
   /** Items accepted by a single `parallel()`/`pipeline()` call (default 4096). */
   maxItemsPerCall?: number
-  /** vm timeout for the script's initial synchronous slice AND the meta-literal evaluation (default 5000 ms). */
+  /** vm timeout for the initial synchronous slice (inside the worker) AND the host-side meta evaluation (default 5000 ms). */
   syncTimeoutMs?: number
   /**
    * How long after a cancellation an unsettled script may keep running before
-   * it is abandoned and `result` force-settles `cancelled` (default 5000 ms);
-   * also bounds `dispose()`.
+   * the run force-settles `cancelled` and its worker is TERMINATED (default
+   * 5000 ms); also bounds `dispose()`.
    */
   disposeGraceMs?: number
 }
 ```
 
-Source: [`packages/workflow/workflow-vm/src/index.ts:58`](../packages/workflow/workflow-vm/src/index.ts)
+Source: [`packages/workflow/workflow-vm/src/index.ts:69`](../packages/workflow/workflow-vm/src/index.ts)
 
 ## Loadable plugins with no config
 

+ 2 - 1
docs/cordis-catalog/services.md

@@ -238,12 +238,13 @@ Semantics every implementation must honor:
 - start throws synchronously for a request that cannot begin (an unparseable script, an invalid meta block). Once it returns a WorkflowRun, `result` NEVER rejects — every failure resolves with `stopReason: 'error'` (or `'cancelled'`) — and once the run is cancelled, `result` SETTLES within the implementation's bounded grace even if the script itself never settles (a consumer awaiting `result` must never be wedged past a cancellation).
 - The `workflow/*` events fire through emitWorkflowEvent (data snapshots, per-listener containment); `workflow/end` fires exactly once per started run, after `result` is settled or as it settles.
 - `dispose()` reaches quiescence within a bounded grace: it cancels, waits for the script to settle AND its started children to finish disposing, and abandons whatever is left rather than hanging its caller (the engine documents what abandonment leaves behind).
+- Runs are HOLDER-OWNED: the engine hands control (`cancel`/`dispose`) to the `start()` caller and does not track its live runs — disposing the engine's own fiber mid-run deliberately leaves those runs to their holders' teardown, so an engine reload cannot yank a run out from under the consumer awaiting it.
 
 ```ts cordis-catalog
 abstract start(request: WorkflowStartRequest): WorkflowRun
 ```
 
-Source: [`packages/workflow/workflow/src/index.ts:202`](../../packages/workflow/workflow/src/index.ts)
+Source: [`packages/workflow/workflow/src/index.ts:207`](../../packages/workflow/workflow/src/index.ts)
 
 ## Inherited `ctx` members (cordis core + loader/hmr/timer)
 

+ 1 - 0
docs/core-data-structures/core.md

@@ -24,6 +24,7 @@ Everything else is documented on a **sub-page**, not here. The rule that draws t
 | [compaction.md](compaction.md) | the compaction seam: the `compact/*` session events, `CompactionResult`, the `CompactService` interface |
 | [subagent.md](subagent.md) | the subagent seam: the named-provider registry, `SubagentStartRequest`/`Result`/`Run`, the start-time-vs-runtime capability split |
 | [web.md](web.md) | the web access seam: `WebSearchRequest`/`Result`, `WebFetchRequest`/`Result`, `WebFetchBody`, provider/capability status, `WebError` |
+| [workflow.md](workflow.md) | the workflow seam: `WorkflowStartRequest`, `WorkflowMeta`, `WorkflowRun`/`Result`, the `workflow/*` event payloads, `WorkflowError` fatality |
 
 > Type definitions on this page are pasted **verbatim** from source and drift-checked by `pnpm run verify-type-equiv` (see [development.md](../development.md#documenting-types-verbatim-ts-type-equiv)). Inline JSDoc is omitted for readability; follow the source link for the full contracts.
 

+ 2 - 2
docs/core-data-structures/workflow.md

@@ -2,7 +2,7 @@
 
 The workflow seam — an agent running a model-written orchestration SCRIPT that fans out subagents. Like [subagent](subagent.md) it is **one optional capability**, not part of the agent-loop spine, so its vocabulary lives here rather than in [core.md](core.md). Unlike the subagent registry it takes the bash shape: ONE engine implementation per context provides `ctx.workflows`; there is no named-provider registry (a second engine is a plugin swap, not a co-resident).
 
-Interface: [dsh-workflow](../../packages/workflow/workflow) (`ctx.workflows` + the vocabulary below). The implementation is [dsh-workflow-vm](../../packages/workflow/workflow-vm) (an in-process `node:vm` engine); the model-facing consumer is [dsh-tool-workflow](../../packages/workflow/tool-workflow). The proposal and rationale: [the dynamic-workflows RFC](../rfc/implemented/feature/2026-07-05-dynamic-workflows.md).
+Interface: [dsh-workflow](../../packages/workflow/workflow) (`ctx.workflows` + the vocabulary below). The implementation is [dsh-workflow-vm](../../packages/workflow/workflow-vm) (a `node:worker_threads` engine — one worker per run, the script's vm context inside it); the model-facing consumer is [dsh-tool-workflow](../../packages/workflow/tool-workflow). The proposal and rationale: [the dynamic-workflows RFC](../rfc/implemented/feature/2026-07-05-dynamic-workflows.md).
 
 Source: [`packages/workflow/workflow/src/types.ts`](../../packages/workflow/workflow/src/types.ts)
 
@@ -47,7 +47,7 @@ interface WorkflowResult {
 
 ## A live run: `WorkflowRun`
 
-The handle the consumer holds while a script executes. The consumer awaits `result`, may `cancel` mid-flight, and MUST `dispose` on every path. `result` does NOT reject — a script failure resolves with `stopReason: 'error'` — and once the run is cancelled it SETTLES within the engine's bounded grace even if the script itself never settles (the engine abandons the script and reports `cancelled`), so a consumer awaiting `result` is never wedged past a cancellation. `dispose()` = cancel + that bounded settle + child quiescence (the engine documents what abandonment leaves behind); it never hangs on a stuck script.
+The handle the consumer holds while a script executes. The consumer awaits `result`, may `cancel` mid-flight, and MUST `dispose` on every path. `result` does NOT reject — a script failure resolves with `stopReason: 'error'` — and once the run is cancelled it SETTLES within the engine's bounded grace even if the script itself never settles (the engine force-settles `cancelled`; the worker-thread engine then terminates the script's worker), so a consumer awaiting `result` is never wedged past a cancellation. `dispose()` = cancel + that bounded settle + child quiescence; it never hangs on a stuck script.
 
 ```ts type-equiv
 interface WorkflowRun {

+ 6 - 6
docs/event-producer-consumer.md

@@ -34,11 +34,11 @@ This matrix shows which packages dispatch each harness-owned event and which pac
 | `tools/change` | `emit` | [`packages/core/tools/src/index.ts:97`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`emit`) | - |
 | `tools/post-execute` | `waterfall` | [`packages/core/tools/src/index.ts:92`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) |
 | `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/index.ts:76`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) |
-| `workflow/agent-end` | `emit` | [`packages/workflow/workflow/src/index.ts:93`](../packages/workflow/workflow/src/index.ts) | - | - |
-| `workflow/agent-start` | `emit` | [`packages/workflow/workflow/src/index.ts:85`](../packages/workflow/workflow/src/index.ts) | - | - |
-| `workflow/end` | `emit` | [`packages/workflow/workflow/src/index.ts:103`](../packages/workflow/workflow/src/index.ts) | - | - |
-| `workflow/log` | `emit` | [`packages/workflow/workflow/src/index.ts:77`](../packages/workflow/workflow/src/index.ts) | - | - |
-| `workflow/phase` | `emit` | [`packages/workflow/workflow/src/index.ts:70`](../packages/workflow/workflow/src/index.ts) | - | - |
-| `workflow/start` | `emit` | [`packages/workflow/workflow/src/index.ts:62`](../packages/workflow/workflow/src/index.ts) | - | - |
+| `workflow/agent-end` | `emit` | [`packages/workflow/workflow/src/index.ts:93`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | - |
+| `workflow/agent-start` | `emit` | [`packages/workflow/workflow/src/index.ts:85`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | - |
+| `workflow/end` | `emit` | [`packages/workflow/workflow/src/index.ts:103`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | - |
+| `workflow/log` | `emit` | [`packages/workflow/workflow/src/index.ts:77`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | - |
+| `workflow/phase` | `emit` | [`packages/workflow/workflow/src/index.ts:70`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | - |
+| `workflow/start` | `emit` | [`packages/workflow/workflow/src/index.ts:62`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | - |
 
 Maintenance mode: hybrid generated: Cordis event declarations and most producer/listener edges are AST-scanned; dynamic dispatch sites are classified in `scripts/gen-doc-graphs.ts`.

Разница между файлами не показана из-за своего большого размера
+ 2 - 3
docs/rfc/implemented/feature/2026-07-05-dynamic-workflows.md


+ 4 - 4
examples/acp-agent/cordis.yml

@@ -82,10 +82,10 @@
     toolName: subagent_fork
 
 
-# Dynamic workflows: the node:vm engine (ctx.workflows) over the spawn subagent
-# backend above, plus the model-facing `workflow` tool. The model writes a
-# JavaScript orchestration script (meta + body); the engine runs it in-process
-# and fans agent() calls out as spawn children.
+# Dynamic workflows: the worker-thread engine (ctx.workflows) over the spawn
+# subagent backend above, plus the model-facing `workflow` tool. The model
+# writes a JavaScript orchestration script (meta + body); the engine runs it
+# in its own worker thread and fans agent() calls out as spawn children.
 - id: workflow-vm
   name: '@deepseek-ai/dsh-workflow-vm'
   config:

+ 4 - 4
examples/coding-agent/cordis.yml

@@ -103,10 +103,10 @@
     toolName: subagent_fork
 
 
-# Dynamic workflows: the node:vm engine (ctx.workflows) over the spawn subagent
-# backend above, plus the model-facing `workflow` tool. The model writes a
-# JavaScript orchestration script (meta + body); the engine runs it in-process
-# and fans agent() calls out as spawn children.
+# Dynamic workflows: the worker-thread engine (ctx.workflows) over the spawn
+# subagent backend above, plus the model-facing `workflow` tool. The model
+# writes a JavaScript orchestration script (meta + body); the engine runs it
+# in its own worker thread and fans agent() calls out as spawn children.
 - id: workflow-vm
   name: '@deepseek-ai/dsh-workflow-vm'
   config:

+ 1 - 1
packages/README.md

@@ -14,7 +14,7 @@ Packages are grouped by modular role at `packages/<group>/<pkg>/`. The group dir
 | [`fs/`](fs/README.md) | Filesystem capability family: the abstract seam, a local impl, and the model-facing file tools | Product — stable surface |
 | [`compact/`](compact/README.md) | Compaction capability family: the abstract seam + a basic backend (tool deferred) | Product — stable surface |
 | [`subagent/`](subagent/README.md) | Subagent capability family: the provider-registry seam and the model-facing delegation tool | Product — stable surface |
-| [`workflow/`](workflow/README.md) | Workflow capability family: the script-engine seam, the node:vm engine, and the model-facing `workflow` tool | Product — stable surface |
+| [`workflow/`](workflow/README.md) | Workflow capability family: the script-engine seam, the worker-thread engine, and the model-facing `workflow` tool | Product — stable surface |
 | [`web/`](web/README.md) | Web capability family: the abstract seam, search/fetch provider impls, and the model-facing web tools | Product — stable surface |
 | [`todo/`](todo/README.md) | Todo/planning family: the model-facing `todo_write` tool (whole-list task tracking on the session log) | Product — stable surface |
 | [`hooks/`](hooks/README.md) | Hook bridges + the shared Claude Code / Codex wire-protocol library | Product — stable surface |

+ 2 - 2
packages/workflow/README.md

@@ -5,9 +5,9 @@ The workflow seam: a model-written JavaScript orchestration script that fans out
 | Package | Role | ctx key |
 |---|---|---|
 | `workflow/` | Abstract workflow seam: service base class + run vocabulary + `workflow/*` events | `ctx.workflows` |
-| `workflow-vm/` | In-process `node:vm` engine: parses the script, injects the hooks, drives `ctx.subagents` | (provides `ctx.workflows`) |
+| `workflow-vm/` | `node:worker_threads` engine: one worker per run; the script's vm context lives inside the worker, `agent()` bridges to `ctx.subagents` over the message port | (provides `ctx.workflows`) |
 | `tool-workflow/` | Model-facing `workflow` tool over `ctx.workflows` | (registers on `ctx.tools`) |
 
-The interface lives at `workflow/workflow/`. The engine's `agent()` hook rides the [subagent seam](../subagent/README.md) (any registered provider; the shipped examples use `spawn`), and `agent({ schema })` rides the structured-output support the in-process backends implement. The seam split exists for engine hardening: `node:vm` is in-process and cannot kill a pathological synchronous spin — a worker-thread or isolated-vm engine swaps in behind the same interface if that ever matters.
+The interface lives at `workflow/workflow/`. The engine's `agent()` hook rides the [subagent seam](../subagent/README.md) (any registered provider; the shipped examples use `spawn`), and `agent({ schema })` rides the structured-output support the in-process backends implement. The worker thread isolates the SCRIPT — the host never blocks on it, and a cancelled run's post-grace termination is real — but it is NOT a security boundary; an isolated-vm/separate-process engine (actual sandboxing) swaps in behind the same interface if that ever matters.
 
 The proposal, decisions, and deferred work: [docs/rfc/implemented/feature/2026-07-05-dynamic-workflows.md](../../docs/rfc/implemented/feature/2026-07-05-dynamic-workflows.md).

+ 3 - 3
packages/workflow/workflow/README.md

@@ -1,10 +1,10 @@
 # @deepseek-ai/dsh-workflow
 
-The **workflow seam** (`ctx.workflows`): an abstract service defining WHAT a workflow engine does — execute a model-written orchestration script that fans out subagents — without saying HOW. The bash-shaped third of the [workflow family](../README.md): implementations subclass `WorkflowService` and register as the `workflows` service (one per context); [`dsh-workflow-vm`](../workflow-vm/README.md) is the first, and [`dsh-tool-workflow`](../tool-workflow/README.md) is the model-facing consumer.
+The **workflow seam** (`ctx.workflows`): an abstract service defining WHAT a workflow engine does — execute a model-written orchestration script that fans out subagents — without saying HOW. The bash-shaped third of the [workflow family](../README.md): implementations subclass `WorkflowService` and register as the `workflows` service (one per context); [`dsh-workflow-vm`](../workflow-vm/README.md) (one worker thread per run) is the implementation, and [`dsh-tool-workflow`](../tool-workflow/README.md) is the model-facing consumer.
 
 ## Service: `WorkflowService` (abstract)
 
-`start(request: WorkflowStartRequest): WorkflowRun` — parse and execute a script. Throws synchronously (`SCRIPT_PARSE`/`META_INVALID`) for a script that cannot begin; once a run is returned, its `result` NEVER rejects — every failure resolves with `stopReason: 'error'` (or `'cancelled'`) — and once the run is cancelled, `result` settles within the implementation's bounded grace even if the script itself never settles (a consumer awaiting `result` must never be wedged past a cancellation). `dispose()` must reach quiescence within a bounded grace (cancel → wait for the script to settle and its children to finish disposing → abandon), never hanging its caller.
+`start(request: WorkflowStartRequest): WorkflowRun` — parse and execute a script. Throws synchronously (`SCRIPT_PARSE`/`META_INVALID`) for a script that cannot begin; once a run is returned, its `result` NEVER rejects — every failure resolves with `stopReason: 'error'` (or `'cancelled'`) — and once the run is cancelled, `result` settles within the implementation's bounded grace even if the script itself never settles (a consumer awaiting `result` must never be wedged past a cancellation). `dispose()` must reach quiescence within a bounded grace (cancel → wait for the script to settle and its children to finish disposing → abandon), never hanging its caller. Runs are HOLDER-owned: the engine does not track its live runs, so disposing the engine's fiber mid-run leaves each run to its holder's teardown.
 
 The protected `emitWorkflowEvent` helper dispatches the `workflow/*` events with PER-LISTENER containment and PER-LISTENER payload snapshots (a throwing subscriber is logged, never propagated, and cannot starve later listeners; each subscriber gets its own clone of the payload, so mutating it corrupts neither the engine nor other listeners) — the same containment guarantee as the subagent seam's lifecycle emits.
 
@@ -22,7 +22,7 @@ All observe-only emits carrying DATA SNAPSHOTS (`WorkflowRunInfo` = id + meta) 
 
 - `workflow/start`(info) / `workflow/end`(info, resultInfo) — run lifecycle; `resultInfo` deliberately omits the value.
 - `workflow/phase`(info, title) / `workflow/log`(info, message) — script narration.
-- `workflow/agent-start`(info, agent) / `workflow/agent-end`(info, agent + outcome) — one pair per `agent()` call, correlated by `seq`.
+- `workflow/agent-start`(info, agent) / `workflow/agent-end`(info, agent + outcome) — one pair per `agent()` call that STARTED a child run (a call rejected at validation or caps, refused at start, or cancelled while queued for a slot emits no pair), correlated by `seq`.
 
 ## Non-goals (this cut)
 

+ 9 - 4
packages/workflow/workflow/src/index.ts

@@ -4,10 +4,10 @@
  * that fans out subagents — without saying HOW. Implementations subclass
  * {@link WorkflowService} and register as the `workflows` service (one
  * implementation per context, cordis' standard duplicate-service behavior);
- * `@deepseek-ai/dsh-workflow-vm` (an in-process `node:vm` engine) is the
- * first. Future engines (a worker-thread or isolated-vm sandbox) swap in
- * without touching the model-facing tool that consumes them
- * (`@deepseek-ai/dsh-tool-workflow`).
+ * the implementation is `@deepseek-ai/dsh-workflow-vm`, which runs each
+ * script in its own worker thread. Hardened engines (an isolated-vm or
+ * separate-process sandbox) swap in without touching the model-facing tool
+ * that consumes them (`@deepseek-ai/dsh-tool-workflow`).
  *
  * The `workflow/*` lifecycle events are OBSERVE-ONLY data snapshots: they
  * carry {@link WorkflowRunInfo} (id + meta), never the live {@link WorkflowRun}
@@ -198,6 +198,11 @@ export function isFatalWorkflowError(error: unknown): boolean {
  *   for the script to settle AND its started children to finish disposing,
  *   and abandons whatever is left rather than hanging its caller (the engine
  *   documents what abandonment leaves behind).
+ * - Runs are HOLDER-OWNED: the engine hands control (`cancel`/`dispose`) to
+ *   the `start()` caller and does not track its live runs — disposing the
+ *   engine's own fiber mid-run deliberately leaves those runs to their
+ *   holders' teardown, so an engine reload cannot yank a run out from under
+ *   the consumer awaiting it.
  */
 export abstract class WorkflowService extends Service {
   constructor(ctx: Context) {

+ 14 - 7
packages/workflow/workflow/src/types.ts

@@ -89,7 +89,13 @@ export interface WorkflowResult {
   stopReason: WorkflowStopReason
   /** The failure message (present iff `stopReason` is not `completed`). */
   error?: string
-  /** How many `agent()` calls the run accepted (whole lifetime, including calls still queued for a slot when the run was cancelled). */
+  /**
+   * How many `agent()` calls the run accepted over its whole lifetime. On a
+   * graceful settlement this is the script-side count (calls still queued for
+   * a concurrency slot included); on a termination path (grace force-settle,
+   * worker death) it degrades to the host-observed count — calls queued
+   * inside a terminated script are unknowable then.
+   */
   agentsStarted: number
 }
 
@@ -98,18 +104,19 @@ export interface WorkflowResult {
  * `result`, may `cancel` mid-flight, and MUST `dispose` on every path.
  * `result` does NOT reject — a script failure resolves with `stopReason:
  * 'error'` — and once the run is cancelled it SETTLES within the engine's
- * bounded grace even if the script itself never settles (the engine abandons
- * the script and reports `cancelled`), so a consumer awaiting `result` is
- * never wedged past a cancellation. `dispose()` = cancel + that bounded
- * settle + child quiescence; it never hangs on a stuck script and is safe to
- * call on every path (idempotent).
+ * bounded grace even if the script itself never settles (the engine
+ * force-settles `cancelled`; what becomes of the script is engine-documented
+ * — the worker-thread engine terminates its worker), so a consumer awaiting
+ * `result` is never wedged past a cancellation. `dispose()` = cancel + that
+ * bounded settle + child quiescence; it never hangs on a stuck script and is
+ * safe to call on every path (idempotent).
  */
 export interface WorkflowRun {
   readonly id: WorkflowRunId
   /** The validated meta block (available before the body runs). */
   readonly meta: WorkflowMeta
   readonly result: Promise<WorkflowResult>
-  /** Cancel the run: children abort, pending hooks reject, the script dies at its next await (or is abandoned at the grace). */
+  /** Cancel the run: children abort, pending hooks reject, the script dies at its next await (or is force-settled at the grace). */
   cancel(reason?: string): void
   /** Cancel + bounded-grace settle; safe to call on every path (idempotent). */
   dispose(): Promise<void>

+ 9 - 1
scripts/gen-doc-graphs.ts

@@ -193,7 +193,7 @@ const SERVICE_ROLES: ServiceRole[] = [
     mode: 'seam',
     implementations: ['workflow-vm'],
     consumers: ['tool-workflow'],
-    note: 'One engine per context (bash shape, no named-provider registry); the vm engine fans agent() calls out through ctx.subagents.',
+    note: 'One engine per context (bash shape, no named-provider registry); the worker-thread engine fans agent() calls out through ctx.subagents.',
   },
 ]
 
@@ -203,6 +203,14 @@ const DYNAMIC_EVENT_DISPATCHERS: Array<{ event: string; pkg: string; method: str
   // listeners or strand an already-started child run.
   { event: 'subagent/start', pkg: 'subagent', method: 'events.dispatch' },
   { event: 'subagent/end', pkg: 'subagent', method: 'events.dispatch' },
+  // The workflow/* lifecycle events dispatch the same way, for the same
+  // per-listener-containment reason (WorkflowService.emitWorkflowEvent).
+  { event: 'workflow/start', pkg: 'workflow', method: 'events.dispatch' },
+  { event: 'workflow/phase', pkg: 'workflow', method: 'events.dispatch' },
+  { event: 'workflow/log', pkg: 'workflow', method: 'events.dispatch' },
+  { event: 'workflow/agent-start', pkg: 'workflow', method: 'events.dispatch' },
+  { event: 'workflow/agent-end', pkg: 'workflow', method: 'events.dispatch' },
+  { event: 'workflow/end', pkg: 'workflow', method: 'events.dispatch' },
 ]
 
 function generatedHeader(title: string): string[] {

Некоторые файлы не были показаны из-за большого количества измененных файлов