@bastani/atomic 0.9.11-alpha.1 → 0.9.11-alpha.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 (133) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/builtin/cursor/package.json +3 -3
  3. package/dist/builtin/intercom/package.json +2 -2
  4. package/dist/builtin/mcp/package.json +3 -3
  5. package/dist/builtin/subagents/package.json +4 -4
  6. package/dist/builtin/web-access/package.json +2 -2
  7. package/dist/builtin/workflows/CHANGELOG.md +6 -0
  8. package/dist/builtin/workflows/package.json +2 -2
  9. package/dist/builtin/workflows/src/durable/child-primitive.ts +3 -0
  10. package/dist/builtin/workflows/src/durable/completed-catalog.ts +182 -33
  11. package/dist/builtin/workflows/src/durable/completed-inspection.ts +9 -4
  12. package/dist/builtin/workflows/src/durable/dbos-envelope.ts +31 -8
  13. package/dist/builtin/workflows/src/durable/resume-runtime.ts +6 -2
  14. package/dist/builtin/workflows/src/durable/stage-primitive.ts +48 -10
  15. package/dist/builtin/workflows/src/durable/types.ts +11 -0
  16. package/dist/builtin/workflows/src/engine/run-durable-stage-session.ts +9 -9
  17. package/dist/builtin/workflows/src/engine/run-durable-topology.ts +89 -0
  18. package/dist/builtin/workflows/src/engine/run.ts +18 -16
  19. package/dist/builtin/workflows/src/extension/runtime-durable-resume.ts +3 -0
  20. package/dist/builtin/workflows/src/runs/foreground/executor-stage-control.ts +3 -0
  21. package/dist/builtin/workflows/src/runs/foreground/executor-stage-factory.ts +6 -3
  22. package/dist/builtin/workflows/src/runs/foreground/executor-stage-types.ts +2 -2
  23. package/dist/builtin/workflows/src/runs/foreground/executor-types.ts +4 -1
  24. package/dist/builtin/workflows/src/tui/workflow-resume-selector.ts +2 -3
  25. package/dist/cli/args.d.ts.map +1 -1
  26. package/dist/cli/args.js +2 -0
  27. package/dist/cli/args.js.map +1 -1
  28. package/dist/core/agent-session-compaction.d.ts.map +1 -1
  29. package/dist/core/agent-session-compaction.js +7 -1
  30. package/dist/core/agent-session-compaction.js.map +1 -1
  31. package/dist/core/agent-session-services.d.ts.map +1 -1
  32. package/dist/core/agent-session-services.js +3 -0
  33. package/dist/core/agent-session-services.js.map +1 -1
  34. package/dist/core/agent-session-tree.d.ts.map +1 -1
  35. package/dist/core/agent-session-tree.js +4 -1
  36. package/dist/core/agent-session-tree.js.map +1 -1
  37. package/dist/core/agent-session-types.d.ts +15 -0
  38. package/dist/core/agent-session-types.d.ts.map +1 -1
  39. package/dist/core/agent-session-types.js.map +1 -1
  40. package/dist/core/compaction/branch-summarization.d.ts +5 -1
  41. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  42. package/dist/core/compaction/branch-summarization.js +7 -8
  43. package/dist/core/compaction/branch-summarization.js.map +1 -1
  44. package/dist/core/compaction/compaction-runner.d.ts +3 -1
  45. package/dist/core/compaction/compaction-runner.d.ts.map +1 -1
  46. package/dist/core/compaction/compaction-runner.js +6 -1
  47. package/dist/core/compaction/compaction-runner.js.map +1 -1
  48. package/dist/core/compaction/range-planner.d.ts +3 -0
  49. package/dist/core/compaction/range-planner.d.ts.map +1 -1
  50. package/dist/core/compaction/range-planner.js +2 -1
  51. package/dist/core/compaction/range-planner.js.map +1 -1
  52. package/dist/core/model-registry.js +2 -2
  53. package/dist/core/model-registry.js.map +1 -1
  54. package/dist/core/model-resolver-defaults.d.ts.map +1 -1
  55. package/dist/core/model-resolver-defaults.js +2 -0
  56. package/dist/core/model-resolver-defaults.js.map +1 -1
  57. package/dist/core/prompt-templates.d.ts +1 -0
  58. package/dist/core/prompt-templates.d.ts.map +1 -1
  59. package/dist/core/prompt-templates.js +6 -4
  60. package/dist/core/prompt-templates.js.map +1 -1
  61. package/dist/core/remote-catalog-provider.d.ts +1 -1
  62. package/dist/core/remote-catalog-provider.d.ts.map +1 -1
  63. package/dist/core/remote-catalog-provider.js +25 -6
  64. package/dist/core/remote-catalog-provider.js.map +1 -1
  65. package/dist/core/sdk-exports.d.ts +1 -1
  66. package/dist/core/sdk-exports.d.ts.map +1 -1
  67. package/dist/core/sdk-exports.js.map +1 -1
  68. package/dist/core/sdk.d.ts.map +1 -1
  69. package/dist/core/sdk.js +4 -1
  70. package/dist/core/sdk.js.map +1 -1
  71. package/dist/core/session-manager-core.d.ts +1 -0
  72. package/dist/core/session-manager-core.d.ts.map +1 -1
  73. package/dist/core/session-manager-core.js +7 -4
  74. package/dist/core/session-manager-core.js.map +1 -1
  75. package/dist/core/session-manager-validation.d.ts.map +1 -1
  76. package/dist/core/session-manager-validation.js +3 -16
  77. package/dist/core/session-manager-validation.js.map +1 -1
  78. package/dist/core/summarization-retry.d.ts +11 -0
  79. package/dist/core/summarization-retry.d.ts.map +1 -0
  80. package/dist/core/summarization-retry.js +21 -0
  81. package/dist/core/summarization-retry.js.map +1 -0
  82. package/dist/core/tools/read.js +1 -1
  83. package/dist/core/tools/read.js.map +1 -1
  84. package/dist/index.d.ts +1 -1
  85. package/dist/index.d.ts.map +1 -1
  86. package/dist/index.js.map +1 -1
  87. package/dist/main.d.ts.map +1 -1
  88. package/dist/main.js +2 -0
  89. package/dist/main.js.map +1 -1
  90. package/dist/modes/interactive/components/chat-session-host-events.d.ts.map +1 -1
  91. package/dist/modes/interactive/components/chat-session-host-events.js +16 -0
  92. package/dist/modes/interactive/components/chat-session-host-events.js.map +1 -1
  93. package/dist/modes/interactive/components/footer.js +2 -2
  94. package/dist/modes/interactive/components/footer.js.map +1 -1
  95. package/dist/modes/interactive/interactive-agent-events.js +6 -0
  96. package/dist/modes/interactive/interactive-agent-events.js.map +1 -1
  97. package/dist/modes/interactive/interactive-model-catalog-startup.d.ts +13 -0
  98. package/dist/modes/interactive/interactive-model-catalog-startup.d.ts.map +1 -0
  99. package/dist/modes/interactive/interactive-model-catalog-startup.js +24 -0
  100. package/dist/modes/interactive/interactive-model-catalog-startup.js.map +1 -0
  101. package/dist/modes/interactive/interactive-model-routing.js +4 -3
  102. package/dist/modes/interactive/interactive-model-routing.js.map +1 -1
  103. package/dist/modes/interactive/interactive-queueing.js +3 -2
  104. package/dist/modes/interactive/interactive-queueing.js.map +1 -1
  105. package/dist/modes/interactive/interactive-startup.d.ts.map +1 -1
  106. package/dist/modes/interactive/interactive-startup.js +4 -7
  107. package/dist/modes/interactive/interactive-startup.js.map +1 -1
  108. package/dist/modes/interactive/interactive-summarization-retry-events.d.ts +8 -0
  109. package/dist/modes/interactive/interactive-summarization-retry-events.d.ts.map +1 -0
  110. package/dist/modes/interactive/interactive-summarization-retry-events.js +40 -0
  111. package/dist/modes/interactive/interactive-summarization-retry-events.js.map +1 -0
  112. package/dist/modes/rpc/rpc-client-api.d.ts +1 -0
  113. package/dist/modes/rpc/rpc-client-api.d.ts.map +1 -1
  114. package/dist/modes/rpc/rpc-client-api.js +3 -0
  115. package/dist/modes/rpc/rpc-client-api.js.map +1 -1
  116. package/dist/modes/rpc/rpc-command-handler.d.ts.map +1 -1
  117. package/dist/modes/rpc/rpc-command-handler.js +5 -0
  118. package/dist/modes/rpc/rpc-command-handler.js.map +1 -1
  119. package/dist/modes/rpc/rpc-types.d.ts +11 -0
  120. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  121. package/dist/modes/rpc/rpc-types.js.map +1 -1
  122. package/docs/extensions.md +3 -3
  123. package/docs/json.md +7 -1
  124. package/docs/prompt-templates.md +1 -0
  125. package/docs/providers.md +4 -0
  126. package/docs/rpc.md +49 -0
  127. package/docs/sdk.md +3 -0
  128. package/docs/tui.md +2 -2
  129. package/docs/workflows.md +6 -6
  130. package/examples/extensions/README.md +1 -1
  131. package/examples/extensions/sandbox/package-lock.json +3 -3
  132. package/npm-shrinkwrap.json +36 -36
  133. package/package.json +5 -5
