@hyperdrive.bot/paseo-server 0.3.41 → 0.3.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/server/server/agent/agent-manager.d.ts +15 -0
  2. package/dist/server/server/agent/agent-manager.js +142 -25
  3. package/dist/server/server/agent/mcp-server.js +6 -1
  4. package/dist/server/server/agent/providers/claude/background-task-tracker.d.ts +38 -1
  5. package/dist/server/server/agent/providers/claude/background-task-tracker.js +114 -9
  6. package/dist/server/server/agent/providers/claude/background-work-kinds.d.ts +95 -0
  7. package/dist/server/server/agent/providers/claude/background-work-kinds.js +73 -0
  8. package/dist/server/server/agent/providers/claude/pty-session-launcher.js +3 -0
  9. package/dist/server/server/agent/providers/claude/transport/pty.d.ts +17 -0
  10. package/dist/server/server/agent/providers/claude/transport/pty.js +51 -1
  11. package/dist/server/server/agent/providers/claude/transport/tmux.d.ts +74 -0
  12. package/dist/server/server/agent/providers/claude/transport/tmux.js +157 -0
  13. package/dist/server/server/agent/providers/claude/transport/types.d.ts +6 -0
  14. package/dist/server/server/agent/providers/opencode-agent.d.ts +7 -0
  15. package/dist/server/server/agent/providers/opencode-agent.js +51 -1
  16. package/dist/server/server/bootstrap.js +1 -0
  17. package/dist/server/server/session.js +1 -0
  18. package/dist/server/server/workflow/workflow-agent-resolution.d.ts +82 -0
  19. package/dist/server/server/workflow/workflow-agent-resolution.js +105 -0
  20. package/dist/server/server/workflow/workflow-manager.d.ts +155 -20
  21. package/dist/server/server/workflow/workflow-manager.js +439 -31
  22. package/dist/server/server/workflow/workflow-progress.d.ts +53 -0
  23. package/dist/server/server/workflow/workflow-progress.js +96 -0
  24. package/dist/server/server/workspace-directory.js +32 -14
  25. package/dist/server/web-ui/_expo/static/js/web/{index-cb251ddad56c08c3021036af3a43fc0f.js → index-2ff48a7009ad2309c577d5a43b925f69.js} +18 -18
  26. package/dist/server/web-ui/_expo/static/js/web/index-2ff48a7009ad2309c577d5a43b925f69.js.br +0 -0
  27. package/dist/server/web-ui/_expo/static/js/web/index-2ff48a7009ad2309c577d5a43b925f69.js.gz +0 -0
  28. package/dist/server/web-ui/_expo/static/js/web/{index-cb251ddad56c08c3021036af3a43fc0f.js.map.br → index-2ff48a7009ad2309c577d5a43b925f69.js.map.br} +0 -0
  29. package/dist/server/web-ui/_expo/static/js/web/{index-cb251ddad56c08c3021036af3a43fc0f.js.map.gz → index-2ff48a7009ad2309c577d5a43b925f69.js.map.gz} +0 -0
  30. package/dist/server/web-ui/index.html +1 -1
  31. package/dist/server/web-ui/index.html.br +0 -0
  32. package/dist/server/web-ui/index.html.gz +0 -0
  33. package/package.json +6 -6
  34. package/dist/server/web-ui/_expo/static/js/web/index-cb251ddad56c08c3021036af3a43fc0f.js.br +0 -0
  35. package/dist/server/web-ui/_expo/static/js/web/index-cb251ddad56c08c3021036af3a43fc0f.js.gz +0 -0
