@osolmaz/pi-workflows 0.13.0 → 0.13.2

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 (84) hide show
  1. package/README.md +11 -9
  2. package/dist/builtins/autodoc.workflow.d.ts +191 -4
  3. package/dist/builtins/autodoc.workflow.js +156 -30
  4. package/dist/builtins/autodoc.workflow.js.map +1 -1
  5. package/dist/builtins/autoimplement-command-batches.d.ts +1 -0
  6. package/dist/builtins/autoimplement-command-batches.js +29 -31
  7. package/dist/builtins/autoimplement-command-batches.js.map +1 -1
  8. package/dist/builtins/autoimplement.workflow.d.ts +1259 -29
  9. package/dist/builtins/autoimplement.workflow.js +416 -153
  10. package/dist/builtins/autoimplement.workflow.js.map +1 -1
  11. package/dist/builtins/autoplan.workflow.d.ts +6 -0
  12. package/dist/builtins/autoplan.workflow.js +68 -32
  13. package/dist/builtins/autoplan.workflow.js.map +1 -1
  14. package/dist/builtins/catalog.js +5 -5
  15. package/dist/builtins/catalog.js.map +1 -1
  16. package/dist/builtins/change-verification.workflow.d.ts +110 -0
  17. package/dist/builtins/change-verification.workflow.js +860 -0
  18. package/dist/builtins/change-verification.workflow.js.map +1 -0
  19. package/dist/builtins/monitor.workflow.js +4 -3
  20. package/dist/builtins/monitor.workflow.js.map +1 -1
  21. package/dist/builtins/plain-summary.workflow.js +35 -24
  22. package/dist/builtins/plain-summary.workflow.js.map +1 -1
  23. package/dist/builtins/plan-change.workflow.d.ts +361 -6
  24. package/dist/builtins/plan-change.workflow.js +26 -0
  25. package/dist/builtins/plan-change.workflow.js.map +1 -1
  26. package/dist/builtins/sanity-check.workflow.js +19 -22
  27. package/dist/builtins/sanity-check.workflow.js.map +1 -1
  28. package/dist/builtins/workspace-preparation.workflow.d.ts +75 -0
  29. package/dist/builtins/workspace-preparation.workflow.js +498 -0
  30. package/dist/builtins/workspace-preparation.workflow.js.map +1 -0
  31. package/dist/controllers/sqlite.js +16 -51
  32. package/dist/controllers/sqlite.js.map +1 -1
  33. package/dist/extension/decision-channels.js +45 -7
  34. package/dist/extension/decision-channels.js.map +1 -1
  35. package/dist/extension/executor.js +1 -4
  36. package/dist/extension/executor.js.map +1 -1
  37. package/dist/extension/index.js +2 -15
  38. package/dist/extension/index.js.map +1 -1
  39. package/dist/extension/recorder.d.ts +1 -1
  40. package/dist/extension/recorder.js +8 -8
  41. package/dist/extension/recorder.js.map +1 -1
  42. package/dist/state/schema.js +22 -3
  43. package/dist/state/schema.js.map +1 -1
  44. package/dist/viewer/session-reducer.d.ts +0 -1
  45. package/dist/viewer/session-reducer.js +2 -24
  46. package/dist/viewer/session-reducer.js.map +1 -1
  47. package/dist/workflows/engine.js +4 -3
  48. package/dist/workflows/engine.js.map +1 -1
  49. package/dist/workflows/store.d.ts +6 -1
  50. package/dist/workflows/store.js +286 -33
  51. package/dist/workflows/store.js.map +1 -1
  52. package/dist/workflows/types.d.ts +1 -1
  53. package/docs/SQLITE_STATE.md +20 -14
  54. package/docs/WORKFLOW_COMPOSITION.md +8 -0
  55. package/docs/plans/2026-08-23-assistant-agent-completion-plan.md +1 -1
  56. package/docs/plans/2026-08-24-change-scoped-verification-plan.md +419 -0
  57. package/docs/plans/2026-08-25-autoplan-user-intent-capture-plan.md +107 -0
  58. package/docs/session-event-journal.md +2 -3
  59. package/docs/workflows.md +43 -21
  60. package/herdr-plugin.toml +1 -1
  61. package/package.json +1 -1
  62. package/skills/autodoc/SKILL.md +7 -0
  63. package/skills/autoimplement/SKILL.md +4 -0
  64. package/src/builtins/autodoc.workflow.ts +184 -33
  65. package/src/builtins/autoimplement-command-batches.ts +39 -33
  66. package/src/builtins/autoimplement.workflow.ts +483 -175
  67. package/src/builtins/autoplan.workflow.ts +80 -32
  68. package/src/builtins/catalog.ts +5 -5
  69. package/src/builtins/change-verification.workflow.ts +1143 -0
  70. package/src/builtins/monitor.workflow.ts +6 -3
  71. package/src/builtins/plain-summary.workflow.ts +38 -40
  72. package/src/builtins/plan-change.workflow.ts +35 -0
  73. package/src/builtins/sanity-check.workflow.ts +19 -22
  74. package/src/builtins/workspace-preparation.workflow.ts +668 -0
  75. package/src/controllers/sqlite.ts +19 -53
  76. package/src/extension/decision-channels.ts +47 -7
  77. package/src/extension/executor.ts +1 -4
  78. package/src/extension/index.ts +2 -15
  79. package/src/extension/recorder.ts +10 -8
  80. package/src/state/schema.ts +22 -3
  81. package/src/viewer/session-reducer.ts +2 -29
  82. package/src/workflows/engine.ts +4 -3
  83. package/src/workflows/store.ts +397 -43
  84. package/src/workflows/types.ts +0 -1