package/CHANGELOG.md CHANGED
@@ -2,6 +2,29 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.9.11-alpha.2] - 2026-07-21
6
+
7
+ ### Added
8
+
9
+ - Added Qwen Token Plan and Qwen Token Plan China to Atomic's built-in provider defaults and credential guidance, inherited from `pi-ai` 0.81.1 ([#6858](https://github.com/earendil-works/pi/pull/6858)).
10
+ - Added the `get_available_thinking_levels` RPC command and `RpcClient.getAvailableThinkingLevels()` so headless clients can discover the active model's exact supported cycle order ([#6865](https://github.com/earendil-works/pi/pull/6865)).
11
+ - Exported message and tool-execution lifecycle event types from the Atomic package root for extension and SDK consumers ([#6772](https://github.com/earendil-works/pi/pull/6772)).
12
+
13
+ ### Changed
14
+
15
+ - Updated `@earendil-works/pi-agent-core`, `pi-ai`, and `pi-tui` to 0.81.1, inheriting provider, transport, model-catalog, Kimi K3 thinking, OpenAI Responses retry, Codex request-ID, terminal rendering, and queue fixes from Pi 0.81.0–0.81.1 while retaining Atomic's branding, bundled packages, Verbatim Compaction, and versionless release flow.
16
+ - Restored persisted model catalogs without network access during service creation, then moved automatic network refresh into running interactive and RPC modes. Interactive startup computes its first footer provider count from the restored snapshot and starts network refresh only after TUI initialization.
17
+
18
+ ### Fixed
19
+
20
+ - Fixed transient provider failures aborting Verbatim Compaction planning or branch summarization. Both now honor the configured retry policy and publish `summarization_retry_scheduled`, `summarization_retry_attempt_start`, and `summarization_retry_finished` lifecycle events to interactive, JSON, RPC, and SDK consumers ([#6901](https://github.com/earendil-works/pi/pull/6901)).
21
+ - Fixed persisted remote model catalogs overriding newer bundled catalogs after an upgrade by comparing catalog modification times and treating legacy undated overlays as stale ([upstream commit](https://github.com/earendil-works/pi/commit/54fad505b9d8cbc8922ff55d7e2938f70cbf6a3d)).
22
+ - Fixed steering and follow-up delivery modes being lost when queued messages were flushed as compaction finished ([#6730](https://github.com/earendil-works/pi/pull/6730)).
23
+ - Fixed read-tool errors being syntax-highlighted as though they were successful file contents ([#6731](https://github.com/earendil-works/pi/pull/6731)).
24
+ - Fixed `${@:-default}` and `${ARGUMENTS:-default}` prompt-template defaults, while retaining Atomic's bounded default-value parser ([#6695](https://github.com/earendil-works/pi/issues/6695)).
25
+ - Fixed persisted sessions being read and parsed twice when opened, and adopted the shared UUIDv7 generator for new session IDs ([#6793](https://github.com/earendil-works/pi/pull/6793), [#6834](https://github.com/earendil-works/pi/pull/6834)).
26
+ - Fixed Kimi Coding usage costs being shown without the subscription estimate indicator.
27
+
5
28
  ## [0.9.11-alpha.1] - 2026-07-20
6
29
 
7
30
  ### Changed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/cursor",
3
- "version": "0.9.11-alpha.1",
3
+ "version": "0.9.11-alpha.2",
4
4
  "private": true,
5
5
  "description": "Experimental first-party Atomic extension for Cursor OAuth, model discovery, and streaming provider registration.",
6
6
  "contributors": [
@@ -40,8 +40,8 @@
40
40
  }
41
41
  },
42
42
  "dependencies": {
43
- "@bastani/atomic-natives": "0.9.11-alpha.1",
43
+ "@bastani/atomic-natives": "0.9.11-alpha.2",
44
44
  "@bufbuild/protobuf": "^2.12.1",
45
- "@earendil-works/pi-ai": "^0.80.10"
45
+ "@earendil-works/pi-ai": "^0.81.1"
46
46
  }
47
47
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/intercom",
3
- "version": "0.9.11-alpha.1",
3
+ "version": "0.9.11-alpha.2",
4
4
  "private": true,
5
5
  "description": "Atomic extension providing a private coordination channel between parent and child agent sessions. Fork of: https://github.com/nicobailon/pi-intercom",
6
6
  "contributors": [
@@ -44,7 +44,7 @@
44
44
  },
45
45
  "peerDependencies": {
46
46
  "@bastani/atomic": "*",
47
- "@earendil-works/pi-tui": "^0.80.10"
47
+ "@earendil-works/pi-tui": "^0.81.1"
48
48
  },
49
49
  "peerDependenciesMeta": {
50
50
  "@bastani/atomic": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/mcp",
3
- "version": "0.9.11-alpha.1",
3
+ "version": "0.9.11-alpha.2",
4
4
  "private": true,
5
5
  "description": "Atomic extension that adapts MCP (Model Context Protocol) servers into the coding agent. Fork of: https://github.com/nicobailon/pi-mcp-adapter",
6
6
  "contributors": [
@@ -32,8 +32,8 @@
32
32
  },
33
33
  "peerDependencies": {
34
34
  "@bastani/atomic": "*",
35
- "@earendil-works/pi-ai": "^0.80.10",
36
- "@earendil-works/pi-tui": "^0.80.10",
35
+ "@earendil-works/pi-ai": "^0.81.1",
36
+ "@earendil-works/pi-tui": "^0.81.1",
37
37
  "zod": "^3.25.0 || ^4.0.0"
38
38
  },
39
39
  "peerDependenciesMeta": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/subagents",
3
- "version": "0.9.11-alpha.1",
3
+ "version": "0.9.11-alpha.2",
4
4
  "private": true,
5
5
  "description": "Atomic extension for delegating tasks to subagents with chains, parallel execution, and background runs. Fork of: https://github.com/nicobailon/pi-subagents",
6
6
  "contributors": [
@@ -38,9 +38,9 @@
38
38
  },
39
39
  "peerDependencies": {
40
40
  "@bastani/atomic": "*",
41
- "@earendil-works/pi-agent-core": "^0.80.10",
42
- "@earendil-works/pi-ai": "^0.80.10",
43
- "@earendil-works/pi-tui": "^0.80.10"
41
+ "@earendil-works/pi-agent-core": "^0.81.1",
42
+ "@earendil-works/pi-ai": "^0.81.1",
43
+ "@earendil-works/pi-tui": "^0.81.1"
44
44
  },
45
45
  "peerDependenciesMeta": {
46
46
  "@bastani/atomic": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/web-access",
3
- "version": "0.9.11-alpha.1",
3
+ "version": "0.9.11-alpha.2",
4
4
  "private": true,
5
5
  "description": "Atomic extension for web search, URL fetching, GitHub repo cloning, PDF/video extraction. Fork of: https://github.com/nicobailon/pi-web-access",
6
6
  "contributors": [
@@ -31,7 +31,7 @@
31
31
  },
32
32
  "peerDependencies": {
33
33
  "@bastani/atomic": "*",
34
- "@earendil-works/pi-tui": "^0.80.10"
34
+ "@earendil-works/pi-tui": "^0.81.1"
35
35
  },
36
36
  "peerDependenciesMeta": {
37
37
  "@bastani/atomic": {
@@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.9.11-alpha.2] - 2026-07-21
10
+
11
+ ### Fixed
12
+
13
+ - Fixed fresh-process workflow resume and completed-run inspection losing nested child-workflow and parallel stage hierarchy. DBOS stage checkpoints now retain owning-run and boundary topology, replay restores source parentage and child runs, graceful quit persists exact sub-30-second stage timing, completed runs persist and display accumulated run duration, and project-local workflows reloaded during a chat are rediscovered from their original invocation directory. The `/workflow resume` and `/workflows` picker rows also no longer display the redundant `N prompts` segment.
14
+
9
15
  ## [0.9.11-alpha.1] - 2026-07-20
10
16
 
11
17
  ### Added
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/workflows",
3
- "version": "0.9.11-alpha.1",
3
+ "version": "0.9.11-alpha.2",
4
4
  "private": true,
5
5
  "description": "Atomic extension for multi-stage workflow authoring and execution.",
6
6
  "contributors": [
@@ -83,7 +83,7 @@
83
83
  },
84
84
  "peerDependencies": {
85
85
  "@bastani/atomic": "*",
86
- "@earendil-works/pi-tui": "^0.80.10"
86
+ "@earendil-works/pi-tui": "^0.81.1"
87
87
  },
88
88
  "peerDependenciesMeta": {
89
89
  "@bastani/atomic": {
@@ -11,6 +11,7 @@ import type { DurableWorkflowBackend } from "./backend.js";
11
11
  import type { DurableScope } from "./scoped-backend.js";
12
12
  import { recordCheckpointDurably } from "./tool-primitive.js";
13
13
  import { stageCheckpointWithOutput, type DurableCompletedStageCheckpoint } from "./stage-primitive.js";
14
+ import type { DurableStageCheckpoint } from "./types.js";
14
15
 
15
16
  export function createDurableChildWorkflowPrimitive(input: {
16
17
  readonly workflowId: string;
@@ -18,6 +19,7 @@ export function createDurableChildWorkflowPrimitive(input: {
18
19
  readonly backend: DurableWorkflowBackend;
19
20
  readonly nextReplayKey: (name: string) => string;
20
21
  readonly recordCachedStage: (name: string, replayKey: string, checkpoint: DurableCompletedStageCheckpoint) => void;
22
+ readonly checkpointMetadata: (replayKey: string) => Partial<DurableStageCheckpoint>;
21
23
  /**
22
24
  * Publish the durable scope computed for the next child invocation so the
23
25
  * child runner can consume it and route its internal side-effect checkpoints
@@ -66,6 +68,7 @@ export function createDurableChildWorkflowPrimitive(input: {
66
68
  output: result as WorkflowSerializableValue,
67
69
  completedAt: Date.now(),
68
70
  result: result.status,
71
+ ...input.checkpointMetadata(replayKey),
69
72
  });
70
73
  return result as WorkflowChildResult<TChildOutputs>;
71
74
  };
@@ -1,5 +1,5 @@
1
1
  import type { RunSnapshot, StageSnapshot } from "../shared/store-types.js";
2
- import type { WorkflowInputValues } from "../shared/types.js";
2
+ import type { WorkflowChildResult, WorkflowInputValues, WorkflowOutputValues, WorkflowSerializableValue } from "../shared/types.js";
3
3
  import { isReopenableSessionTranscript } from "../shared/session-transcript.js";
4
4
  import type { DurableWorkflowBackend } from "./backend.js";
5
5
  import {
@@ -9,6 +9,7 @@ import {
9
9
  type ResumableWorkflowEntry,
10
10
  } from "./types.js";
11
11
  import { resolveDurableEntry } from "./resume-runtime.js";
12
+ import { priorRunElapsedMs } from "./run-timing.js";
12
13
 
13
14
  export type CompletedWorkflowResolution =
14
15
  | { readonly kind: "found"; readonly entry: ResumableWorkflowEntry; readonly snapshot: RunSnapshot }
@@ -76,24 +77,52 @@ export function completedWorkflowSnapshot(
76
77
  backend: DurableWorkflowBackend,
77
78
  entry: ResumableWorkflowEntry,
78
79
  ): RunSnapshot | undefined {
80
+ const runs = completedWorkflowRunSnapshots(backend, entry);
81
+ const root = runs.find((run) => run.id === entry.workflowId);
82
+ if (root === undefined || !runs.some((run) => run.stages.some((stage) => stage.sessionFile !== undefined))) return undefined;
83
+ return root;
84
+ }
85
+
86
+ /** Rebuild the root plus every nested child run required by graph expansion. */
87
+ export function completedWorkflowRunSnapshots(
88
+ backend: DurableWorkflowBackend,
89
+ entry: ResumableWorkflowEntry,
90
+ ): readonly RunSnapshot[] {
79
91
  const handle = backend.getWorkflow(entry.workflowId);
80
- if (handle === undefined || handle.status !== "completed") return undefined;
92
+ if (handle === undefined || handle.status !== "completed") return [];
81
93
  const checkpoints = backend.listCheckpoints(entry.workflowId);
82
- if (checkpoints.length === 0) return undefined;
83
- const stages = stageSnapshotsFromCheckpoints(checkpoints, handle.updatedAt).map(validatedStageTranscript);
84
- if (!stages.some((stage) => stage.sessionFile !== undefined)) return undefined;
85
-
86
- return {
87
- id: handle.workflowId,
88
- name: handle.name,
94
+ if (checkpoints.length === 0) return [];
95
+ const runs = runSnapshotsFromCheckpoints(checkpoints, handle.workflowId, handle.name, handle.updatedAt)
96
+ .map((run) => ({ ...run, stages: run.stages.map(validatedStageTranscript) }));
97
+ const rootIndex = runs.findIndex((run) => run.id === handle.workflowId);
98
+ if (rootIndex < 0) return [];
99
+ const rootDuration = priorRunElapsedMs(backend, handle.workflowId)
100
+ ?? Math.max(0, handle.updatedAt - handle.createdAt);
101
+ const root: RunSnapshot = {
102
+ ...runs[rootIndex]!,
89
103
  inputs: { ...handle.inputs } as WorkflowInputValues,
90
- status: "completed",
91
- stages,
92
104
  startedAt: handle.createdAt,
93
105
  endedAt: handle.updatedAt,
94
- durationMs: Math.max(0, handle.updatedAt - handle.createdAt),
106
+ durationMs: rootDuration,
95
107
  resumable: false,
96
108
  };
109
+ return [root, ...runs.filter((_, index) => index !== rootIndex)];
110
+ }
111
+
112
+ /** Rebuild completed nested runs while a paused root is replaying cached boundaries. */
113
+ export function durableNestedRunSnapshots(
114
+ backend: DurableWorkflowBackend,
115
+ rootWorkflowId: string,
116
+ ): readonly RunSnapshot[] {
117
+ const handle = backend.getWorkflow(rootWorkflowId);
118
+ if (handle === undefined) return [];
119
+ return runSnapshotsFromCheckpoints(
120
+ backend.listCheckpoints(rootWorkflowId),
121
+ rootWorkflowId,
122
+ handle.name,
123
+ handle.updatedAt,
124
+ false,
125
+ ).filter((run) => run.id !== rootWorkflowId);
97
126
  }
98
127
 
99
128
  function validatedStageTranscript(stage: StageSnapshot): StageSnapshot {
@@ -104,19 +133,109 @@ function validatedStageTranscript(stage: StageSnapshot): StageSnapshot {
104
133
  }
105
134
 
106
135
 
107
- function stageSnapshotsFromCheckpoints(
136
+ function runSnapshotsFromCheckpoints(
108
137
  checkpoints: readonly DurableCheckpoint[],
138
+ rootRunId: string,
139
+ rootRunName: string,
109
140
  fallbackCompletedAt: number,
110
- ): StageSnapshot[] {
141
+ strict = true,
142
+ ): RunSnapshot[] {
111
143
  const drafts = new Map<string, StageDraft>();
112
144
  for (const checkpoint of checkpoints) {
113
145
  if (checkpoint.kind !== "stage") continue;
114
146
  const existing = drafts.get(checkpoint.replayKey);
115
147
  drafts.set(checkpoint.replayKey, mergeStageDraft(existing, checkpoint));
116
148
  }
117
- const ordered = [...drafts.values()].sort((a, b) => a.firstCompletedAt - b.firstCompletedAt);
118
- if (ordered.length === 0) return [syntheticCheckpointStage(checkpoints.length, fallbackCompletedAt)];
119
- return stageSnapshotsFromDrafts(ordered);
149
+ const ordered = [...drafts.values()]
150
+ .filter((draft) => !strict || draft.topology?.run === undefined || draft.endedAt !== undefined || draft.output !== undefined)
151
+ .sort((a, b) => a.firstCompletedAt - b.firstCompletedAt);
152
+ if (ordered.length === 0) return [syntheticRun(rootRunId, rootRunName, checkpoints.length, fallbackCompletedAt)];
153
+
154
+ const grouped = new Map<string, StageDraft[]>();
155
+ for (const draft of ordered) {
156
+ if (draft.topology?.version !== DURABLE_STAGE_TOPOLOGY_VERSION) {
157
+ if (strict) return [];
158
+ continue;
159
+ }
160
+ const runId = draft.topology.run?.runId ?? rootRunId;
161
+ const group = grouped.get(runId) ?? [];
162
+ group.push(draft);
163
+ grouped.set(runId, group);
164
+ }
165
+ retainReachableRunGroups(grouped, rootRunId);
166
+ const idMaps = new Map<string, Map<string, string>>();
167
+ for (const [runId, runDrafts] of grouped) {
168
+ const ids = new Map<string, string>();
169
+ runDrafts.forEach((draft, index) => {
170
+ const sourceId = draft.topology!.stageId;
171
+ if (!ids.has(sourceId)) ids.set(sourceId, `completed-stage-${index + 1}`);
172
+ });
173
+ if (ids.size !== runDrafts.length) {
174
+ if (strict) return [];
175
+ grouped.delete(runId);
176
+ continue;
177
+ }
178
+ idMaps.set(runId, ids);
179
+ }
180
+
181
+ const runs: RunSnapshot[] = [];
182
+ for (const [runId, runDrafts] of grouped) {
183
+ const ids = idMaps.get(runId)!;
184
+ if (runDrafts.some((draft) => draft.topology!.parentIds.some((parentId) => !ids.has(parentId)))) {
185
+ if (strict) return [];
186
+ continue;
187
+ }
188
+ const stages = runDrafts.map((draft) => stageSnapshotFromDraft(
189
+ draft,
190
+ ids.get(draft.topology!.stageId)!,
191
+ draft.topology!.parentIds.map((parentId) => ids.get(parentId)!),
192
+ ));
193
+ const run = runDrafts.find((draft) => draft.topology?.run !== undefined)?.topology?.run;
194
+ const startedAt = Math.min(...stages.map((stage) => stage.startedAt ?? fallbackCompletedAt));
195
+ const endedAt = Math.max(...stages.map((stage) => stage.endedAt ?? fallbackCompletedAt));
196
+ const parentRunId = run?.parentRunId ?? rootRunId;
197
+ const boundarySourceId = grouped.get(parentRunId)?.find((draft) =>
198
+ workflowChildFromOutput(draft.output)?.runId === runId
199
+ )?.topology?.stageId;
200
+ const declaredParentStageId = run?.parentStageId === undefined
201
+ ? undefined
202
+ : idMaps.get(parentRunId)?.get(run.parentStageId);
203
+ const parentStageId = declaredParentStageId
204
+ ?? (boundarySourceId === undefined ? undefined : idMaps.get(parentRunId)?.get(boundarySourceId));
205
+ runs.push({
206
+ id: runId,
207
+ name: run?.runName ?? rootRunName,
208
+ inputs: {},
209
+ status: "completed",
210
+ stages,
211
+ startedAt,
212
+ endedAt,
213
+ durationMs: Math.max(0, endedAt - startedAt),
214
+ ...(run?.parentRunId !== undefined ? { parentRunId: run.parentRunId } : {}),
215
+ ...(parentStageId !== undefined ? { parentStageId } : {}),
216
+ ...(run?.rootRunId !== undefined ? { rootRunId: run.rootRunId } : {}),
217
+ resumable: false,
218
+ });
219
+ }
220
+ return runs;
221
+ }
222
+
223
+ function retainReachableRunGroups(grouped: Map<string, StageDraft[]>, rootRunId: string): void {
224
+ const reachable = new Set([rootRunId]);
225
+ const pending = [rootRunId];
226
+ while (pending.length > 0) {
227
+ const drafts = grouped.get(pending.pop()!);
228
+ if (drafts === undefined) continue;
229
+ for (const draft of drafts) {
230
+ const childRunId = workflowChildFromOutput(draft.output)?.runId;
231
+ if (childRunId === undefined || reachable.has(childRunId) || !grouped.has(childRunId)) continue;
232
+ reachable.add(childRunId);
233
+ pending.push(childRunId);
234
+ }
235
+ }
236
+ for (const runId of grouped.keys()) {
237
+ if (!reachable.has(runId)) grouped.delete(runId);
238
+ }
120
239
  }
121
240
 
122
241
  function mergeStageDraft(
@@ -138,10 +257,21 @@ function mergeStageDraft(
138
257
  ...valueOrExisting("fastMode", checkpoint, existing),
139
258
  ...valueOrExisting("attemptedModels", checkpoint, existing),
140
259
  ...valueOrExisting("modelAttempts", checkpoint, existing),
141
- ...valueOrExisting("topology", checkpoint, existing),
260
+ ...preferredTopology(checkpoint, existing),
142
261
  };
143
262
  }
144
263
 
264
+ function preferredTopology(
265
+ checkpoint: DurableStageCheckpoint,
266
+ existing: StageDraft | undefined,
267
+ ): Pick<StageDraft, "topology"> | object {
268
+ const current = checkpoint.topology;
269
+ const prior = existing?.topology;
270
+ if (current?.run !== undefined) return { topology: current };
271
+ if (prior?.run !== undefined) return { topology: prior };
272
+ return current !== undefined ? { topology: current } : prior !== undefined ? { topology: prior } : {};
273
+ }
274
+
145
275
  function valueOrExisting<
146
276
  K extends keyof Omit<StageDraft, "replayKey" | "name" | "firstCompletedAt">
147
277
  >(key: K, checkpoint: DurableStageCheckpoint, existing: StageDraft | undefined): Pick<StageDraft, K> | object {
@@ -151,21 +281,25 @@ function valueOrExisting<
151
281
  return existingValue === undefined ? {} : { [key]: existingValue } as Pick<StageDraft, K>;
152
282
  }
153
283
 
154
- function stageSnapshotsFromDrafts(drafts: readonly StageDraft[]): StageSnapshot[] {
155
- const reconstructedIds = drafts.map((_, index) => `completed-stage-${index + 1}`);
156
- const sourceToReconstructed = new Map<string, string>();
157
- for (let index = 0; index < drafts.length; index += 1) {
158
- const topology = drafts[index]!.topology;
159
- if (topology?.version !== DURABLE_STAGE_TOPOLOGY_VERSION
160
- || sourceToReconstructed.has(topology.stageId)) return [];
161
- sourceToReconstructed.set(topology.stageId, reconstructedIds[index]!);
162
- }
163
- if (drafts.some((draft) => draft.topology!.parentIds.some((parentId) => !sourceToReconstructed.has(parentId)))) return [];
164
- return drafts.map((draft, index) => stageSnapshotFromDraft(
165
- draft,
166
- reconstructedIds[index]!,
167
- draft.topology!.parentIds.map((parentId) => sourceToReconstructed.get(parentId)!),
168
- ));
284
+ function workflowChildFromOutput(output: WorkflowSerializableValue | undefined): StageSnapshot["workflowChild"] | undefined {
285
+ if (!isWorkflowChildResult(output)) return undefined;
286
+ return {
287
+ alias: output.workflow,
288
+ workflow: output.workflow,
289
+ runId: output.runId,
290
+ status: output.status,
291
+ ...(output.exited !== undefined ? { exited: output.exited } : {}),
292
+ outputs: output.outputs,
293
+ ...(typeof output.exitReason === "string" ? { exitReason: output.exitReason } : {}),
294
+ };
295
+ }
296
+
297
+ function isWorkflowChildResult(value: WorkflowSerializableValue | undefined): value is WorkflowChildResult<WorkflowOutputValues> {
298
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
299
+ const item = value as Record<string, WorkflowSerializableValue | undefined>;
300
+ return typeof item["workflow"] === "string" && typeof item["runId"] === "string"
301
+ && typeof item["status"] === "string" && typeof item["outputs"] === "object"
302
+ && item["outputs"] !== null && !Array.isArray(item["outputs"]);
169
303
  }
170
304
 
171
305
  function stageSnapshotFromDraft(
@@ -187,6 +321,7 @@ function stageSnapshotFromDraft(
187
321
  replayKey: draft.replayKey,
188
322
  toolEvents: [],
189
323
  attachable: false,
324
+ ...(workflowChildFromOutput(draft.output) !== undefined ? { workflowChild: workflowChildFromOutput(draft.output) } : {}),
190
325
  ...(draft.sessionId !== undefined ? { sessionId: draft.sessionId } : {}),
191
326
  ...(draft.sessionFile !== undefined ? { sessionFile: draft.sessionFile } : {}),
192
327
  ...(draft.model !== undefined ? { model: draft.model } : {}),
@@ -202,6 +337,20 @@ function stageResult(draft: StageDraft): string | undefined {
202
337
  return typeof draft.output === "string" ? draft.output : JSON.stringify(draft.output);
203
338
  }
204
339
 
340
+ function syntheticRun(runId: string, runName: string, checkpointCount: number, completedAt: number): RunSnapshot {
341
+ return {
342
+ id: runId,
343
+ name: runName,
344
+ inputs: {},
345
+ status: "completed",
346
+ stages: [syntheticCheckpointStage(checkpointCount, completedAt)],
347
+ startedAt: completedAt,
348
+ endedAt: completedAt,
349
+ durationMs: 0,
350
+ resumable: false,
351
+ };
352
+ }
353
+
205
354
  function syntheticCheckpointStage(checkpointCount: number, completedAt: number): StageSnapshot {
206
355
  return {
207
356
  id: "completed-checkpoints",
@@ -10,6 +10,7 @@ import {
10
10
  import type { DurableWorkflowBackend } from "./backend.js";
11
11
  import {
12
12
  listOpenableCompletedWorkflows,
13
+ completedWorkflowRunSnapshots,
13
14
  resolveCompletedWorkflow,
14
15
  } from "./completed-catalog.js";
15
16
  import type { ResumableWorkflowEntry } from "./types.js";
@@ -69,10 +70,14 @@ export function openCompletedDurableWorkflow(
69
70
  `Workflow ${resolved.snapshot.id.slice(0, 8)} is already active in this session; attach with /workflow connect ${resolved.snapshot.id.slice(0, 8)} instead.`,
70
71
  );
71
72
  }
72
- const snapshot = resolved.snapshot;
73
- if (existing !== undefined) deps.store.removeRun(existing.id);
74
- deps.store.recordRunStart(snapshot);
75
- registerCompletedChatHandles(snapshot, deps);
73
+ const snapshots = completedWorkflowRunSnapshots(deps.durableBackend, resolved.entry);
74
+ const snapshot = snapshots.find((run) => run.id === resolved.snapshot.id) ?? resolved.snapshot;
75
+ for (const restored of snapshots) {
76
+ const previous = deps.store.runs().find((run) => run.id === restored.id);
77
+ if (previous !== undefined) deps.store.removeRun(previous.id);
78
+ deps.store.recordRunStart(restored);
79
+ registerCompletedChatHandles(restored, deps);
80
+ }
76
81
  return {
77
82
  ok: true,
78
83
  runId: snapshot.id,
@@ -62,12 +62,7 @@ export interface DbosCheckpointEnvelope extends WorkflowSerializableObject {
62
62
  readonly topology?: WorkflowSerializableValue;
63
63
  }
64
64
 
65
- /**
66
- * Stage-kind checkpoints from older session-timing writers may omit
67
- * topology. The current format requires it, so default at every write
68
- * boundary — persisted records must match the normalized in-memory mirror or
69
- * a fresh process would reject them as foreign.
70
- */
65
+ /** Add a compatibility root topology when a legacy/output-only writer omits it. */
71
66
  export function withCurrentStageTopology(cp: DurableCheckpoint): DurableCheckpoint {
72
67
  if (cp.kind !== "stage" || cp.topology !== undefined) return cp;
73
68
  return {
@@ -104,7 +99,12 @@ export function encodeCheckpoint(checkpoint: DurableCheckpoint): DbosCheckpointE
104
99
  ...base,
105
100
  name: s.name,
106
101
  replayKey: s.replayKey,
107
- ...(s.topology !== undefined ? { topology: { version: s.topology.version, stageId: s.topology.stageId, parentIds: [...s.topology.parentIds] } } : {}),
102
+ ...(s.topology !== undefined ? { topology: {
103
+ version: s.topology.version,
104
+ stageId: s.topology.stageId,
105
+ parentIds: [...s.topology.parentIds],
106
+ ...(s.topology.run !== undefined ? { run: { ...s.topology.run } } : {}),
107
+ } } : {}),
108
108
  ...(s.sessionId !== undefined ? { sessionId: s.sessionId } : {}),
109
109
  ...(s.sessionFile !== undefined ? { sessionFile: s.sessionFile } : {}),
110
110
  ...(s.startedAt !== undefined ? { startedAt: s.startedAt } : {}),
@@ -227,7 +227,30 @@ function stageTopology(value: WorkflowSerializableValue | undefined): DurableSta
227
227
  const record = value as Record<string, WorkflowSerializableValue>;
228
228
  if (record["version"] !== DURABLE_STAGE_TOPOLOGY_VERSION
229
229
  || typeof record["stageId"] !== "string" || !isStringArray(record["parentIds"])) return undefined;
230
- return { version: DURABLE_STAGE_TOPOLOGY_VERSION, stageId: record["stageId"], parentIds: record["parentIds"] };
230
+ const run = stageRunTopology(record["run"]);
231
+ if (record["run"] !== undefined && run === undefined) return undefined;
232
+ return {
233
+ version: DURABLE_STAGE_TOPOLOGY_VERSION,
234
+ stageId: record["stageId"],
235
+ parentIds: record["parentIds"],
236
+ ...(run !== undefined ? { run } : {}),
237
+ };
238
+ }
239
+
240
+ function stageRunTopology(value: WorkflowSerializableValue | undefined): NonNullable<DurableStageTopology["run"]> | undefined {
241
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
242
+ const run = value as Record<string, WorkflowSerializableValue>;
243
+ if (typeof run["runId"] !== "string" || typeof run["runName"] !== "string") return undefined;
244
+ for (const key of ["parentRunId", "parentStageId", "rootRunId"] as const) {
245
+ if (run[key] !== undefined && typeof run[key] !== "string") return undefined;
246
+ }
247
+ return {
248
+ runId: run["runId"],
249
+ runName: run["runName"],
250
+ ...(typeof run["parentRunId"] === "string" ? { parentRunId: run["parentRunId"] } : {}),
251
+ ...(typeof run["parentStageId"] === "string" ? { parentStageId: run["parentStageId"] } : {}),
252
+ ...(typeof run["rootRunId"] === "string" ? { rootRunId: run["rootRunId"] } : {}),
253
+ };
231
254
  }
232
255
 
233
256
  function isModelAttempts(value: WorkflowSerializableValue | undefined): boolean {
@@ -18,7 +18,7 @@
18
18
  * top-level workflow id."
19
19
  */
20
20
 
21
- import type { WorkflowInputValues } from "../shared/types.js";
21
+ import type { WorkflowDefinition, WorkflowInputValues } from "../shared/types.js";
22
22
  import type { WorkflowRegistry } from "../workflows/registry.js";
23
23
  import type { RunOpts } from "../runs/foreground/executor-types.js";
24
24
  import { launchDetachedUntilStartup, workflowStartupFailureMessage } from "../runs/background/startup-admission.js";
@@ -44,6 +44,8 @@ export interface ResumeDurableDeps {
44
44
  readonly baseRunOpts: RunOpts;
45
45
  /** Durable backend override (defaults to the global singleton). */
46
46
  readonly durableBackend?: DurableWorkflowBackend;
47
+ /** Resolve a definition from its original invocation directory after restart. */
48
+ readonly resolveDefinition?: (name: string, cwd: string | undefined) => Promise<WorkflowDefinition | undefined>;
47
49
  /** Job tracker used by the detached resume launch. */
48
50
  readonly jobs?: JobTracker;
49
51
  }
@@ -146,7 +148,9 @@ export async function resumeDurableWorkflow(
146
148
  return { ok: false, reason: "not_resumable", message: `Workflow ${resolved.workflowId.slice(0, 8)} is ${handle.status}, not resumable.` };
147
149
  }
148
150
 
149
- const def = deps.registry.get(handle.name);
151
+ const def = handle.invocationCwd === undefined
152
+ ? deps.registry.get(handle.name)
153
+ : await deps.resolveDefinition?.(handle.name, handle.invocationCwd) ?? deps.registry.get(handle.name);
150
154
  if (def === undefined) {
151
155
  return { ok: false, reason: "workflow_not_found", message: `Workflow definition not found: ${handle.name}` };
152
156
  }