@@ -0,0 +1,82 @@
1
+ import type { WorkflowAgentPreset } from "@hyperdrive.bot/paseo-protocol/messages";
2
+ import type { AgentSessionConfig } from "../agent/agent-sdk-types.js";
3
+ import type { TaskGraph, TaskNode } from "./workflow-manager.js";
4
+ /**
5
+ * workflow-agent-resolution — Epic 2, Story 2.1.
6
+ *
7
+ * Per-step agent selection. Before this module every child of a workflow was spawned
8
+ * with the one graph-level `baseConfig`, so `TaskNode.agentType` was a declared field
9
+ * with no reader: delegation to a different agent per step was simply not supported.
10
+ * This module is that reader.
11
+ *
12
+ * Why it is PURE and lives outside `workflow-manager.ts` (the `workflow-progress.ts`
13
+ * precedent):
14
+ * - **Unit-testable with no harness.** Precedence is a function of three plain objects;
15
+ * proving it should not require an `AgentManager`, a temp dir or a spawned child.
16
+ * - **The manager stays an orchestrator, not a calculator.** One helper serves BOTH
17
+ * spawn call sites (`startWorkflow`'s readiness loop and `continueWorkflow`), so the
18
+ * precedence rule exists exactly once.
19
+ * - **No runtime cycle.** Every import above is type-only and therefore erased, so the
20
+ * manager can import this module as a VALUE. `WorkflowUnknownAgentError` is declared
21
+ * here (the leaf) and re-exported from the manager, exactly as `WorkflowCycleError`
22
+ * is declared in `workflow-progress.ts` and re-exported.
23
+ *
24
+ * Why the merge copies only DEFINED keys rather than spreading:
25
+ * `{ ...base, ...preset, ...override }` lets an explicit `undefined` in a higher tier
26
+ * ERASE a defined lower-tier value — `{ model: undefined }` on a task would blank the
27
+ * workflow's model instead of leaving it alone. A tier states what it overrides; a key
28
+ * it does not set must be invisible.
29
+ *
30
+ * Why `cwd` is forced back to `baseConfig.cwd` as the final statement:
31
+ * the working directory is workflow-owned. A step relocating its own child would break
32
+ * the containment every other surface assumes. There are two independent guards, on
33
+ * purpose: `WorkflowAgentPresetSchema` rejects `cwd` at parse time (so it can never
34
+ * arrive over the wire), and this assignment wins over any hand-built object literal
35
+ * that bypassed the schema.
36
+ *
37
+ * Determinism rules, mirroring `workflow-progress.ts`: synchronous, no I/O, no
38
+ * `Date.now()` / `new Date()`, no snapshot read, no module-level mutable state.
39
+ */
40
+ /** Thrown when a task's `agentType` names an agent absent from the workflow's presets. */
41
+ export declare class WorkflowUnknownAgentError extends Error {
42
+ readonly workflowId: string | undefined;
43
+ readonly taskId: string;
44
+ readonly agentName: string;
45
+ readonly knownAgents: string[];
46
+ constructor(workflowId: string | undefined, taskId: string, agentName: string, knownAgents: string[]);
47
+ }
48
+ /**
49
+ * Create-time validation: reject a graph whose any task names an agent nobody defined.
50
+ *
51
+ * Reports the FIRST offender in `graph.tasks` array order, so the error is stable for
52
+ * a given graph rather than dependent on object-key iteration. The message names the
53
+ * task id, the unknown name and the known names, because both launch boundaries
54
+ * (`session.ts` RPC, `mcp-server.ts` tool) forward `error.message` verbatim to the
55
+ * operator and that string is the whole diagnosis.
56
+ *
57
+ * Own-key lookup, never a bare `in`: a preset named `toString` or `constructor` must
58
+ * not be satisfied by `Object.prototype`.
59
+ */
60
+ export declare function assertKnownAgents(args: {
61
+ graph: TaskGraph;
62
+ agentPresets: Record<string, WorkflowAgentPreset>;
63
+ workflowId?: string;
64
+ }): void;
65
+ /**
66
+ * Resolve the `AgentSessionConfig` one task's child is spawned with.
67
+ *
68
+ * Precedence, lowest to highest: `baseConfig`, then `agentPresets[node.agentType]`,
69
+ * then `node.agentConfig`. Only keys whose value is `!== undefined` are copied from a
70
+ * higher tier, so a preset that sets only `model` leaves `baseConfig.provider` intact.
71
+ *
72
+ * TOTAL by design: an `agentType` naming a missing preset returns `{ ...baseConfig }`
73
+ * rather than throwing. That path is unreachable after `assertKnownAgents` runs at
74
+ * create time, and a helper that threw on it would convert a validation gap into a
75
+ * mid-graph spawn crash.
76
+ */
77
+ export declare function resolveTaskConfig(args: {
78
+ node: TaskNode;
79
+ baseConfig: AgentSessionConfig;
80
+ agentPresets: Record<string, WorkflowAgentPreset>;
81
+ }): AgentSessionConfig;
82
+ //# sourceMappingURL=workflow-agent-resolution.d.ts.map
@@ -0,0 +1,105 @@
1
+ /**
2
+ * workflow-agent-resolution — Epic 2, Story 2.1.
3
+ *
4
+ * Per-step agent selection. Before this module every child of a workflow was spawned
5
+ * with the one graph-level `baseConfig`, so `TaskNode.agentType` was a declared field
6
+ * with no reader: delegation to a different agent per step was simply not supported.
7
+ * This module is that reader.
8
+ *
9
+ * Why it is PURE and lives outside `workflow-manager.ts` (the `workflow-progress.ts`
10
+ * precedent):
11
+ * - **Unit-testable with no harness.** Precedence is a function of three plain objects;
12
+ * proving it should not require an `AgentManager`, a temp dir or a spawned child.
13
+ * - **The manager stays an orchestrator, not a calculator.** One helper serves BOTH
14
+ * spawn call sites (`startWorkflow`'s readiness loop and `continueWorkflow`), so the
15
+ * precedence rule exists exactly once.
16
+ * - **No runtime cycle.** Every import above is type-only and therefore erased, so the
17
+ * manager can import this module as a VALUE. `WorkflowUnknownAgentError` is declared
18
+ * here (the leaf) and re-exported from the manager, exactly as `WorkflowCycleError`
19
+ * is declared in `workflow-progress.ts` and re-exported.
20
+ *
21
+ * Why the merge copies only DEFINED keys rather than spreading:
22
+ * `{ ...base, ...preset, ...override }` lets an explicit `undefined` in a higher tier
23
+ * ERASE a defined lower-tier value — `{ model: undefined }` on a task would blank the
24
+ * workflow's model instead of leaving it alone. A tier states what it overrides; a key
25
+ * it does not set must be invisible.
26
+ *
27
+ * Why `cwd` is forced back to `baseConfig.cwd` as the final statement:
28
+ * the working directory is workflow-owned. A step relocating its own child would break
29
+ * the containment every other surface assumes. There are two independent guards, on
30
+ * purpose: `WorkflowAgentPresetSchema` rejects `cwd` at parse time (so it can never
31
+ * arrive over the wire), and this assignment wins over any hand-built object literal
32
+ * that bypassed the schema.
33
+ *
34
+ * Determinism rules, mirroring `workflow-progress.ts`: synchronous, no I/O, no
35
+ * `Date.now()` / `new Date()`, no snapshot read, no module-level mutable state.
36
+ */
37
+ /** Thrown when a task's `agentType` names an agent absent from the workflow's presets. */
38
+ export class WorkflowUnknownAgentError extends Error {
39
+ constructor(workflowId, taskId, agentName, knownAgents) {
40
+ super(`Task ${taskId} names unknown agent '${agentName}'. Known agents: ${knownAgents.length > 0 ? [...knownAgents].sort().join(", ") : "none"}.`);
41
+ this.workflowId = workflowId;
42
+ this.taskId = taskId;
43
+ this.agentName = agentName;
44
+ this.knownAgents = knownAgents;
45
+ this.name = "WorkflowUnknownAgentError";
46
+ }
47
+ }
48
+ /**
49
+ * Create-time validation: reject a graph whose any task names an agent nobody defined.
50
+ *
51
+ * Reports the FIRST offender in `graph.tasks` array order, so the error is stable for
52
+ * a given graph rather than dependent on object-key iteration. The message names the
53
+ * task id, the unknown name and the known names, because both launch boundaries
54
+ * (`session.ts` RPC, `mcp-server.ts` tool) forward `error.message` verbatim to the
55
+ * operator and that string is the whole diagnosis.
56
+ *
57
+ * Own-key lookup, never a bare `in`: a preset named `toString` or `constructor` must
58
+ * not be satisfied by `Object.prototype`.
59
+ */
60
+ export function assertKnownAgents(args) {
61
+ for (const task of args.graph.tasks) {
62
+ const agentName = task.agentType;
63
+ if (!agentName) {
64
+ continue;
65
+ }
66
+ if (!Object.prototype.hasOwnProperty.call(args.agentPresets, agentName)) {
67
+ throw new WorkflowUnknownAgentError(args.workflowId, task.id, agentName, Object.keys(args.agentPresets));
68
+ }
69
+ }
70
+ }
71
+ /**
72
+ * Resolve the `AgentSessionConfig` one task's child is spawned with.
73
+ *
74
+ * Precedence, lowest to highest: `baseConfig`, then `agentPresets[node.agentType]`,
75
+ * then `node.agentConfig`. Only keys whose value is `!== undefined` are copied from a
76
+ * higher tier, so a preset that sets only `model` leaves `baseConfig.provider` intact.
77
+ *
78
+ * TOTAL by design: an `agentType` naming a missing preset returns `{ ...baseConfig }`
79
+ * rather than throwing. That path is unreachable after `assertKnownAgents` runs at
80
+ * create time, and a helper that threw on it would convert a validation gap into a
81
+ * mid-graph spawn crash.
82
+ */
83
+ export function resolveTaskConfig(args) {
84
+ const resolved = { ...args.baseConfig };
85
+ // Keyed write surface for the tier copy below. `AgentSessionConfig` has no index
86
+ // signature, so a per-key assignment needs this widening; the preset schema is a
87
+ // closed subset of that interface, so every key copied is a real config key.
88
+ const target = resolved;
89
+ const preset = args.node.agentType ? args.agentPresets[args.node.agentType] : undefined;
90
+ for (const tier of [preset, args.node.agentConfig]) {
91
+ if (!tier) {
92
+ continue;
93
+ }
94
+ for (const [key, value] of Object.entries(tier)) {
95
+ if (value !== undefined) {
96
+ target[key] = value;
97
+ }
98
+ }
99
+ }
100
+ // `cwd` is workflow-owned and never overridable. Two independent guards: the schema
101
+ // rejects `cwd` at parse time, and this assignment wins over any hand-built object.
102
+ resolved.cwd = args.baseConfig.cwd;
103
+ return resolved;
104
+ }
105
+ //# sourceMappingURL=workflow-agent-resolution.js.map
@@ -1,16 +1,28 @@
1
- import type { WorkflowLifecycleEvent, WorkflowSnapshot } from "@hyperdrive.bot/paseo-protocol/messages";
1
+ import type { WorkflowAgentPreset, WorkflowLifecycleEvent, WorkflowSnapshot } from "@hyperdrive.bot/paseo-protocol/messages";
2
2
  import type { AgentManager, ManagedAgent } from "../agent/agent-manager.js";