@@ -156,9 +156,9 @@ type RunRow = {
156
156
  workflowRef: string;
157
157
  runStatus: string;
158
158
  workflowSourceHash: Buffer;
159
+ workflowSourcesHash: Buffer | null;
159
160
  definitionDigest: Buffer;
160
161
  inputHash: Buffer;
161
- outputHash: Buffer | null;
162
162
  launchOptionsHash: Buffer;
163
163
  status: WorkflowRunLaunchStatus;
164
164
  availableAt: number;
@@ -1049,34 +1049,11 @@ export class SqliteControllerStore implements ControllerStore {
1049
1049
  const resourceId = resourceIdFor("run", options.runId);
1050
1050
  const definitionHash = this.state.putJson(options.definitionSnapshot, now);
1051
1051
  const queuedSource = queuedWorkflowSource(options.workflowSource);
1052
- const sourceHash = this.state.putJson(options.workflowSource, now);
1052
+ const sourceHash = this.state.putJson(queuedSource.root, now);
1053
+ const mountedSourcesHash =
1054
+ queuedSource.mounted.length === 0 ? null : this.state.putJson(queuedSource.mounted, now);
1053
1055
  const inputHash = this.state.putJson(options.input ?? null, now);
1054
1056
  const launchHash = this.state.putJson(options.launchOptions ?? {}, now);
1055
- const queuedStateHash = this.state.putJson(
1056
- {
1057
- schema: "pi-workflows.run-state.v1",
1058
- traceSeq: 1,
1059
- runId: options.runId,
1060
- workflowName: options.workflowName,
1061
- workflowSource: queuedSource.root,
1062
- ...(queuedSource.mounted.length === 0
1063
- ? {}
1064
- : {
1065
- workflowSources: queuedSource.mounted,
1066
- definitionDigest: options.definitionDigest,
1067
- }),
1068
- ...(options.parentRunId === undefined ? {} : { parentRunId: options.parentRunId }),
1069
- startedAt: new Date(now).toISOString(),
1070
- updatedAt: new Date(now).toISOString(),
1071
- status: "running",
1072
- input: options.input ?? null,
1073
- outputs: {},
1074
- results: {},
1075
- steps: [],
1076
- updates: [],
1077
- },
1078
- now,
1079
- );
1080
1057
  this.state.connection
1081
1058
  .prepare(
1082
1059
  `INSERT INTO workflow_definitions(
@@ -1101,7 +1078,7 @@ export class SqliteControllerStore implements ControllerStore {
1101
1078
  run_id, resource_id, project_id, parent_run_id, definition_digest,
1102
1079
  workflow_ref, workflow_source_hash, launch_options_hash,
1103
1080
  source_type, source_ref, source_revision, status, paused,
1104
- input_hash, output_hash, created_at, updated_at
1081
+ input_hash, workflow_sources_hash, created_at, updated_at
1105
1082
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'queued', 0, ?, ?, ?, ?)`,
1106
1083
  )
1107
1084
  .run(
@@ -1117,7 +1094,7 @@ export class SqliteControllerStore implements ControllerStore {
1117
1094
  source.ref,
1118
1095
  source.revision,
1119
1096
  inputHash,
1120
- queuedStateHash,
1097
+ mountedSourcesHash,
1121
1098
  now,
1122
1099
  now,
1123
1100
  );
@@ -2261,7 +2238,11 @@ export class SqliteControllerStore implements ControllerStore {
2261
2238
  runId: row.runId,
2262
2239
  workflowName: row.workflowName,
2263
2240
  workflowSourceRef: row.workflowRef,
2264
- workflowSource: this.state.readJson(row.workflowSourceHash),
2241
+ workflowSource: {
2242
+ root: this.state.readJson(row.workflowSourceHash),
2243
+ mounted:
2244
+ row.workflowSourcesHash === null ? [] : this.state.readJson(row.workflowSourcesHash),
2245
+ },
2265
2246
  initialized: row.runStatus !== "queued",
2266
2247
  definitionDigest: `sha256:${row.definitionDigest.toString("hex")}`,
2267
2248
  input: this.state.readJson(row.inputHash),
@@ -2461,27 +2442,8 @@ export class SqliteControllerStore implements ControllerStore {
2461
2442
  ) {
2462
2443
  return false;
2463
2444
  }
2464
- if (row.outputHash === null) throw new Error(`Workflow run ${runId} has no state projection`);
2465
- const projected = this.state.readJson(row.outputHash);
2466
- if (!isRecord(projected)) throw new Error(`Workflow run ${runId} state is invalid`);
2467
2445
  const revision = this.resourceRevision(row.resourceId);
2468
- const at = new Date(now).toISOString();
2469
- const terminalState: Record<string, unknown> = {
2470
- ...projected,
2471
- traceSeq: revision + 1,
2472
- status,
2473
- updatedAt: at,
2474
- finishedAt: at,
2475
- error,
2476
- };
2477
- delete terminalState.currentNode;
2478
- delete terminalState.currentAttemptId;
2479
- delete terminalState.currentNodeStartedAt;
2480
- delete terminalState.statusDetail;
2481
- delete terminalState.paused;
2482
- delete terminalState.waitingOn;
2483
2446
  const errorHash = this.state.putText(error, now);
2484
- const outputHash = this.state.putJson(terminalState, now);
2485
2447
  const queueUpdate = this.state.connection
2486
2448
  .prepare(
2487
2449
  `UPDATE run_queue
@@ -2506,10 +2468,13 @@ export class SqliteControllerStore implements ControllerStore {
2506
2468
  this.state.connection
2507
2469
  .prepare(
2508
2470
  `UPDATE runs
2509
- SET status = ?, output_hash = ?, error_hash = ?, updated_at = ?, finished_at = ?
2471
+ SET status = ?, paused = 0, status_detail = NULL, error_hash = ?,
2472
+ current_node = NULL, current_attempt_id = NULL,
2473
+ current_node_started_at = NULL, waiting_on = NULL,
2474
+ updated_at = ?, finished_at = ?
2510
2475
  WHERE run_id = ?`,
2511
2476
  )
2512
- .run(status, outputHash, errorHash, now, now, runId);
2477
+ .run(status, errorHash, now, now, runId);
2513
2478
  this.bumpResource(row.resourceId, revision, now);
2514
2479
  this.insertEvent(
2515
2480
  row.resourceId,
@@ -2811,8 +2776,9 @@ function workflowSelect(clause: string): string {
2811
2776
  function workflowRunSelect(clause: string): string {
2812
2777
  return `SELECT r.run_id AS runId, r.resource_id AS resourceId,
2813
2778
  d.workflow_name AS workflowName, r.workflow_ref AS workflowRef, r.status AS runStatus,
2814
- r.workflow_source_hash AS workflowSourceHash, r.definition_digest AS definitionDigest,
2815
- r.input_hash AS inputHash, r.output_hash AS outputHash,
2779
+ r.workflow_source_hash AS workflowSourceHash,
2780
+ r.workflow_sources_hash AS workflowSourcesHash,
2781
+ r.definition_digest AS definitionDigest, r.input_hash AS inputHash,
2816
2782
  r.launch_options_hash AS launchOptionsHash,
2817
2783
  q.status, q.available_at AS availableAt, q.affinity_runner_id AS affinityRunnerId,
2818
2784
  q.consecutive_errors AS consecutiveErrors, q.error_code AS errorCode,
@@ -28,6 +28,7 @@ const TELEGRAM_TEXT_LIMIT = 4_096;
28
28
  const PI_PRESENTATION_WINDOW_LINES = 18;
29
29
  const DEFAULT_API_BASE = "https://api.telegram.org";
30
30
  const LEASE_TTL_MS = 60_000;
31
+ const LEASE_RENEW_WINDOW_MS = 20_000;
31
32
  const LEASE_RETRY_MS = 5_000;
32
33
  const POLL_BACKOFF_MS = 1_000;
33
34
  const MAX_SETTLEMENT_ATTEMPTS = 3;
@@ -428,6 +429,21 @@ class TelegramProjection {
428
429
  }
429
430
 
430
431
  acquire(now = Date.now()): boolean {
432
+ const observed = this.lease();
433
+ const observedToken = tokenHash(this.token);
434
+ const ownedByThis =
435
+ observed.ownerId === this.owner &&
436
+ observed.tokenHash !== null &&
437
+ observed.tokenHash.equals(observedToken) &&
438
+ observed.expiresAt !== null &&
439
+ observed.expiresAt > now;
440
+ if (ownedByThis) {
441
+ this.generation = observed.generation;
442
+ return true;
443
+ }
444
+ if (observed.ownerId !== null && observed.expiresAt !== null && observed.expiresAt > now) {
445
+ return false;
446
+ }
431
447
  return this.state.transaction(() => {
432
448
  const lease = this.lease();
433
449
  if (
@@ -438,7 +454,11 @@ class TelegramProjection {
438
454
  ) {
439
455
  return false;
440
456
  }
441
- const generation = lease.ownerId === this.owner ? lease.generation : lease.generation + 1;
457
+ const sameToken =
458
+ lease.ownerId === this.owner &&
459
+ lease.tokenHash !== null &&
460
+ lease.tokenHash.equals(observedToken);
461
+ const generation = sameToken ? lease.generation : lease.generation + 1;
442
462
  const result = this.state.connection
443
463
  .prepare(
444
464
  `UPDATE leases
@@ -449,7 +469,7 @@ class TelegramProjection {
449
469
  .run(
450
470
  generation,
451
471
  this.owner,
452
- tokenHash(this.token),
472
+ observedToken,
453
473
  now,
454
474
  now,
455
475
  now + LEASE_TTL_MS,
@@ -465,6 +485,18 @@ class TelegramProjection {
465
485
  }
466
486
 
467
487
  renew(now = Date.now()): boolean {
488
+ const lease = this.lease();
489
+ if (
490
+ lease.ownerId !== this.owner ||
491
+ lease.generation !== this.generation ||
492
+ lease.tokenHash === null ||
493
+ !lease.tokenHash.equals(tokenHash(this.token)) ||
494
+ lease.expiresAt === null ||
495
+ lease.expiresAt <= now
496
+ ) {
497
+ return false;
498
+ }
499
+ if (lease.expiresAt - now > LEASE_RENEW_WINDOW_MS) return true;
468
500
  return (
469
501
  this.state.connection
470
502
  .prepare(
@@ -1222,11 +1254,11 @@ export class TelegramDecisionChannel implements HumanDecisionChannel {
1222
1254
 
1223
1255
  private async poll(signal: AbortSignal): Promise<void> {
1224
1256
  while (this.running && !signal.aborted) {
1225
- if (!this.projection.acquire()) {
1226
- await wait(LEASE_RETRY_MS, signal);
1227
- continue;
1228
- }
1229
1257
  try {
1258
+ if (!this.projection.acquire()) {
1259
+ await wait(leaseRetryDelay(), signal);
1260
+ continue;
1261
+ }
1230
1262
  await this.deliverPendingRequests();
1231
1263
  const result = await this.call(
1232
1264
  "getUpdates",
@@ -1246,7 +1278,11 @@ export class TelegramDecisionChannel implements HumanDecisionChannel {
1246
1278
  if (!this.projection.renew()) this.projection.release();
1247
1279
  } catch {
1248
1280
  if (signal.aborted) return;
1249
- await wait(POLL_BACKOFF_MS, signal);
1281
+ try {
1282
+ await wait(POLL_BACKOFF_MS, signal);
1283
+ } catch {
1284
+ if (signal.aborted) return;
1285
+ }
1250
1286
  }
1251
1287
  }
1252
1288
  }
@@ -1773,6 +1809,10 @@ async function writePrivateJson(filePath: string, value: unknown): Promise<void>
1773
1809
  await fsp.chmod(filePath, 0o600);
1774
1810
  }
1775
1811
 
1812
+ function leaseRetryDelay(): number {
1813
+ return LEASE_RETRY_MS + Math.floor(Math.random() * 1_000);
1814
+ }
1815
+
1776
1816
  async function wait(milliseconds: number, signal: AbortSignal): Promise<void> {
1777
1817
  await new Promise<void>((resolve) => {
1778
1818
  const timer = setTimeout(resolve, milliseconds);
@@ -253,10 +253,7 @@ export class ConversationStepExecutor implements AgentStepExecutor {
253
253
  });
254
254
  return {
255
255
  accepted: true,
256
- message: [
257
- `Output accepted for step ${JSON.stringify(stepId)}.`,
258
- "If the workflow continues, the next step arrives as a new workflow message. End your turn now.",
259
- ].join(" "),
256
+ message: `Output accepted for step ${JSON.stringify(stepId)}.`,
260
257
  };
261
258
  }
262
259
 
@@ -9,6 +9,7 @@ import type {
9
9
  } from "../controllers/sqlite.js";
10
10
  import type { JsonObject } from "../controllers/types.js";
11
11
  import type { WorkflowSchedulerResult } from "../controllers/workflows.js";
12
+ import { canonicalJson } from "../state/json.js";
12
13
  import { compositionMetadata } from "../workflows/composition.js";
13
14
  import { humanDecisionChannelRequest } from "../workflows/decision-presentation.js";
14
15
  import { WorkflowEngine } from "../workflows/engine.js";
@@ -219,7 +220,7 @@ type StartRunOptions = {
219
220
  };
220
221
 
221
222
  function definitionDigest(snapshot: WorkflowDefinitionSnapshot): string {
222
- return `sha256:${createHash("sha256").update(JSON.stringify(snapshot)).digest("hex")}`;
223
+ return `sha256:${createHash("sha256").update(canonicalJson(snapshot)).digest("hex")}`;
223
224
  }
224
225
 
225
226
  function launchSourceIdentity(workflow: WorkflowDefinition, root: unknown): unknown {
@@ -560,7 +561,6 @@ export default function piWorkflows(pi: ExtensionAPI) {
560
561
  };
561
562
  let activeRun: ActiveRun | null = null;
562
563
  let systemTurnAbort: { contract: AgentStepContract; intentId?: string } | null = null;
563
- let suppressWorkflowAssistantTail = false;
564
564
  let lastExpiredAttempt: { contract: AgentStepContract; reason: string } | null = null;
565
565
  // The interactive run currently parked at a checkpoint, if any.
566
566
  let lastWaitingRunId: string | null = null;
@@ -999,7 +999,6 @@ export default function piWorkflows(pi: ExtensionAPI) {
999
999
  resolution: "presentation",
1000
1000
  send: (turnIntentId) => {
1001
1001
  presentationPending = run.generation;
1002
- suppressWorkflowAssistantTail = false;
1003
1002
  pi.sendMessage(
1004
1003
  {
1005
1004
  customType: PRESENTATION_MESSAGE_TYPE,
@@ -2916,7 +2915,6 @@ export default function piWorkflows(pi: ExtensionAPI) {
2916
2915
  if (!result.accepted) {
2917
2916
  throw new Error(result.message);
2918
2917
  }
2919
- suppressWorkflowAssistantTail = true;
2920
2918
  return {
2921
2919
  content: [{ type: "text", text: result.message }],
2922
2920
  details: { action: "submit", step: params.step, accepted: true },
@@ -2994,7 +2992,6 @@ export default function piWorkflows(pi: ExtensionAPI) {
2994
2992
  });
2995
2993
 
2996
2994
  pi.on("agent_start", () => {
2997
- suppressWorkflowAssistantTail = false;
2998
2995
  if (!activeRun && presentationPending === null && presentationAbort) {
2999
2996
  // A normal user turn started while an async presentation prompt was
3000
2997
  // still resolving. The user's new request supersedes that old result.
@@ -3005,7 +3002,6 @@ export default function piWorkflows(pi: ExtensionAPI) {
3005
3002
  });
3006
3003
 
3007
3004
  pi.on("agent_end", (event, ctx) => {
3008
- suppressWorkflowAssistantTail = false;
3009
3005
  const aborted = event.messages.some(
3010
3006
  (message) =>
3011
3007
  typeof message === "object" &&
@@ -3057,14 +3053,6 @@ export default function piWorkflows(pi: ExtensionAPI) {
3057
3053
  });
3058
3054
 
3059
3055
  pi.on("message_end", (event) => {
3060
- if (suppressWorkflowAssistantTail && event.message.role === "assistant") {
3061
- const message = {
3062
- ...event.message,
3063
- content: event.message.content.filter((part) => part.type !== "text"),
3064
- };
3065
- activeRun?.recorder?.handleMessageEnd({ ...event, message });
3066
- return { message };
3067
- }
3068
3056
  activeRun?.recorder?.handleMessageEnd(event);
3069
3057
  });
3070
3058
 
@@ -3127,7 +3115,6 @@ export default function piWorkflows(pi: ExtensionAPI) {
3127
3115
  sessionClosed = true;
3128
3116
  herdrProbeGeneration += 1;
3129
3117
  systemTurnAbort = null;
3130
- suppressWorkflowAssistantTail = false;
3131
3118
  turnCoordinator.clearDeferred();
3132
3119
  supersedePresentation();
3133
3120
  const run = activeRun;
@@ -246,6 +246,13 @@ export class SessionRecorder {
246
246
  return;
247
247
  }
248
248
  const normalized = normalizeAssistantEvent(event.assistantMessageEvent);
249
+ if (
250
+ normalized.type === "text_delta" ||
251
+ normalized.type === "thinking_delta" ||
252
+ normalized.type === "toolcall_delta"
253
+ ) {
254
+ return;
255
+ }
249
256
  const toolCallId = toolCallIdFromAssistantEvent(normalized);
250
257
  if (toolCallId) {
251
258
  this.toolOwners.set(toolCallId, owner);
@@ -295,14 +302,9 @@ export class SessionRecorder {
295
302
  });
296
303
  }
297
304
 
298
- handleToolUpdate(event: ToolExecutionUpdateEventLike): void {
299
- const owner = this.toolOwners.get(event.toolCallId);
300
- if (!owner) {
301
- return;
302
- }
303
- this.enqueue(owner, "tool_execution_updated", {}, undefined, {
304
- toolCallId: event.toolCallId,
305
- });
305
+ handleToolUpdate(_event: ToolExecutionUpdateEventLike): void {
306
+ // Incremental tool progress is transient. The recorder stores the settled
307
+ // tool result and the surrounding lifecycle facts.
306
308
  }
307
309
 
308
310
  handleToolEnd(event: ToolExecutionEndEventLike): void {
@@ -106,8 +106,15 @@ CREATE TABLE runs (
106
106
  paused INTEGER NOT NULL DEFAULT 0 CHECK (paused IN (0, 1)),
107
107
  status_detail TEXT,
108
108
  input_hash BLOB NOT NULL REFERENCES blobs(blob_hash),
109
- output_hash BLOB REFERENCES blobs(blob_hash),
109
+ workflow_sources_hash BLOB REFERENCES blobs(blob_hash),
110
+ human_decision_hash BLOB REFERENCES blobs(blob_hash),
111
+ final_output_hash BLOB REFERENCES blobs(blob_hash),
110
112
  error_hash BLOB REFERENCES blobs(blob_hash),
113
+ carried_step_count INTEGER NOT NULL DEFAULT 0 CHECK (carried_step_count >= 0),
114
+ current_node TEXT,
115
+ current_attempt_id TEXT,
116
+ current_node_started_at INTEGER,
117
+ waiting_on TEXT,
111
118
  created_at INTEGER NOT NULL,
112
119
  updated_at INTEGER NOT NULL,
113
120
  finished_at INTEGER,
@@ -159,9 +166,9 @@ CREATE TABLE node_attempts (
159
166
  )),
160
167
  input_hash BLOB REFERENCES blobs(blob_hash),
161
168
  contract_hash BLOB REFERENCES blobs(blob_hash),
162
- presentation_hash BLOB REFERENCES blobs(blob_hash),
169
+ prompt_hash BLOB REFERENCES blobs(blob_hash),
163
170
  output_hash BLOB REFERENCES blobs(blob_hash),
164
- result_hash BLOB REFERENCES blobs(blob_hash),
171
+ step_metadata_hash BLOB REFERENCES blobs(blob_hash),
165
172
  error_hash BLOB REFERENCES blobs(blob_hash),
166
173
  started_at INTEGER,
167
174
  deadline_at INTEGER,
@@ -175,10 +182,22 @@ CREATE UNIQUE INDEX node_attempts_active_idx ON node_attempts(run_id)
175
182
  WHERE status IN ('pending', 'running', 'waiting');
176
183
  CREATE INDEX node_attempts_run_idx ON node_attempts(run_id, created_at);
177
184
 
185
+ CREATE TABLE run_steps (
186
+ run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
187
+ step_index INTEGER NOT NULL CHECK (step_index >= 0),
188
+ attempt_id TEXT NOT NULL REFERENCES node_attempts(attempt_id),
189
+ output_override_hash BLOB REFERENCES blobs(blob_hash),
190
+ PRIMARY KEY (run_id, step_index),
191
+ UNIQUE (run_id, attempt_id)
192
+ ) STRICT;
193
+
194
+ CREATE INDEX run_steps_attempt_idx ON run_steps(attempt_id);
195
+
178
196
  CREATE TABLE workflow_updates (
179
197
  update_id TEXT PRIMARY KEY,
180
198
  attempt_id TEXT NOT NULL REFERENCES node_attempts(attempt_id) ON DELETE CASCADE,
181
199
  update_seq INTEGER NOT NULL CHECK (update_seq > 0),
200
+ run_revision INTEGER NOT NULL CHECK (run_revision > 0),
182
201
  update_type TEXT NOT NULL,
183
202
  update_key TEXT NOT NULL,
184
203
  data_hash BLOB NOT NULL REFERENCES blobs(blob_hash),
@@ -20,7 +20,6 @@ export type TemporalTool = {
20
20
  messageId: string;
21
21
  toolName: string;
22
22
  status: "running" | "finished" | "failed";
23
- updates: number;
24
23
  args?: unknown;
25
24
  result?: unknown;
26
25
  };
@@ -155,22 +154,6 @@ function foldSessionEvents(
155
154
  ? "thinking"
156
155
  : "toolCall",
157
156
  );
158
- } else if (
159
- contentIndex !== undefined &&
160
- (assistantType === "text_delta" ||
161
- assistantType === "thinking_delta" ||
162
- assistantType === "toolcall_delta")
163
- ) {
164
- const block = ensureBlock(
165
- message,
166
- contentIndex,
167
- assistantType === "text_delta"
168
- ? "text"
169
- : assistantType === "thinking_delta"
170
- ? "thinking"
171
- : "toolCall",
172
- );
173
- block.text += payloadString(event.payload, "delta") ?? "";
174
157
  } else if (
175
158
  contentIndex !== undefined &&
176
159
  (assistantType === "text_end" || assistantType === "thinking_end")
@@ -181,10 +164,10 @@ function foldSessionEvents(
181
164
  assistantType === "text_end" ? "text" : "thinking",
182
165
  );
183
166
  const content = payloadString(event.payload, "content") ?? "";
184
- if (block.text !== content) {
167
+ if (block.text.length > 0 && block.text !== content) {
185
168
  diagnostics.push(`${assistantType} mismatch for ${event.messageId}:${contentIndex}`);
186
- block.text = content;
187
169
  }
170
+ block.text = content;
188
171
  } else if (contentIndex !== undefined && assistantType === "toolcall_end") {
189
172
  const block = ensureBlock(message, contentIndex, "toolCall");
190
173
  block.value = event.payload.toolCall;
@@ -229,22 +212,12 @@ function foldSessionEvents(
229
212
  messageId: event.messageId,
230
213
  toolName: payloadString(event.payload, "toolName") ?? "tool",
231
214
  status: "running",
232
- updates: 0,
233
215
  ...(event.payload.args === undefined ? {} : { args: event.payload.args }),
234
216
  };
235
217
  tools.set(event.toolCallId, tool);
236
218
  toolOrder.push(event.toolCallId);
237
219
  break;
238
220
  }
239
- case "tool_execution_updated": {
240
- const tool = event.toolCallId ? tools.get(event.toolCallId) : undefined;
241
- if (tool) {
242
- tool.updates += 1;
243
- } else {
244
- diagnostics.push(`tool_execution_updated ${event.seq} precedes start`);
245
- }
246
- break;
247
- }
248
221
  case "tool_execution_finished": {
249
222
  const tool = event.toolCallId ? tools.get(event.toolCallId) : undefined;
250
223
  if (!tool) {
@@ -1,5 +1,6 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { isDeepStrictEqual } from "node:util";
3
+ import { canonicalJson } from "../state/json.js";
3
4
  import {
4
5
  compileWorkflowDefinition,
5
6
  compositionMetadata,
@@ -723,8 +724,8 @@ export class WorkflowEngine {
723
724
  throw new RunParkedError();
724
725
  }
725
726
  this.recordAttempt(workflow, state, attempt);
726
- // The terminal node event carries the output, receipt, and conversation
727
- // linkage so the trace alone is sufficient to reconstruct the run.
727
+ // The durable step row owns the output. The trace keeps the terminal fact
728
+ // and compact execution metadata without copying the output value.
728
729
  await this.persist(runId, state, {
729
730
  scope: "node",
730
731
  type: attempt.result.outcome === "ok" ? "node_finished" : "node_failed",
@@ -1561,7 +1562,7 @@ function workflowIdentityMismatch(
1561
1562
 
1562
1563
  function definitionDigest(workflow: WorkflowDefinition): string {
1563
1564
  return `sha256:${createHash("sha256")
1564
- .update(JSON.stringify(createDefinitionSnapshot(workflow)))
1565
+ .update(canonicalJson(createDefinitionSnapshot(workflow)))
1565
1566
  .digest("hex")}`;
1566
1567
  }
1567
1568