@smithers-orchestrator/driver 0.24.0 → 0.25.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smithers-orchestrator/driver",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
4
4
  "description": "Workflow execution driver — runs tasks, acts on EngineDecisions, reports results",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -63,11 +63,11 @@
63
63
  "dependencies": {
64
64
  "effect": "^3.21.1",
65
65
  "zod": "^4.3.6",
66
- "@smithers-orchestrator/db": "0.24.0",
67
- "@smithers-orchestrator/errors": "0.24.0",
68
- "@smithers-orchestrator/graph": "0.24.0",
69
- "@smithers-orchestrator/observability": "0.24.0",
70
- "@smithers-orchestrator/scheduler": "0.24.0"
66
+ "@smithers-orchestrator/db": "0.25.0",
67
+ "@smithers-orchestrator/graph": "0.25.0",
68
+ "@smithers-orchestrator/observability": "0.25.0",
69
+ "@smithers-orchestrator/errors": "0.25.0",
70
+ "@smithers-orchestrator/scheduler": "0.25.0"
71
71
  },
72
72
  "devDependencies": {
73
73
  "@types/bun": "latest",
@@ -0,0 +1,26 @@
1
+ import type { z } from "zod";
2
+
3
+ /**
4
+ * Resolve the row type a `ctx.output`/`ctx.outputMaybe`/`ctx.latest` call returns
5
+ * from the `table` argument it was given:
6
+ *
7
+ * - a string table name (a key of the workflow Schema) → the inferred row for
8
+ * that registered schema (`outputs.X` keyed by name);
9
+ * - a Zod schema object (e.g. `outputs.research`) → `z.infer` of that schema;
10
+ * - a Drizzle table (carries `$inferSelect`) → its select row;
11
+ * - anything else (a widened `string`, `unknown`) → an untyped output row.
12
+ *
13
+ * This is what makes `ctx.outputMaybe(outputs.research, ...)` carry the research
14
+ * fields instead of an untyped `Record<string, unknown>`.
15
+ */
16
+ export type ResolveOutputRow<Schema, T> = T extends keyof Schema
17
+ ? Schema[T] extends z.ZodTypeAny
18
+ ? z.infer<Schema[T]>
19
+ : Schema[T] extends { $inferSelect: infer R }
20
+ ? R
21
+ : Record<string, unknown>
22
+ : T extends z.ZodTypeAny
23
+ ? z.infer<T>
24
+ : T extends { $inferSelect: infer R }
25
+ ? R
26
+ : Record<string, unknown>;
package/src/RunOptions.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { RunAuthContext } from "./RunAuthContext.ts";
2
+ import type { OutputSnapshot } from "./OutputSnapshot.ts";
2
3
  import type { SmithersEvent } from "@smithers-orchestrator/observability/SmithersEvent";
3
4
 
4
5
  export type HotReloadOptions = {
@@ -19,6 +20,7 @@ export type RunOptions = {
19
20
  parentRunId?: string | null;
20
21
  input: Record<string, unknown>;
21
22
  maxConcurrency?: number;
23
+ requireRerenderOnOutputChange?: boolean;
22
24
  onProgress?: (e: SmithersEvent) => void;
23
25
  signal?: AbortSignal;
24
26
  resume?: boolean;
@@ -34,6 +36,9 @@ export type RunOptions = {
34
36
  auth?: RunAuthContext | null;
35
37
  config?: Record<string, unknown>;
36
38
  cliAgentToolsDefault?: "all" | "explicit-only";
39
+ initialOutputs?: OutputSnapshot;
40
+ initialIteration?: number;
41
+ initialIterations?: Record<string, number> | ReadonlyMap<string, number>;
37
42
  resumeClaim?: {
38
43
  claimOwnerId: string;
39
44
  claimHeartbeatAtMs: number;
package/src/RunStatus.ts CHANGED
@@ -3,6 +3,7 @@ export type RunStatus =
3
3
  | "waiting-approval"
4
4
  | "waiting-event"
5
5
  | "waiting-timer"
6
+ | "waiting-quota"
6
7
  | "finished"
7
8
  | "continued"
8
9
  | "failed"
@@ -12,6 +12,11 @@ import { resolveWorktreePath } from "@smithers-orchestrator/graph";
12
12
  /** @typedef {import("./SmithersRuntimeConfig.ts").SmithersRuntimeConfig} SmithersRuntimeConfig */
13
13
  /** @typedef {unknown} TableRef */
14
14
  /** @typedef {Record<string, unknown>} OutputRow User-visible output row — harness metadata fields (runId, nodeId, iteration) are stripped. */
15
+ /**
16
+ * @template Schema
17
+ * @template T
18
+ * @typedef {import("./ResolveOutputRow.ts").ResolveOutputRow<Schema, T>} ResolveOutputRow
19
+ */
15
20
  /**
16
21
  * @template Schema
17
22
  * @typedef {import("./OutputAccessor.ts").OutputAccessor<Schema>} OutputAccessor
@@ -119,36 +124,93 @@ export class SmithersCtx {
119
124
  });
120
125
  }
121
126
  /**
122
- * @param {TableRef} table
127
+ * @template {keyof Schema & string} K
128
+ * @overload
129
+ * @param {K} table
130
+ * @param {OutputKey} key
131
+ * @returns {ResolveOutputRow<Schema, K>}
132
+ */
133
+ /**
134
+ * @template {TableRef} T
135
+ * @overload
136
+ * @param {T} table
123
137
  * @param {OutputKey} key
124
- * @returns {OutputRow}
138
+ * @returns {ResolveOutputRow<Schema, T>}
139
+ */
140
+ /**
141
+ * @template {TableRef} T
142
+ * @param {T} table
143
+ * @param {OutputKey} key
144
+ * @returns {ResolveOutputRow<Schema, T>}
125
145
  */
126
146
  output(table, key) {
127
147
  const row = this.resolveRow(table, key);
128
148
  if (!row) {
129
149
  throw new SmithersError("MISSING_OUTPUT", `Missing output for nodeId=${key.nodeId} iteration=${key.iteration ?? 0}`, { nodeId: key.nodeId, iteration: key.iteration ?? 0 });
130
150
  }
131
- return /** @type {OutputRow} */ (stripAutoColumns(row));
151
+ return /** @type {ResolveOutputRow<Schema, T>} */ (/** @type {unknown} */ (stripAutoColumns(row)));
132
152
  }
133
153
  /**
134
- * @param {TableRef} table
154
+ * Resolve a single output row. Without an explicit `key.iteration` this
155
+ * resolves the CURRENT render iteration — which equals the loop iteration
156
+ * only for a single, non-nested loop, and is 0 when several loops coexist.
157
+ * For a `<Loop>` exit condition use {@link latest} (the most recent
158
+ * iteration), not `outputMaybe`, or an `until` built on it never advances.
159
+ *
160
+ * @template {keyof Schema & string} K
161
+ * @overload
162
+ * @param {K} table
135
163
  * @param {OutputKey} key
136
- * @returns {OutputRow | undefined}
164
+ * @returns {ResolveOutputRow<Schema, K> | undefined}
165
+ */
166
+ /**
167
+ * @template {TableRef} T
168
+ * @overload
169
+ * @param {T} table
170
+ * @param {OutputKey} key
171
+ * @returns {ResolveOutputRow<Schema, T> | undefined}
172
+ */
173
+ /**
174
+ * @template {TableRef} T
175
+ * @param {T} table
176
+ * @param {OutputKey} key
177
+ * @returns {ResolveOutputRow<Schema, T> | undefined}
137
178
  */
138
179
  outputMaybe(table, key) {
139
180
  const row = this.resolveRow(table, key);
140
- return row !== undefined ? /** @type {OutputRow} */ (stripAutoColumns(row)) : undefined;
181
+ return row !== undefined ? /** @type {ResolveOutputRow<Schema, T>} */ (/** @type {unknown} */ (stripAutoColumns(row))) : undefined;
141
182
  }
142
183
  /**
143
- * @param {TableRef} table
184
+ * Resolve the most recent iteration's output row for `nodeId` (highest
185
+ * iteration across all matching rows). This is the correct reader for a
186
+ * `<Loop>`/`<Ralph>` `until` exit condition; {@link outputMaybe} resolves
187
+ * the current render iteration and can read stale/iteration-0 data inside a
188
+ * loop.
189
+ *
190
+ * @template {keyof Schema & string} K
191
+ * @overload
192
+ * @param {K} table
144
193
  * @param {string} nodeId
145
- * @returns {OutputRow | undefined}
194
+ * @returns {ResolveOutputRow<Schema, K> | undefined}
195
+ */
196
+ /**
197
+ * @template {TableRef} T
198
+ * @overload
199
+ * @param {T} table
200
+ * @param {string} nodeId
201
+ * @returns {ResolveOutputRow<Schema, T> | undefined}
202
+ */
203
+ /**
204
+ * @template {TableRef} T
205
+ * @param {T} table
206
+ * @param {string} nodeId
207
+ * @returns {ResolveOutputRow<Schema, T> | undefined}
146
208
  */
147
209
  latest(table, nodeId) {
148
210
  const tableName = this.resolveTableName(table);
149
211
  const rows = this._outputs[tableName] ?? [];
150
212
  const matching = filterRowsByNodeId(rows, nodeId, this._currentScopes);
151
- /** @type {OutputRow | undefined} */
213
+ /** @type {ResolveOutputRow<Schema, T> | undefined} */
152
214
  let best = undefined;
153
215
  let bestIteration = -Infinity;
154
216
  for (const row of matching) {
@@ -156,7 +218,7 @@ export class SmithersCtx {
156
218
  ? Number(row.iteration)
157
219
  : 0;
158
220
  if (!best || iter >= bestIteration) {
159
- best = row;
221
+ best = /** @type {ResolveOutputRow<Schema, T>} */ (/** @type {unknown} */ (row));
160
222
  bestIteration = iter;
161
223
  }
162
224
  }
@@ -20,7 +20,6 @@ import { withAbort } from "./withAbort.js";
20
20
  /** @typedef {import("@smithers-orchestrator/graph/types").TaskDescriptor} TaskDescriptor */
21
21
 
22
22
  const SCHEDULER_SPECIFIER = "@smithers-orchestrator/scheduler";
23
- const LOCAL_SCHEDULER_SPECIFIER = "../../scheduler/src/index.js";
24
23
  function createRunId() {
25
24
  return `run_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
26
25
  }
@@ -155,19 +154,17 @@ function mergeOutputSnapshots(base, live) {
155
154
  * @returns {Promise<CreateWorkflowSession | null>}
156
155
  */
157
156
  async function loadCreateSession() {
158
- for (const specifier of [SCHEDULER_SPECIFIER, LOCAL_SCHEDULER_SPECIFIER]) {
159
- let mod;
160
- try {
161
- mod = (await import(specifier));
162
- }
163
- catch {
164
- continue;
165
- }
166
- if (typeof mod.createSession === "function")
167
- return mod.createSession;
168
- if (typeof mod.makeWorkflowSession === "function") {
169
- return mod.makeWorkflowSession;
170
- }
157
+ let mod;
158
+ try {
159
+ // The scheduler is a workspace dependency, so the package specifier always
160
+ // resolves (no relative-path fallback needed).
161
+ mod = (await import(SCHEDULER_SPECIFIER));
162
+ }
163
+ catch {
164
+ return null;
165
+ }
166
+ if (typeof mod.makeWorkflowSession === "function") {
167
+ return mod.makeWorkflowSession;
171
168
  }
172
169
  return null;
173
170
  }
@@ -276,6 +273,8 @@ export class WorkflowDriver {
276
273
  baseOutputs = {};
277
274
  /** @type {Map<string, Promise<{ key: string; task: TaskDescriptor; kind: "completed" | "failed" | "cancelled"; output?: unknown; error?: unknown }>>} */
278
275
  inflightTasks = new Map();
276
+ /** @type {Map<string, TaskDescriptor>} */
277
+ inflightTaskDescriptors = new Map();
279
278
  /** @type {Array<{ key: string; task: TaskDescriptor; kind: "completed" | "failed" | "cancelled"; output?: unknown; error?: unknown }>} */
280
279
  settledTasks = [];
281
280
  /**
@@ -428,6 +427,7 @@ export class WorkflowDriver {
428
427
  defaultIteration: iteration,
429
428
  baseRootDir,
430
429
  workflowPath,
430
+ trigger: context.trigger,
431
431
  });
432
432
  // Capture tasks that deferred on unresolved deps this render so the run
433
433
  // loop can fail loudly if any survive to a Finished decision instead of
@@ -490,10 +490,12 @@ export class WorkflowDriver {
490
490
  }
491
491
  })().then((settled) => {
492
492
  this.inflightTasks.delete(key);
493
+ this.inflightTaskDescriptors.delete(key);
493
494
  this.settledTasks.push(settled);
494
495
  return settled;
495
496
  });
496
497
  this.inflightTasks.set(key, promise);
498
+ this.inflightTaskDescriptors.set(key, task);
497
499
  }
498
500
  /**
499
501
  * Wait for the next settled task (or an optional deadline) and report it to
@@ -506,9 +508,12 @@ export class WorkflowDriver {
506
508
  if (!this.session) {
507
509
  throw new Error("WorkflowSession is not initialized.");
508
510
  }
509
- const waitStart = performance.now();
511
+ let waitedTasks = [];
512
+ let waitStart = 0;
510
513
  try {
511
514
  if (this.settledTasks.length === 0 && this.inflightTasks.size > 0) {
515
+ waitedTasks = [...this.inflightTaskDescriptors.values()];
516
+ waitStart = performance.now();
512
517
  const racers = [...this.inflightTasks.values()];
513
518
  if (deadlineMs != null) {
514
519
  racers.push(sleepWithAbort(deadlineMs, this.activeOptions?.signal).then(() => null));
@@ -517,10 +522,12 @@ export class WorkflowDriver {
517
522
  }
518
523
  }
519
524
  finally {
520
- await this.onSchedulerWait?.(performance.now() - waitStart, {
521
- runId: this.activeRunId,
522
- tasks: [],
523
- });
525
+ if (waitedTasks.length > 0) {
526
+ await this.onSchedulerWait?.(performance.now() - waitStart, {
527
+ runId: this.activeRunId,
528
+ tasks: waitedTasks,
529
+ });
530
+ }
524
531
  }
525
532
  if (this.activeOptions?.signal?.aborted) {
526
533
  return this.cancelRun();
@@ -603,6 +610,8 @@ export class WorkflowDriver {
603
610
  return { runId: this.activeRunId, status: "waiting-event" };
604
611
  case "Timer":
605
612
  return { runId: this.activeRunId, status: "waiting-timer" };
613
+ case "Quota":
614
+ return { runId: this.activeRunId, status: "waiting-quota" };
606
615
  case "RetryBackoff": {
607
616
  await sleepWithAbort(reason.waitMs, this.activeOptions?.signal);
608
617
  if (this.activeOptions?.signal?.aborted) {
package/src/index.d.ts CHANGED
@@ -1,12 +1,12 @@
1
- import * as _smithers_graph_types from '@smithers-orchestrator/graph/types';
1
+ import * as _smithers_orchestrator_graph_types from '@smithers-orchestrator/graph/types';
2
2
  import { WorkflowGraph as WorkflowGraph$1, TaskDescriptor as TaskDescriptor$1 } from '@smithers-orchestrator/graph/types';
3
3
  import { SmithersEvent } from '@smithers-orchestrator/observability/SmithersEvent';
4
- import * as _smithers_scheduler from '@smithers-orchestrator/scheduler';
4
+ import * as _smithers_orchestrator_scheduler from '@smithers-orchestrator/scheduler';
5
5
  import { WaitReason as WaitReason$1, EngineDecision as EngineDecision$1 } from '@smithers-orchestrator/scheduler';
6
6
  import { z } from 'zod';
7
7
  import { SmithersWorkflowOptions } from '@smithers-orchestrator/scheduler/SmithersWorkflowOptions';
8
8
  import { SchemaRegistryEntry } from '@smithers-orchestrator/db/SchemaRegistryEntry';
9
- import * as _smithers_graph from '@smithers-orchestrator/graph';
9
+ import * as _smithers_orchestrator_graph from '@smithers-orchestrator/graph';
10
10
  import { ExtractOptions, WorkflowGraph } from '@smithers-orchestrator/graph';
11
11
 
12
12
  type TaskCompletedEvent = {
@@ -40,6 +40,10 @@ type RunAuthContext$2 = {
40
40
  createdAt: string;
41
41
  };
42
42
 
43
+ type OutputSnapshot$2<TFallback = unknown> = {
44
+ [tableName: string]: Array<TFallback>;
45
+ };
46
+
43
47
  type HotReloadOptions$1 = {
44
48
  /** Root directory to watch for changes (default: auto-detect from workflow entry) */
45
49
  rootDir?: string;
@@ -57,6 +61,7 @@ type RunOptions$2 = {
57
61
  parentRunId?: string | null;
58
62
  input: Record<string, unknown>;
59
63
  maxConcurrency?: number;
64
+ requireRerenderOnOutputChange?: boolean;
60
65
  onProgress?: (e: SmithersEvent) => void;
61
66
  signal?: AbortSignal;
62
67
  resume?: boolean;
@@ -72,6 +77,9 @@ type RunOptions$2 = {
72
77
  auth?: RunAuthContext$2 | null;
73
78
  config?: Record<string, unknown>;
74
79
  cliAgentToolsDefault?: "all" | "explicit-only";
80
+ initialOutputs?: OutputSnapshot$2;
81
+ initialIteration?: number;
82
+ initialIterations?: Record<string, number> | ReadonlyMap<string, number>;
75
83
  resumeClaim?: {
76
84
  claimOwnerId: string;
77
85
  claimHeartbeatAtMs: number;
@@ -80,7 +88,7 @@ type RunOptions$2 = {
80
88
  };
81
89
  };
82
90
 
83
- type RunStatus$1 = "running" | "waiting-approval" | "waiting-event" | "waiting-timer" | "finished" | "continued" | "failed" | "cancelled";
91
+ type RunStatus$1 = "running" | "waiting-approval" | "waiting-event" | "waiting-timer" | "waiting-quota" | "finished" | "continued" | "failed" | "cancelled";
84
92
 
85
93
  type RunResult$2 = {
86
94
  readonly runId: string;
@@ -136,10 +144,25 @@ type OutputAccessor$2<Schema, TRow = unknown> = {
136
144
  } & {
137
145
  [K in keyof Schema & string]: Array<InferOutputEntry$1<Schema[K]>>;
138
146
  };
139
- type OutputSchemaKey<Schema> = Exclude<keyof Schema & string, "input">;
140
- type OutputSchemaValue<Schema> = Schema[OutputSchemaKey<Schema>];
141
- type StrictTableRef<Schema> = [OutputSchemaKey<Schema>] extends [never] ? TableRef : OutputSchemaKey<Schema> | OutputSchemaValue<Schema>;
142
- type OutputForTable<Schema, Table> = Table extends OutputSchemaKey<Schema> ? InferOutputEntry$1<Schema[Table]> : Table extends OutputSchemaValue<Schema> ? InferOutputEntry$1<Table> : OutputRow;
147
+
148
+ /**
149
+ * Resolve the row type a `ctx.output`/`ctx.outputMaybe`/`ctx.latest` call returns
150
+ * from the `table` argument it was given:
151
+ *
152
+ * - a string table name (a key of the workflow Schema) → the inferred row for
153
+ * that registered schema (`outputs.X` keyed by name);
154
+ * - a Zod schema object (e.g. `outputs.research`) → `z.infer` of that schema;
155
+ * - a Drizzle table (carries `$inferSelect`) → its select row;
156
+ * - anything else (a widened `string`, `unknown`) → an untyped output row.
157
+ *
158
+ * This is what makes `ctx.outputMaybe(outputs.research, ...)` carry the research
159
+ * fields instead of an untyped `Record<string, unknown>`.
160
+ */
161
+ type ResolveOutputRow$1<Schema, T> = T extends keyof Schema ? Schema[T] extends z.ZodTypeAny ? z.infer<Schema[T]> : Schema[T] extends {
162
+ $inferSelect: infer R;
163
+ } ? R : Record<string, unknown> : T extends z.ZodTypeAny ? z.infer<T> : T extends {
164
+ $inferSelect: infer R;
165
+ } ? R : Record<string, unknown>;
143
166
 
144
167
  type SmithersRuntimeConfig$1 = {
145
168
  cliAgentToolsDefault?: "all" | "explicit-only";
@@ -148,10 +171,6 @@ type SmithersRuntimeConfig$1 = {
148
171
  worktreePaths?: Record<string, string>;
149
172
  };
150
173
 
151
- type OutputSnapshot$2<TFallback = unknown> = {
152
- [tableName: string]: Array<TFallback>;
153
- };
154
-
155
174
  type SmithersCtxOptions$2 = {
156
175
  runId: string;
157
176
  iteration: number;
@@ -210,6 +229,19 @@ declare class SmithersCtx<Schema extends unknown = unknown> {
210
229
  _zodToKeyName: Map<unknown, string> | undefined;
211
230
  /** @type {Set<string>} */
212
231
  _currentScopes: Set<string>;
232
+ /**
233
+ * Tasks that declared `deps` but could not resolve them this render, so
234
+ * they deferred (returned null) instead of mounting. The engine reads this
235
+ * after each render: a deferral is normal while an upstream is still
236
+ * producing, but one that survives to quiescence means the dependency can
237
+ * never resolve (e.g. a deps/needs key that maps to a node id no task
238
+ * produces) and the run would otherwise finish silently without it.
239
+ * @type {{ nodeId: string; waitingOn: string[] }[]}
240
+ */
241
+ _deferredDeps: {
242
+ nodeId: string;
243
+ waitingOn: string[];
244
+ }[];
213
245
  /**
214
246
  * Return the resolved absolute path for a rendered worktree or task id.
215
247
  * The lookup is populated from task descriptors, so task node ids and
@@ -228,23 +260,65 @@ declare class SmithersCtx<Schema extends unknown = unknown> {
228
260
  */
229
261
  resolveWorktreePath(path: string): string;
230
262
  /**
231
- * @param {TableRef} table
263
+ * @template {keyof Schema & string} K
264
+ * @overload
265
+ * @param {K} table
232
266
  * @param {OutputKey} key
233
- * @returns {OutputRow}
267
+ * @returns {ResolveOutputRow<Schema, K>}
234
268
  */
235
- output<Table extends StrictTableRef<Schema>>(table: Table, key: OutputKey$1): OutputForTable<Schema, Table>;
269
+ output<K extends keyof Schema & string>(table: K, key: OutputKey$1): ResolveOutputRow<Schema, K>;
236
270
  /**
237
- * @param {TableRef} table
271
+ * @template {TableRef} T
272
+ * @overload
273
+ * @param {T} table
238
274
  * @param {OutputKey} key
239
- * @returns {OutputRow | undefined}
275
+ * @returns {ResolveOutputRow<Schema, T>}
240
276
  */
241
- outputMaybe<Table extends StrictTableRef<Schema>>(table: Table, key: OutputKey$1): OutputForTable<Schema, Table> | undefined;
277
+ output<T extends TableRef>(table: T, key: OutputKey$1): ResolveOutputRow<Schema, T>;
242
278
  /**
243
- * @param {TableRef} table
279
+ * Resolve a single output row. Without an explicit `key.iteration` this
280
+ * resolves the CURRENT render iteration — which equals the loop iteration
281
+ * only for a single, non-nested loop, and is 0 when several loops coexist.
282
+ * For a `<Loop>` exit condition use {@link latest} (the most recent
283
+ * iteration), not `outputMaybe`, or an `until` built on it never advances.
284
+ *
285
+ * @template {keyof Schema & string} K
286
+ * @overload
287
+ * @param {K} table
288
+ * @param {OutputKey} key
289
+ * @returns {ResolveOutputRow<Schema, K> | undefined}
290
+ */
291
+ outputMaybe<K extends keyof Schema & string>(table: K, key: OutputKey$1): ResolveOutputRow<Schema, K> | undefined;
292
+ /**
293
+ * @template {TableRef} T
294
+ * @overload
295
+ * @param {T} table
296
+ * @param {OutputKey} key
297
+ * @returns {ResolveOutputRow<Schema, T> | undefined}
298
+ */
299
+ outputMaybe<T extends TableRef>(table: T, key: OutputKey$1): ResolveOutputRow<Schema, T> | undefined;
300
+ /**
301
+ * Resolve the most recent iteration's output row for `nodeId` (highest
302
+ * iteration across all matching rows). This is the correct reader for a
303
+ * `<Loop>`/`<Ralph>` `until` exit condition; {@link outputMaybe} resolves
304
+ * the current render iteration and can read stale/iteration-0 data inside a
305
+ * loop.
306
+ *
307
+ * @template {keyof Schema & string} K
308
+ * @overload
309
+ * @param {K} table
244
310
  * @param {string} nodeId
245
- * @returns {OutputRow | undefined}
311
+ * @returns {ResolveOutputRow<Schema, K> | undefined}
312
+ */
313
+ latest<K extends keyof Schema & string>(table: K, nodeId: string): ResolveOutputRow<Schema, K> | undefined;
314
+ /**
315
+ * @template {TableRef} T
316
+ * @overload
317
+ * @param {T} table
318
+ * @param {string} nodeId
319
+ * @returns {ResolveOutputRow<Schema, T> | undefined}
246
320
  */
247
- latest<Table extends StrictTableRef<Schema>>(table: Table, nodeId: string): OutputForTable<Schema, Table> | undefined;
321
+ latest<T extends TableRef>(table: T, nodeId: string): ResolveOutputRow<Schema, T> | undefined;
248
322
  /**
249
323
  * @param {unknown} value
250
324
  * @param {SafeParser} schema
@@ -262,6 +336,17 @@ declare class SmithersCtx<Schema extends unknown = unknown> {
262
336
  * @returns {string}
263
337
  */
264
338
  resolveTableName(table: TableRef): string;
339
+ /**
340
+ * Record that a task with `deps` deferred this render because its
341
+ * dependencies were not resolvable. Called by the Task component before it
342
+ * returns null. The engine inspects these at quiescence to turn a permanent
343
+ * deferral (a never-satisfiable dependency) into a loud error instead of a
344
+ * silent skip.
345
+ * @param {string} nodeId
346
+ * @param {string[]} waitingOn
347
+ * @returns {void}
348
+ */
349
+ recordDeferredDep(nodeId: string, waitingOn: string[]): void;
265
350
  /**
266
351
  * @param {TableRef} table
267
352
  * @param {OutputKey} key
@@ -275,8 +360,11 @@ type SmithersCtxOptions$1 = SmithersCtxOptions$2;
275
360
  type RunAuthContext$1 = RunAuthContext$2;
276
361
  type SmithersRuntimeConfig = SmithersRuntimeConfig$1;
277
362
  type TableRef = unknown;
278
- /** User-visible output row — harness metadata fields (runId, nodeId, iteration) are stripped. */
363
+ /**
364
+ * User-visible output row — harness metadata fields (runId, nodeId, iteration) are stripped.
365
+ */
279
366
  type OutputRow = Record<string, unknown>;
367
+ type ResolveOutputRow<Schema, T> = ResolveOutputRow$1<Schema, T>;
280
368
  type OutputAccessor$1<Schema> = OutputAccessor$2<Schema>;
281
369
 
282
370
  type WorkflowElement = {
@@ -355,11 +443,36 @@ declare class WorkflowDriver<Schema extends unknown = unknown> {
355
443
  /** @type {RunOptions | undefined} */
356
444
  activeOptions: RunOptions$1 | undefined;
357
445
  /** @type {import("@smithers-orchestrator/graph").WorkflowGraph | undefined} */
358
- lastGraph: _smithers_graph.WorkflowGraph | undefined;
446
+ lastGraph: _smithers_orchestrator_graph.WorkflowGraph | undefined;
447
+ /** @type {{ nodeId: string; waitingOn: string[] }[]} Tasks that deferred on unresolved deps in the latest render. */
448
+ lastDeferredDeps: {
449
+ nodeId: string;
450
+ waitingOn: string[];
451
+ }[];
452
+ /** @type {Record<string, string>} */
453
+ worktreePathsById: Record<string, string>;
359
454
  /** @type {Map<string, string>} */
360
455
  outputTablesByNodeId: Map<string, string>;
361
456
  /** @type {OutputSnapshot} */
362
457
  baseOutputs: OutputSnapshot$1;
458
+ /** @type {Map<string, Promise<{ key: string; task: TaskDescriptor; kind: "completed" | "failed" | "cancelled"; output?: unknown; error?: unknown }>>} */
459
+ inflightTasks: Map<string, Promise<{
460
+ key: string;
461
+ task: TaskDescriptor;
462
+ kind: "completed" | "failed" | "cancelled";
463
+ output?: unknown;
464
+ error?: unknown;
465
+ }>>;
466
+ /** @type {Map<string, TaskDescriptor>} */
467
+ inflightTaskDescriptors: Map<string, TaskDescriptor>;
468
+ /** @type {Array<{ key: string; task: TaskDescriptor; kind: "completed" | "failed" | "cancelled"; output?: unknown; error?: unknown }>} */
469
+ settledTasks: Array<{
470
+ key: string;
471
+ task: TaskDescriptor;
472
+ kind: "completed" | "failed" | "cancelled";
473
+ output?: unknown;
474
+ error?: unknown;
475
+ }>;
363
476
  /**
364
477
  * @param {RunOptions} options
365
478
  * @returns {Promise<RunResult>}
@@ -382,6 +495,38 @@ declare class WorkflowDriver<Schema extends unknown = unknown> {
382
495
  */
383
496
  executeTasks(tasks: readonly TaskDescriptor[]): Promise<EngineDecision | RunResult$1>;
384
497
  /**
498
+ * Start a task without blocking the driver loop on its completion. Settled
499
+ * tasks queue in `settledTasks` and are reported to the session one at a
500
+ * time from `nextCompletionDecision`, so each decision is computed against
501
+ * fresh session state and a slow task never blocks scheduling work that
502
+ * became ready elsewhere in the graph (#267).
503
+ * @param {TaskDescriptor} task
504
+ * @param {{ runId: string; options: RunOptions; signal?: AbortSignal }} context
505
+ */
506
+ startInflightTask(task: TaskDescriptor, context: {
507
+ runId: string;
508
+ options: RunOptions$1;
509
+ signal?: AbortSignal;
510
+ }): void;
511
+ /**
512
+ * Wait for the next settled task (or an optional deadline) and report it to
513
+ * the session for a fresh decision. Completions that landed while a previous
514
+ * one was being processed drain from `settledTasks` first.
515
+ * @param {number | null} [deadlineMs]
516
+ * @returns {Promise<EngineDecision | RunResult>}
517
+ */
518
+ nextCompletionDecision(deadlineMs?: number | null): Promise<EngineDecision | RunResult$1>;
519
+ /**
520
+ * Await every in-flight task without reporting further decisions. Used
521
+ * before run-level exits (failure, continue-as-new) so task executors are
522
+ * not abandoned mid-write. This matches the pre-#267 barrier semantics:
523
+ * failure reporting waits for in-flight siblings (bounded by their
524
+ * timeouts), trading latency for the invariant that no executor writes
525
+ * after the run is terminal. Fail-fast would need a per-run abort threaded
526
+ * through executors.
527
+ */
528
+ drainInflight(): Promise<void>;
529
+ /**
385
530
  * @param {WaitReason} reason
386
531
  * @returns {Promise<EngineDecision | RunResult>}
387
532
  */
@@ -412,11 +557,11 @@ type SchedulerWaitHandler = SchedulerWaitHandler$1;
412
557
  type WaitHandler = WaitHandler$1;
413
558
  type ContinueAsNewHandler = ContinueAsNewHandler$1;
414
559
  type RunOptions$1 = RunOptions$2;
415
- type RunResult$1 = _smithers_scheduler.RunResult;
416
- type EngineDecision = _smithers_scheduler.EngineDecision;
417
- type RenderContext = _smithers_scheduler.RenderContext;
418
- type WaitReason = _smithers_scheduler.WaitReason;
419
- type TaskDescriptor = _smithers_graph_types.TaskDescriptor;
560
+ type RunResult$1 = _smithers_orchestrator_scheduler.RunResult;
561
+ type EngineDecision = _smithers_orchestrator_scheduler.EngineDecision;
562
+ type RenderContext = _smithers_orchestrator_scheduler.RenderContext;
563
+ type WaitReason = _smithers_orchestrator_scheduler.WaitReason;
564
+ type TaskDescriptor = _smithers_orchestrator_graph_types.TaskDescriptor;
420
565
 
421
566
  type HotReloadOptions = HotReloadOptions$1;
422
567
  type OutputAccessor<Schema = any> = OutputAccessor$2<Schema>;