3
3
  import type { AgentSessionConfig } from "../agent/agent-sdk-types.js";
4
4
  import type { WorkflowStorage } from "./workflow-storage.js";
5
5
  export { WorkflowCycleError } from "./workflow-progress.js";
6
+ export { WorkflowInvalidGraphError } from "./workflow-progress.js";
7
+ export { WorkflowUnknownAgentError } from "./workflow-agent-resolution.js";
6
8
  export declare const WORKFLOW_PROJECTION_COALESCE_WINDOW_MS = 60;
7
- /** Injectable timer surface defaults to the globals; overridable for deterministic tests. */
9
+ /** Injectable timer surface - defaults to the globals; overridable for deterministic tests. */
8
10
  export interface WorkflowProjectionTimers {
9
11
  setTimeout: (callback: () => void, ms?: number) => ReturnType<typeof setTimeout>;
10
12
  clearTimeout: (handle: ReturnType<typeof setTimeout>) => void;
11
13
  }
12
14
  /**
13
- * WorkflowManager Epic 1, Story 1.2.
15
+ * Minimal logging surface: a pino `Logger` satisfies it structurally.
16
+ *
17
+ * `error` is declared as a METHOD shorthand (bivariant) rather than a function
18
+ * property, so both a pino `Logger` and a plain test stub are assignable.
19
+ */
20
+ export interface WorkflowManagerLogger {
21
+ error(context: Record<string, unknown>, message: string): void;
22
+ trace(context: Record<string, unknown>, message: string): void;
23
+ }
24
+ /**
25
+ * WorkflowManager - Epic 1, Story 1.2.
14
26
  *
15
27
  * The daemon-owned orchestration core. It decomposes a task-graph into child agents
16
28
  * spawned through the EXISTING `agentManager.createAgent` lifecycle, so every child
@@ -41,12 +53,14 @@ export interface TaskNode {
41
53
  estimatedMinutes: number;
42
54
  /** Optional agent type to use (dev, architect, qa, etc.). */
43
55
  agentType?: string;
56
+ /** Optional inline config override for this task; highest precedence. */
57
+ agentConfig?: WorkflowAgentPreset;
44
58
  /** Optional specific files this task operates on (per-file tasks). */
45
59
  targetFiles?: string[];
46
60
  }
47
61
  /** Metadata about the task graph. */
48
62
  export interface TaskGraphMetadata {
49
- /** Execution layers each layer runs in parallel, layers run sequentially. */
63
+ /** Execution layers - each layer runs in parallel, layers run sequentially. */
50
64
  executionLayers: string[][];
51
65
  /** Maximum number of tasks that can run in parallel. */
52
66
  maxParallelism: number;
@@ -87,13 +101,15 @@ export interface CreateWorkflowInput {
87
101
  graph: TaskGraph;
88
102
  /** Base session config (provider/model template) each child is spawned with. */
89
103
  baseConfig: AgentSessionConfig;
104
+ /** Optional named agent presets; a task's `agentType` selects one. Persisted on the snapshot (Story 2.2). */
105
+ agentPresets?: Record<string, WorkflowAgentPreset>;
90
106
  /** Optional explicit workflow id (defaults to a random UUID). */
91
107
  id?: string;
92
108
  /** Optional display title (defaults to the graph goal). */
93
109
  title?: string | null;
94
110
  /** Optional labels copied onto every spawned child alongside the workflow id. */
95
111
  labels?: Record<string, string>;
96
- /** Optional owning agent id children also carry PARENT_AGENT_ID_LABEL. */
112
+ /** Optional owning agent id - children also carry PARENT_AGENT_ID_LABEL. */
97
113
  parentAgentId?: string;
98
114
  }
99
115
  /** Event emitted to workflow subscribers whenever a snapshot changes. */
@@ -121,11 +137,13 @@ export declare class WorkflowManager {
121
137
  private readonly coalesceWindowMs;
122
138
  private readonly projectionTimers;
123
139
  private readonly unsubscribeAgents;
140
+ private readonly logger?;
124
141
  constructor(options: {
125
142
  agentManager: AgentManager;
126
143
  storage: WorkflowStorage;
127
144
  coalesceWindowMs?: number;
128
145
  timers?: WorkflowProjectionTimers;
146
+ logger?: WorkflowManagerLogger;
129
147
  });
130
148
  /**
131
149
  * Detach the AgentManager subscription and cancel any pending projection timers.
@@ -135,11 +153,11 @@ export declare class WorkflowManager {
135
153
  /**
136
154
  * Build a workflow snapshot from a task graph, persist it (Story 1.1 storage),
137
155
  * register it in memory, emit to subscribers, and return the snapshot. No process
138
- * is spawned here that happens in {@link startWorkflow}/{@link spawnPhaseAgent}.
156
+ * is spawned here - that happens in {@link startWorkflow}/{@link spawnPhaseAgent}.
139
157
  */
140
158
  createWorkflow(input: CreateWorkflowInput): Promise<WorkflowSnapshot>;
141
159
  /**
142
- * List every known workflow storage records overlaid with in-memory snapshots.
160
+ * List every known workflow - storage records overlaid with in-memory snapshots.
143
161
  * Live workflows are returned with progress PROJECTED from current child state
144
162
  * (Story 3.1), so list surfaces show the same live counts as the detail/SSE views.
145
163
  */
@@ -147,7 +165,7 @@ export declare class WorkflowManager {
147
165
  /**
148
166
  * Resolve a single workflow. A hard miss throws {@link WorkflowNotFoundError}.
149
167
  * For a live workflow the returned snapshot carries progress PROJECTED from the
150
- * current child-agent states the persisted counter is never trusted as truth.
168
+ * current child-agent states - the persisted counter is never trusted as truth.
151
169
  */
152
170
  getWorkflow(id: string): Promise<WorkflowSnapshot>;
153
171
  /**
@@ -161,14 +179,48 @@ export declare class WorkflowManager {
161
179
  /**
162
180
  * Return the workflow's recorded lifecycle events, derived deterministically
163
181
  * from its snapshot's timestamps (created/started/completed/archived). A
164
- * one-shot read continuous event push is Epic 2. Throws
182
+ * one-shot read - continuous event push is Epic 2. Throws
165
183
  * {@link WorkflowNotFoundError} for an unknown id.
166
184
  */
167
185
  getWorkflowEvents(workflowId: string): Promise<WorkflowLifecycleEvent[]>;
168
186
  /**
169
- * Drive a workflow to completion of its spawn phase: transition pending -> running,
170
- * then walk `executionLayers` sequentially, spawning each layer's task agents (in
171
- * task order). Returns the updated snapshot.
187
+ * Readiness predicate for the spawn scheduler - a pure function of `live`.
188
+ *
189
+ * Returns, in `graph.tasks` array order (so spawn order is deterministic), the NODE of
190
+ * every task that is not yet spawned, not yet terminal, and whose every declared
191
+ * dependency has already recorded a `"success"` outcome. Nodes, not ids: both callers
192
+ * need the node to spawn it, and returning ids made each of them re-scan `graph.tasks`
193
+ * (an O(n^2) lookup) behind an unreachable `if (!node) continue` guard.
194
+ *
195
+ * Readiness is computed from `TaskNode.dependencies` DIRECTLY, never from
196
+ * `resolveExecutionLayers`, whose declared-layer branch returns
197
+ * `metadata.executionLayers` verbatim and unvalidated, and never from layer output.
198
+ *
199
+ * A dependency id absent from `terminalOutcomes` is NOT satisfied. That single rule
200
+ * also makes an out-of-graph dependency unsatisfiable: a ghost id can never gain a
201
+ * terminal outcome, because outcomes are only ever written for real spawned children.
202
+ * This is a DELIBERATE divergence from `computeLayersFromDependencies`
203
+ * (workflow-progress.ts:268), whose `|| !byId.has(dep)` clause treats an out-of-graph
204
+ * dep as already met so it can keep layering already-stored graphs. Under a scheduler,
205
+ * "satisfied because it does not exist" would launch a task whose stated precondition
206
+ * was never met.
207
+ *
208
+ * Performs no I/O, no clock read and no `agentManager` call.
209
+ */
210
+ private readyTaskNodes;
211
+ /**
212
+ * Drive a workflow's FIRST spawn wave: transition pending -> running, then spawn
213
+ * exactly the tasks that are ready: those whose declared `dependencies` are already
214
+ * satisfied. On a fresh graph that is the dependency-free set; a task that declares a
215
+ * dependency is deliberately NOT launched here, which is what makes
216
+ * `TaskNode.dependencies` a real runtime constraint rather than a decorative field.
217
+ *
218
+ * Parallelism is residual, not declared: two tasks run together because nothing holds
219
+ * them, not because a layer listed them side by side.
220
+ *
221
+ * This method never launches a later wave. Tasks unblocked by a child reaching a
222
+ * terminal state are launched by the continuation path (Story 1.2), driven from
223
+ * `onAgentEvent`, not from here. Returns the updated snapshot.
172
224
  */
173
225
  startWorkflow(workflowId: string): Promise<WorkflowSnapshot>;
174
226
  /**
@@ -176,14 +228,36 @@ export declare class WorkflowManager {
176
228
  * `agentManager.createAgent` surface, tagging it with WORKFLOW_ID_LABEL (and
177
229
  * PARENT_AGENT_ID_LABEL when the workflow is owned by a parent agent). Records the
178
230
  * child id on the snapshot and persists. Never spawns a process directly.
231
+ *
232
+ * `config` arrives ALREADY RESOLVED by the caller (`resolveTaskConfig`, Story 2.1);
233
+ * this method performs no resolution of its own, so one helper serves both spawn
234
+ * call sites with no duplicated precedence rule.
235
+ *
236
+ * Returns the child on the normal path and `undefined` when the workflow was cancelled
237
+ * mid-spawn and the child was discarded (gate 1.002 NIT-4). The two cases are otherwise
238
+ * indistinguishable to a caller - same type, same shape - and the discarded agent is a
239
+ * corpse: archived, and deliberately absent from `childAgentIds`. `undefined` is the
240
+ * only honest way to say "there is no child here" through a public surface.
179
241
  */
180
242
  spawnPhaseAgent(args: {
181
243
  workflowId: string;
182
244
  node: TaskNode;
183
245
  config: AgentSessionConfig;
184
- }): Promise<ManagedAgent>;
246
+ }): Promise<ManagedAgent | undefined>;
185
247
  /**
186
- * Archive every child agent tagged to the workflow no orphans — mirroring
248
+ * Tear down a child that was created for a workflow which went terminal mid-spawn.
249
+ * Uses `archiveAgent`, the same public surface {@link cascadeArchiveChildren} uses, so
250
+ * the child is closed and marked archived rather than left running for a workflow
251
+ * nobody is watching. The child id is deliberately NOT appended to the snapshot: a
252
+ * cancelled workflow gains no children.
253
+ *
254
+ * Best effort and never throws. It runs on the failure path of a spawn that has already
255
+ * happened; a teardown error must not turn into a rejected `startWorkflow`, nor into an
256
+ * unhandled rejection from the detached continuation IIFE.
257
+ */
258
+ private discardOrphanedChild;
259
+ /**
260
+ * Archive every child agent tagged to the workflow - no orphans - mirroring
187
261
  * `AgentManager.cascadeArchiveChildren`, but keyed on WORKFLOW_ID_LABEL and using
188
262
  * only the public archive surface (`listAgents` + `archiveAgent`). `archiveAgent`
189
263
  * requires a live agent, so we iterate the live set exactly as the original guards
@@ -191,7 +265,7 @@ export declare class WorkflowManager {
191
265
  */
192
266
  cascadeArchiveChildren(workflowId: string): Promise<void>;
193
267
  /**
194
- * Number of live snapshot subscribers. Read-only observability used by the
268
+ * Number of live snapshot subscribers. Read-only observability - used by the
195
269
  * SSE route's leak-guard test (Story 2.1) to assert that a disconnected client
196
270
  * unsubscribes back to baseline. Additive; does not affect emit behaviour.
197
271
  */
@@ -200,23 +274,84 @@ export declare class WorkflowManager {
200
274
  subscribe(subscriber: WorkflowSubscriber): () => void;
201
275
  private emit;
202
276
  /**
203
- * AgentManager subscription handler (Story 3.1). Reacts only to child state
204
- * changes for an agent tagged to a tracked, non-terminal workflow; schedules a
205
- * coalesced projection emit for that workflow.
277
+ * AgentManager subscription handler. Reacts only to child state changes for an
278
+ * agent tagged to a tracked, RUNNING workflow (pending, cancelled and finished
279
+ * workflows are all rejected by the early return). It does three things:
280
+ *
281
+ * 1. Records the child's terminal outcome while the event still carries the closed
282
+ * agent (Story 3.1),
283
+ * 2. Drives ORCHESTRATION (Story 1.2): every surviving event sweeps the graph for
284
+ * tasks whose dependencies just became satisfied and spawns them. This is the
285
+ * only continuation path; there is no poll loop and no second `startWorkflow`, and
286
+ * 3. SETTLES the workflow (Story 1.3): once every declared task has a terminal
287
+ * outcome it flips to `completed`, and the first `failed` outcome flips it to
288
+ * `failed`, freezing the projected counts into the snapshot at that instant.
289
+ *
290
+ * Then it schedules a coalesced projection emit for the workflow. Continuation runs
291
+ * ALONGSIDE that emit, never behind it: the 60 ms window throttles display, and
292
+ * routing scheduling through it would add a window's dead time per graph edge.
293
+ *
294
+ * MUST stay synchronous and MUST NOT throw. `AgentManager.dispatch` invokes
295
+ * `subscriber.callback(event)` with no try/catch (agent-manager.ts:3992), so a throw
296
+ * here starves every later subscriber and propagates out of the caller's
297
+ * `await closeAgent(id)`. Making it `async` is the other half of the same bug:
298
+ * `dispatch` ignores the returned promise, so a rejection becomes an unhandled one.
206
299
  */
207
300
  private onAgentEvent;
301
+ /**
302
+ * Spawn every task whose dependencies have just become satisfied. Takes the ALREADY
303
+ * RESOLVED `LiveWorkflow` rather than a workflow id, so it can never call
304
+ * `requireLive` and can never throw `WorkflowNotFoundError` synchronously into
305
+ * `AgentManager.dispatch`. Returns `void`, never a promise, and never throws.
306
+ *
307
+ * The ENTRY guard deliberately lives in the caller (`onAgentEvent`'s early return), not
308
+ * here: a workflow that is not `running` never reaches this method, so cancellation
309
+ * wins for free with no second entry guard to keep in sync. The re-read inside the
310
+ * detached loop is a different question - not "may I start?" but "is this still true
311
+ * after the last await?" - and it has no equivalent in the caller, which returned long
312
+ * before that loop runs.
313
+ *
314
+ * Readiness comes from `readyTaskNodes`, the single Story 1.1 rule. This method adds no
315
+ * second readiness rule, does not flip the workflow status, does not stamp
316
+ * `completedAt` and does not record an outcome for a task whose spawn failed:
317
+ * inventing a failure outcome here would change the projection's counts.
318
+ */
319
+ private continueWorkflow;
320
+ /**
321
+ * Settle the workflow to a terminal status once its graph has finished. Takes the
322
+ * ALREADY RESOLVED `LiveWorkflow`, never a workflow id, so it can never call
323
+ * `requireLive` and can never throw `WorkflowNotFoundError` into
324
+ * `AgentManager.dispatch`.
325
+ *
326
+ * The rule is read THROUGH `graph.tasks`, not off `terminalOutcomes.values()`. With the
327
+ * ledger keyed on `live.childTaskIds` (gate 1.003 MINOR-4) an out-of-graph key can no
328
+ * longer enter the map at all, so this is a second layer rather than the only one; it is
329
+ * kept because it costs nothing and it is the layer that survives if the key ever becomes
330
+ * derivable from an event again. Mapping the declared tasks over the map also means a
331
+ * partially populated ledger reads as "not finished" rather than as "finished".
332
+ *
333
+ * Everything up to the first `await` is synchronous ON PURPOSE. `AgentManager.dispatch`
334
+ * is a plain synchronous loop invoked from inside `await closeAgent(...)`, so the status
335
+ * flip and the frozen counts are committed to memory before that expression resolves -
336
+ * which is what lets a caller read the terminal snapshot on the very next line with no
337
+ * polling. Only `persist` and `emit` are asynchronous.
338
+ *
339
+ * Children are never cancelled, closed, archived or interrupted here: an independent
340
+ * in-flight sibling keeps running after a failure.
341
+ */
342
+ private settleWorkflow;
208
343
  /**
209
344
  * Coalesce a burst of child updates into ONE projection emit per settled window
210
345
  * (the `agent-stream-coalescer.ts` discipline). The first event in a burst arms a
211
346
  * timer; subsequent events within the window are absorbed. On fire, the current
212
347
  * child states are projected and the updated snapshot is emitted through the
213
- * existing `workflow.*` / HTTP / SSE channels no new transport, no polling.
348
+ * existing `workflow.*` / HTTP / SSE channels - no new transport, no polling.
214
349
  */
215
350
  private scheduleProjectionEmit;
216
351
  /**
217
352
  * Overlay LIVE derived progress onto a workflow's base snapshot. Counts, per-phase
218
353
  * gate status, and per-phase completion are recomputed from the current child-agent
219
- * states never read back from the persisted snapshot. Terminal workflows are
354
+ * states - never read back from the persisted snapshot. Terminal workflows are
220
355
  * returned unchanged (nothing left to project). The base snapshot's lifecycle
221
356
  * fields (status, childAgentIds, timestamps, id/title) are preserved verbatim; only
222
357
  * derived progress fields are overlaid, so this stays back-compat and additive.