@bastani/atomic 0.9.3-alpha.1 → 0.9.3-alpha.3

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 (175) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/builtin/cursor/CHANGELOG.md +21 -0
  3. package/dist/builtin/cursor/README.md +2 -1
  4. package/dist/builtin/cursor/package.json +2 -2
  5. package/dist/builtin/cursor/src/cursor-models-raw.json +2 -9
  6. package/dist/builtin/cursor/src/model-mapper.ts +14 -3
  7. package/dist/builtin/cursor/src/proto/protobuf-codec-base64.ts +22 -0
  8. package/dist/builtin/cursor/src/proto/protobuf-codec-request.ts +53 -13
  9. package/dist/builtin/cursor/src/proto/protobuf-codec-wire.ts +24 -7
  10. package/dist/builtin/cursor/src/proto/protobuf-codec.ts +3 -2
  11. package/dist/builtin/cursor/src/stream.ts +5 -11
  12. package/dist/builtin/cursor/src/transport-types.ts +3 -0
  13. package/dist/builtin/cursor/src/transport.ts +1 -0
  14. package/dist/builtin/intercom/CHANGELOG.md +6 -0
  15. package/dist/builtin/intercom/package.json +1 -1
  16. package/dist/builtin/mcp/CHANGELOG.md +6 -0
  17. package/dist/builtin/mcp/package.json +1 -1
  18. package/dist/builtin/subagents/CHANGELOG.md +15 -0
  19. package/dist/builtin/subagents/package.json +1 -1
  20. package/dist/builtin/subagents/src/extension/fanout-child.ts +1 -0
  21. package/dist/builtin/subagents/src/extension/index.ts +6 -3
  22. package/dist/builtin/subagents/src/extension/schemas.ts +0 -5
  23. package/dist/builtin/subagents/src/runs/background/async-job-tracker.ts +1 -4
  24. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-single.ts +15 -1
  25. package/dist/builtin/subagents/src/runs/foreground/subagent-executor.ts +35 -1
  26. package/dist/builtin/subagents/src/runs/shared/subagent-prompt-runtime.ts +4 -2
  27. package/dist/builtin/subagents/src/shared/types-async.ts +1 -0
  28. package/dist/builtin/subagents/src/slash/prompt-template-bridge.ts +27 -5
  29. package/dist/builtin/subagents/src/tui/render-layout.ts +27 -4
  30. package/dist/builtin/subagents/src/tui/render-result-animation.ts +22 -31
  31. package/dist/builtin/subagents/src/tui/render-result-compact.ts +6 -6
  32. package/dist/builtin/subagents/src/tui/render-result.ts +20 -19
  33. package/dist/builtin/subagents/src/tui/render-status-progress.ts +3 -3
  34. package/dist/builtin/subagents/src/tui/render-widget.ts +46 -7
  35. package/dist/builtin/subagents/src/tui/render.ts +2 -2
  36. package/dist/builtin/web-access/CHANGELOG.md +6 -0
  37. package/dist/builtin/web-access/package.json +1 -1
  38. package/dist/builtin/workflows/CHANGELOG.md +49 -0
  39. package/dist/builtin/workflows/README.md +1 -1
  40. package/dist/builtin/workflows/package.json +1 -1
  41. package/dist/builtin/workflows/src/authoring.d.ts +1 -1
  42. package/dist/builtin/workflows/src/durable/backend.ts +343 -0
  43. package/dist/builtin/workflows/src/durable/child-primitive.ts +79 -0
  44. package/dist/builtin/workflows/src/durable/dbos-backend.ts +421 -0
  45. package/dist/builtin/workflows/src/durable/dbos-envelope.ts +171 -0
  46. package/dist/builtin/workflows/src/durable/factory.ts +96 -0
  47. package/dist/builtin/workflows/src/durable/file-backend.ts +433 -0
  48. package/dist/builtin/workflows/src/durable/index.ts +73 -0
  49. package/dist/builtin/workflows/src/durable/resume-catalog.ts +217 -0
  50. package/dist/builtin/workflows/src/durable/resume-runtime.ts +299 -0
  51. package/dist/builtin/workflows/src/durable/scoped-backend.ts +171 -0
  52. package/dist/builtin/workflows/src/durable/stage-primitive.ts +284 -0
  53. package/dist/builtin/workflows/src/durable/tool-primitive.ts +180 -0
  54. package/dist/builtin/workflows/src/durable/types.ts +168 -0
  55. package/dist/builtin/workflows/src/durable/ui-primitive.ts +96 -0
  56. package/dist/builtin/workflows/src/engine/options.ts +3 -0
  57. package/dist/builtin/workflows/src/engine/primitives/parallel.ts +2 -2
  58. package/dist/builtin/workflows/src/engine/primitives/task.ts +4 -4
  59. package/dist/builtin/workflows/src/engine/primitives/ui.ts +22 -8
  60. package/dist/builtin/workflows/src/engine/primitives/workflow.ts +8 -0
  61. package/dist/builtin/workflows/src/engine/run-durable-finalize.ts +69 -0
  62. package/dist/builtin/workflows/src/engine/run-durable-stage-session.ts +31 -0
  63. package/dist/builtin/workflows/src/engine/run.ts +148 -6
  64. package/dist/builtin/workflows/src/engine/runtime.ts +8 -2
  65. package/dist/builtin/workflows/src/extension/extension-factory.ts +6 -12
  66. package/dist/builtin/workflows/src/extension/extension-lifecycle.ts +5 -1
  67. package/dist/builtin/workflows/src/extension/extension-runtime-state.ts +3 -0
  68. package/dist/builtin/workflows/src/extension/runtime.ts +48 -9
  69. package/dist/builtin/workflows/src/extension/workflow-run-control-command.ts +143 -4
  70. package/dist/builtin/workflows/src/runs/background/quit.ts +61 -0
  71. package/dist/builtin/workflows/src/runs/background/status.ts +1 -0
  72. package/dist/builtin/workflows/src/runs/foreground/executor-direct-helpers.ts +5 -5
  73. package/dist/builtin/workflows/src/runs/foreground/executor-stage-call.ts +74 -33
  74. package/dist/builtin/workflows/src/runs/foreground/executor-stage-context.ts +20 -1
  75. package/dist/builtin/workflows/src/runs/foreground/executor-stage-factory.ts +8 -7
  76. package/dist/builtin/workflows/src/runs/foreground/executor-stage-replay.ts +1 -0
  77. package/dist/builtin/workflows/src/runs/foreground/executor-stage-types.ts +1 -1
  78. package/dist/builtin/workflows/src/runs/foreground/executor-types.ts +19 -2
  79. package/dist/builtin/workflows/src/runs/foreground/stage-runner-context.ts +4 -0
  80. package/dist/builtin/workflows/src/runs/foreground/stage-runner-controller.ts +10 -10
  81. package/dist/builtin/workflows/src/runs/foreground/stage-runner-options.ts +5 -1
  82. package/dist/builtin/workflows/src/runs/foreground/stage-runner-send-user-message.ts +25 -0
  83. package/dist/builtin/workflows/src/runs/foreground/stage-runner-types.ts +3 -0
  84. package/dist/builtin/workflows/src/shared/authoring-contract-stage.d.ts +16 -0
  85. package/dist/builtin/workflows/src/shared/authoring-contract-stage.ts +20 -0
  86. package/dist/builtin/workflows/src/shared/authoring-contract-ui.d.ts +23 -1
  87. package/dist/builtin/workflows/src/shared/authoring-contract-ui.ts +30 -1
  88. package/dist/builtin/workflows/src/shared/store-public-types.ts +6 -2
  89. package/dist/builtin/workflows/src/shared/store-run-methods.ts +12 -6
  90. package/dist/builtin/workflows/src/shared/types.ts +55 -0
  91. package/dist/builtin/workflows/src/tui/graph-view-constants.ts +1 -1
  92. package/dist/builtin/workflows/src/tui/graph-view-graph-render.ts +41 -0
  93. package/dist/builtin/workflows/src/tui/graph-view-input.ts +82 -24
  94. package/dist/builtin/workflows/src/tui/graph-view-render.ts +7 -0
  95. package/dist/builtin/workflows/src/tui/graph-view-state.ts +22 -2
  96. package/dist/builtin/workflows/src/tui/graph-view-types.ts +4 -5
  97. package/dist/builtin/workflows/src/tui/overlay-adapter.ts +9 -11
  98. package/dist/builtin/workflows/src/tui/stage-chat-view-footer-status.ts +9 -3
  99. package/dist/builtin/workflows/src/tui/stage-chat-view-input.ts +11 -2
  100. package/dist/builtin/workflows/src/tui/stage-chat-view-live-events.ts +35 -0
  101. package/dist/builtin/workflows/src/tui/stage-chat-view-state.ts +51 -17
  102. package/dist/builtin/workflows/src/tui/stage-chat-view-status.ts +36 -0
  103. package/dist/builtin/workflows/src/tui/stage-chat-view-types.ts +5 -1
  104. package/dist/builtin/workflows/src/tui/stage-chat-view.ts +3 -1
  105. package/dist/builtin/workflows/src/tui/status-list.ts +14 -2
  106. package/dist/builtin/workflows/src/tui/widget.ts +23 -8
  107. package/dist/builtin/workflows/src/tui/workflow-attach-pane-types.ts +5 -4
  108. package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +8 -8
  109. package/dist/builtin/workflows/src/tui/workflow-resume-selector.ts +151 -0
  110. package/dist/core/extensions/loader-virtual-modules.d.ts.map +1 -1
  111. package/dist/core/extensions/loader-virtual-modules.js +47 -30
  112. package/dist/core/extensions/loader-virtual-modules.js.map +1 -1
  113. package/dist/core/messages.d.ts +1 -0
  114. package/dist/core/messages.d.ts.map +1 -1
  115. package/dist/core/messages.js +46 -1
  116. package/dist/core/messages.js.map +1 -1
  117. package/dist/core/sdk.d.ts.map +1 -1
  118. package/dist/core/sdk.js +12 -0
  119. package/dist/core/sdk.js.map +1 -1
  120. package/dist/core/session-manager-core.d.ts +15 -7
  121. package/dist/core/session-manager-core.d.ts.map +1 -1
  122. package/dist/core/session-manager-core.js +20 -9
  123. package/dist/core/session-manager-core.js.map +1 -1
  124. package/dist/core/session-manager-entries.d.ts +2 -2
  125. package/dist/core/session-manager-entries.d.ts.map +1 -1
  126. package/dist/core/session-manager-entries.js +9 -3
  127. package/dist/core/session-manager-entries.js.map +1 -1
  128. package/dist/core/session-manager-history.d.ts.map +1 -1
  129. package/dist/core/session-manager-history.js +2 -1
  130. package/dist/core/session-manager-history.js.map +1 -1
  131. package/dist/core/session-manager-list.d.ts +3 -3
  132. package/dist/core/session-manager-list.d.ts.map +1 -1
  133. package/dist/core/session-manager-list.js +27 -8
  134. package/dist/core/session-manager-list.js.map +1 -1
  135. package/dist/core/session-manager-storage.d.ts +3 -1
  136. package/dist/core/session-manager-storage.d.ts.map +1 -1
  137. package/dist/core/session-manager-storage.js +55 -12
  138. package/dist/core/session-manager-storage.js.map +1 -1
  139. package/dist/core/session-manager-tool-dependencies.d.ts +10 -0
  140. package/dist/core/session-manager-tool-dependencies.d.ts.map +1 -0
  141. package/dist/core/session-manager-tool-dependencies.js +133 -0
  142. package/dist/core/session-manager-tool-dependencies.js.map +1 -0
  143. package/dist/core/session-manager-types.d.ts +22 -0
  144. package/dist/core/session-manager-types.d.ts.map +1 -1
  145. package/dist/core/session-manager-types.js.map +1 -1
  146. package/dist/core/session-manager.d.ts +2 -2
  147. package/dist/core/session-manager.d.ts.map +1 -1
  148. package/dist/core/session-manager.js +1 -1
  149. package/dist/core/session-manager.js.map +1 -1
  150. package/dist/modes/interactive/components/chat-session-host-runtime.d.ts +1 -0
  151. package/dist/modes/interactive/components/chat-session-host-runtime.d.ts.map +1 -1
  152. package/dist/modes/interactive/components/chat-session-host-runtime.js +12 -0
  153. package/dist/modes/interactive/components/chat-session-host-runtime.js.map +1 -1
  154. package/dist/modes/interactive/components/chat-session-host-terminal-cleanup.d.ts +4 -0
  155. package/dist/modes/interactive/components/chat-session-host-terminal-cleanup.d.ts.map +1 -0
  156. package/dist/modes/interactive/components/chat-session-host-terminal-cleanup.js +131 -0
  157. package/dist/modes/interactive/components/chat-session-host-terminal-cleanup.js.map +1 -0
  158. package/dist/modes/interactive/components/chat-session-host.d.ts +2 -0
  159. package/dist/modes/interactive/components/chat-session-host.d.ts.map +1 -1
  160. package/dist/modes/interactive/components/chat-session-host.js +7 -1
  161. package/dist/modes/interactive/components/chat-session-host.js.map +1 -1
  162. package/dist/modes/interactive/components/chat-transcript.d.ts.map +1 -1
  163. package/dist/modes/interactive/components/chat-transcript.js +15 -4
  164. package/dist/modes/interactive/components/chat-transcript.js.map +1 -1
  165. package/dist/modes/interactive/components/tool-execution.d.ts +3 -0
  166. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  167. package/dist/modes/interactive/components/tool-execution.js +26 -0
  168. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  169. package/docs/compaction.md +2 -0
  170. package/docs/models.md +1 -1
  171. package/docs/providers.md +2 -1
  172. package/docs/session-format.md +6 -0
  173. package/docs/sessions.md +6 -0
  174. package/docs/workflows.md +105 -3
  175. package/package.json +4 -3
@@ -0,0 +1,421 @@
1
+ /** DBOS-backed durable backend adapter, loaded only when configured. */
2
+
3
+ import type { DurableCheckpoint, DurableCheckpointEntry, DurableWorkflowHandle, DurableWorkflowStatus, ResumableWorkflowEntry } from "./types.js";
4
+ import type { WorkflowSerializableValue } from "../shared/types.js";
5
+ import type { WorkflowSerializableObject as DurableInputs } from "./types.js";
6
+ import { InMemoryDurableBackend, type DurableWorkflowBackend, type WorkflowRegistrationInput } from "./backend.js";
7
+ import { encodeCheckpoint, decodeToCheckpoint } from "./dbos-envelope.js";
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // SDK abstraction
11
+ // ---------------------------------------------------------------------------
12
+
13
+ /**
14
+ * Abstraction over the real `@dbos-inc/dbos-sdk` so the adapter is testable
15
+ * without Postgres. The real factory (`createRealDbosHandle`) wraps the SDK;
16
+ * tests supply a mock.
17
+ */
18
+ export interface DbosSdkHandle {
19
+ readonly launch: () => Promise<void>;
20
+ readonly shutdown: () => Promise<void>;
21
+ readonly startWorkflow: (workflowId: string, name: string, inputs: Readonly<Record<string, WorkflowSerializableValue>>) => Promise<void>;
22
+ readonly retrieveWorkflow: (workflowId: string) => Promise<DbosWorkflowInfo | undefined>;
23
+ readonly cancelWorkflow: (workflowId: string) => Promise<void>;
24
+ readonly resumeWorkflow: (workflowId: string) => Promise<void>;
25
+ /** List all workflows (any status) with loaded inputs. */
26
+ readonly listAllWorkflows: () => Promise<readonly DbosWorkflowInfo[]>;
27
+ /** List all completed checkpoint step-records for a workflow. */
28
+ readonly listStepRecords: (workflowId: string) => Promise<readonly DbosStepRecord[]>;
29
+ /** Record a checkpoint step output (envelope) to DBOS. */
30
+ readonly recordStepOutput: (workflowId: string, stepName: string, output: WorkflowSerializableValue) => Promise<void>;
31
+ }
32
+
33
+ export interface DbosWorkflowInfo {
34
+ readonly workflowId: string;
35
+ readonly name: string;
36
+ readonly status: string;
37
+ readonly createdAt: number;
38
+ readonly inputs?: DurableInputs;
39
+ }
40
+
41
+ /** A completed checkpoint stored in DBOS, returned by `listStepRecords`. */
42
+ export interface DbosStepRecord {
43
+ readonly stepName: string;
44
+ readonly output: WorkflowSerializableValue;
45
+ readonly completedAt?: number;
46
+ }
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // Real SDK handle factory (lazy import, no top-level dependency)
50
+ // ---------------------------------------------------------------------------
51
+
52
+ interface DbosWorkflowHandle {
53
+ readonly workflowID?: string;
54
+ getStatus(): Promise<DbosStatus | null>;
55
+ getResult(): Promise<WorkflowSerializableValue>;
56
+ }
57
+
58
+ interface DbosStatus {
59
+ readonly workflowID?: string;
60
+ readonly workflowId?: string;
61
+ readonly workflowName?: string;
62
+ readonly name?: string;
63
+ readonly status?: string;
64
+ readonly createdAt?: number;
65
+ readonly input?: readonly WorkflowSerializableValue[];
66
+ }
67
+
68
+ interface DbosStatic {
69
+ setConfig(config: Record<string, WorkflowSerializableValue>): void;
70
+ launch(): Promise<void>;
71
+ shutdown(): Promise<void>;
72
+ registerWorkflow<Args extends readonly WorkflowSerializableValue[]>(
73
+ fn: (...args: Args) => Promise<WorkflowSerializableValue>,
74
+ config?: { readonly name?: string },
75
+ ): (...args: Args) => Promise<WorkflowSerializableValue>;
76
+ startWorkflow<Args extends readonly WorkflowSerializableValue[]>(
77
+ target: (...args: Args) => Promise<WorkflowSerializableValue>,
78
+ params?: { readonly workflowID?: string },
79
+ ): (...args: Args) => Promise<DbosWorkflowHandle>;
80
+ retrieveWorkflow(workflowId: string): DbosWorkflowHandle;
81
+ resumeWorkflow(workflowId: string): Promise<DbosWorkflowHandle>;
82
+ cancelWorkflow(workflowId: string, options?: { readonly cancelChildren?: boolean }): Promise<void>;
83
+ listWorkflows(input: Record<string, WorkflowSerializableValue>): Promise<readonly DbosStatus[]>;
84
+ }
85
+
86
+ export function isDbosConfigured(): boolean {
87
+ const url = process.env.DBOS_SYSTEM_DATABASE_URL;
88
+ return typeof url === "string" && url.length > 0;
89
+ }
90
+
91
+ export async function createDbosDurableBackend(config?: { readonly systemDatabaseUrl?: string }): Promise<DurableWorkflowBackend> {
92
+ const sdk = await importDbosSdk();
93
+ const url = config?.systemDatabaseUrl ?? process.env.DBOS_SYSTEM_DATABASE_URL;
94
+ if (url === undefined || url.length === 0) throw new Error("DBOS_SYSTEM_DATABASE_URL is required for DBOS workflow durability.");
95
+ sdk.setConfig({ name: "atomic-workflows", systemDatabaseUrl: url, runAdminServer: false });
96
+ const mainWorkflow = sdk.registerWorkflow(async (_name: string, inputs: DurableInputs) => inputs, { name: "atomicWorkflowHandle" });
97
+ const checkpointWorkflow = sdk.registerWorkflow(async (_workflowId: string, _stepName: string, output: WorkflowSerializableValue) => output, { name: "atomicWorkflowCheckpoint" });
98
+ await sdk.launch();
99
+ return new DbosDurableBackend(createRealDbosHandle(sdk, mainWorkflow, checkpointWorkflow));
100
+ }
101
+
102
+ async function importDbosSdk(): Promise<DbosStatic> {
103
+ const spec = "@dbos-inc/dbos-sdk";
104
+ try {
105
+ const mod = await import(spec);
106
+ const dbos = (mod as { readonly DBOS?: DbosStatic }).DBOS;
107
+ if (dbos === undefined) throw new Error("@dbos-inc/dbos-sdk did not export DBOS");
108
+ return dbos;
109
+ } catch (err) {
110
+ const msg = err instanceof Error ? err.message : String(err);
111
+ throw new Error(`DBOS workflow durability is configured but @dbos-inc/dbos-sdk could not be loaded: ${msg}`);
112
+ }
113
+ }
114
+
115
+ function createRealDbosHandle(
116
+ dbos: DbosStatic,
117
+ mainWorkflow: (name: string, inputs: Record<string, WorkflowSerializableValue>) => Promise<WorkflowSerializableValue>,
118
+ checkpointWorkflow: (workflowId: string, stepName: string, output: WorkflowSerializableValue) => Promise<WorkflowSerializableValue>,
119
+ ): DbosSdkHandle {
120
+ const checkpointId = (workflowId: string, stepName: string): string => `${workflowId}:checkpoint:${stepName}`;
121
+ return {
122
+ launch: () => dbos.launch(),
123
+ shutdown: () => dbos.shutdown(),
124
+ async startWorkflow(workflowId, name, inputs) {
125
+ try {
126
+ await dbos.startWorkflow(mainWorkflow, { workflowID: workflowId })(name, { ...inputs });
127
+ } catch (err) {
128
+ if (!isDbosDuplicateWorkflowError(err)) throw err;
129
+ }
130
+ },
131
+ async retrieveWorkflow(workflowId) {
132
+ const statuses = await dbos.listWorkflows({ workflowIDs: [workflowId], loadInput: true, limit: 1 });
133
+ const status = statuses[0];
134
+ if (status === undefined) return undefined;
135
+ return statusToInfo(status, workflowId);
136
+ },
137
+ async cancelWorkflow(workflowId) { await dbos.cancelWorkflow(workflowId, { cancelChildren: true }); },
138
+ async resumeWorkflow(workflowId) { await dbos.resumeWorkflow(workflowId); },
139
+ async listAllWorkflows() {
140
+ const statuses = await dbos.listWorkflows({ workflowName: "atomicWorkflowHandle", loadInput: true, sortDesc: true });
141
+ return statuses.map((s) => statusToInfo(s, s.workflowID ?? s.workflowId ?? ""));
142
+ },
143
+ async listStepRecords(workflowId) {
144
+ const prefix = `${workflowId}:checkpoint:`;
145
+ const statuses = await dbos.listWorkflows({ workflow_id_prefix: prefix, loadOutput: true, sortDesc: false });
146
+ const records: DbosStepRecord[] = [];
147
+ for (const s of statuses) {
148
+ if (s.status !== "SUCCESS") continue;
149
+ const wid = s.workflowID ?? s.workflowId ?? "";
150
+ const stepName = wid.slice(prefix.length);
151
+ if (stepName.length === 0) continue;
152
+ const handle = dbos.retrieveWorkflow(wid);
153
+ const output = await handle.getResult();
154
+ records.push({ stepName, output, completedAt: s.createdAt });
155
+ }
156
+ return records;
157
+ },
158
+ async recordStepOutput(workflowId, stepName, output) {
159
+ await dbos.startWorkflow(checkpointWorkflow, { workflowID: checkpointId(workflowId, stepName) })(workflowId, stepName, output);
160
+ },
161
+ };
162
+ }
163
+
164
+ function isDbosDuplicateWorkflowError(err: unknown): boolean {
165
+ const msg = err instanceof Error ? err.message : String(err);
166
+ return /duplicate|conflict|already/i.test(msg);
167
+ }
168
+
169
+ function statusToInfo(status: DbosStatus, fallbackId: string): DbosWorkflowInfo {
170
+ const info: DbosWorkflowInfo = {
171
+ workflowId: status.workflowID ?? status.workflowId ?? fallbackId,
172
+ name: status.workflowName ?? status.name ?? "atomicWorkflowHandle",
173
+ status: status.status ?? "PENDING",
174
+ createdAt: status.createdAt ?? Date.now(),
175
+ };
176
+ // Inputs were passed as (name, inputs) to the main workflow; extract the
177
+ // inputs object from the second positional argument.
178
+ if (status.input !== undefined && status.input.length >= 2) {
179
+ const inputs = status.input[1];
180
+ if (typeof inputs === "object" && inputs !== null && !Array.isArray(inputs)) {
181
+ return { ...info, inputs: inputs as DurableInputs };
182
+ }
183
+ }
184
+ return info;
185
+ }
186
+
187
+ // ---------------------------------------------------------------------------
188
+ // Backend adapter
189
+ // ---------------------------------------------------------------------------
190
+
191
+ /**
192
+ * DBOS-backed durable backend. Wraps a {@link DbosSdkHandle} to implement the
193
+ * {@link DurableWorkflowBackend} interface. Writes are serialized to DBOS
194
+ * with an in-memory mirror for synchronous queries. A fresh process hydrates
195
+ * its mirror from DBOS via {@link hydrateWorkflow} / {@link hydrateResumableWorkflows}
196
+ * before resume/replay reads.
197
+ *
198
+ * cross-ref: issue #1498 — DBOS read-side hydration.
199
+ */
200
+ export class DbosDurableBackend implements DurableWorkflowBackend {
201
+ public readonly persistent = true;
202
+ private readonly mem = new InMemoryDurableBackend();
203
+ private readonly sdk: DbosSdkHandle;
204
+ private readonly hydrated = new Set<string>();
205
+ private writeQueue: Promise<void> = Promise.resolve();
206
+ private writeErrors: Error[] = [];
207
+
208
+ constructor(sdk: DbosSdkHandle) {
209
+ this.sdk = sdk;
210
+ }
211
+
212
+ registerWorkflow(handle: WorkflowRegistrationInput): void {
213
+ this.mem.registerWorkflow(handle);
214
+ this.enqueueWrite(async () => {
215
+ await this.sdk.startWorkflow(handle.workflowId, handle.name, handle.inputs);
216
+ await this.writeMetadata(handle.workflowId);
217
+ });
218
+ }
219
+
220
+ recordCheckpoint(checkpoint: DurableCheckpoint): void {
221
+ this.mem.recordCheckpoint(checkpoint);
222
+ this.enqueueWrite(() => this.persistCheckpoint(checkpoint));
223
+ }
224
+
225
+ async recordCheckpointAsync(checkpoint: DurableCheckpoint): Promise<void> {
226
+ await this.enqueueWrite(async () => {
227
+ await this.persistCheckpointRecord(checkpoint);
228
+ this.mem.recordCheckpoint(checkpoint);
229
+ await this.writeMetadata(checkpoint.workflowId);
230
+ });
231
+ }
232
+
233
+ private async persistCheckpoint(checkpoint: DurableCheckpoint): Promise<void> {
234
+ await this.persistCheckpointRecord(checkpoint);
235
+ await this.writeMetadata(checkpoint.workflowId);
236
+ }
237
+
238
+ private async persistCheckpointRecord(checkpoint: DurableCheckpoint): Promise<void> {
239
+ await this.sdk.recordStepOutput(checkpoint.workflowId, checkpoint.checkpointId, encodeCheckpoint(checkpoint));
240
+ }
241
+
242
+ getToolOutput(workflowId: string, argsHash: string): WorkflowSerializableValue | undefined { return this.mem.getToolOutput(workflowId, argsHash); }
243
+ getUiResponse(workflowId: string, promptHash: string): WorkflowSerializableValue | undefined { return this.mem.getUiResponse(workflowId, promptHash); }
244
+ getStageOutput(workflowId: string, replayKey: string): WorkflowSerializableValue | undefined { return this.mem.getStageOutput(workflowId, replayKey); }
245
+ getStageSession(workflowId: string, replayKey: string) { return this.mem.getStageSession(workflowId, replayKey); }
246
+ listCheckpoints(workflowId: string): readonly DurableCheckpoint[] { return this.mem.listCheckpoints(workflowId); }
247
+ getWorkflow(workflowId: string): DurableWorkflowHandle | undefined { return this.mem.getWorkflow(workflowId); }
248
+
249
+ setWorkflowStatus(workflowId: string, status: DurableWorkflowStatus, pendingPrompts?: number, resumable?: boolean): void {
250
+ this.mem.setWorkflowStatus(workflowId, status, pendingPrompts, resumable);
251
+ this.enqueueWrite(async () => {
252
+ if (status === "cancelled") await this.sdk.cancelWorkflow(workflowId);
253
+ else if (status === "running") await this.sdk.resumeWorkflow(workflowId);
254
+ await this.writeMetadata(workflowId);
255
+ });
256
+ }
257
+
258
+ listResumableWorkflows(): readonly ResumableWorkflowEntry[] { return this.mem.listResumableWorkflows(); }
259
+ toCacheEntry(workflowId: string) { return this.mem.toCacheEntry(workflowId); }
260
+ reset(): void { this.mem.reset(); this.hydrated.clear(); this.writeQueue = Promise.resolve(); this.writeErrors = []; }
261
+ async flush(): Promise<void> {
262
+ await this.writeQueue;
263
+ if (this.writeErrors.length === 0) return;
264
+ const [first] = this.writeErrors;
265
+ this.writeErrors = [];
266
+ throw first;
267
+ }
268
+
269
+ /**
270
+ * Hydrate a single workflow's handle and checkpoints from DBOS into the
271
+ * in-memory mirror. Idempotent: skips workflows already hydrated with
272
+ * checkpoints. Safe to call before synchronous replay reads.
273
+ */
274
+ async hydrateWorkflow(workflowId: string): Promise<void> {
275
+ if (this.hydrated.has(workflowId) && this.mem.getWorkflow(workflowId) !== undefined) return;
276
+ const info = await this.sdk.retrieveWorkflow(workflowId);
277
+ if (info !== undefined && this.mem.getWorkflow(workflowId) === undefined) {
278
+ this.mem.registerWorkflow({
279
+ workflowId: info.workflowId,
280
+ name: info.name,
281
+ inputs: info.inputs ?? {},
282
+ createdAt: info.createdAt,
283
+ status: dbosStatusToDurable(info.status),
284
+ });
285
+ }
286
+ const stepRecords = await this.sdk.listStepRecords(workflowId);
287
+ this.applyMetadata(workflowId, stepRecords);
288
+ for (const rec of stepRecords) {
289
+ if (isMetadataStep(rec.stepName)) continue;
290
+ const cp = decodeToCheckpoint(workflowId, rec.stepName, rec.output);
291
+ if (cp !== undefined) this.mem.recordCheckpoint(cp);
292
+ }
293
+ this.hydrated.add(workflowId);
294
+ }
295
+
296
+ /**
297
+ * Hydrate all resumable workflows from DBOS into the in-memory mirror.
298
+ * Called by the resume/list path before enumerating resumable entries so
299
+ * a fresh process discovers workflows persisted by a prior session.
300
+ */
301
+ async hydrateResumableWorkflows(): Promise<void> {
302
+ const all = await this.sdk.listAllWorkflows();
303
+ for (const info of all) {
304
+ if (this.mem.getWorkflow(info.workflowId) === undefined) {
305
+ this.mem.registerWorkflow({
306
+ workflowId: info.workflowId,
307
+ name: info.name,
308
+ inputs: info.inputs ?? {},
309
+ createdAt: info.createdAt,
310
+ status: dbosStatusToDurable(info.status),
311
+ });
312
+ }
313
+ if (!this.hydrated.has(info.workflowId)) {
314
+ const stepRecords = await this.sdk.listStepRecords(info.workflowId);
315
+ this.applyMetadata(info.workflowId, stepRecords);
316
+ for (const rec of stepRecords) {
317
+ if (isMetadataStep(rec.stepName)) continue;
318
+ const cp = decodeToCheckpoint(info.workflowId, rec.stepName, rec.output);
319
+ if (cp !== undefined) this.mem.recordCheckpoint(cp);
320
+ }
321
+ this.hydrated.add(info.workflowId);
322
+ }
323
+ }
324
+ }
325
+
326
+ private enqueueWrite(fn: () => Promise<void>): Promise<void> {
327
+ const next = this.writeQueue.then(fn, fn);
328
+ this.writeQueue = next.catch((err) => {
329
+ const error = err instanceof Error ? err : new Error(String(err));
330
+ this.writeErrors.push(error);
331
+ console.warn(`atomic-workflows: DBOS durable write failed: ${error.message}`);
332
+ });
333
+ return next;
334
+ }
335
+
336
+ private async writeMetadata(workflowId: string): Promise<void> {
337
+ const entry = this.mem.toCacheEntry(workflowId);
338
+ if (entry === undefined) return;
339
+ await this.sdk.recordStepOutput(workflowId, metadataStepName(entry.ts), encodeMetadata(entry));
340
+ }
341
+
342
+ private applyMetadata(workflowId: string, records: readonly DbosStepRecord[]): void {
343
+ const entries = records
344
+ .filter((r) => isMetadataStep(r.stepName))
345
+ .map((r) => decodeMetadata(r.output))
346
+ .filter((entry): entry is DurableCheckpointEntry => entry !== undefined)
347
+ .sort((a, b) => a.ts - b.ts);
348
+ const entry = entries.at(-1);
349
+ if (entry === undefined) return;
350
+ this.mem.registerWorkflow({
351
+ workflowId,
352
+ name: entry.name,
353
+ inputs: entry.inputs,
354
+ createdAt: entry.ts,
355
+ updatedAt: entry.ts,
356
+ status: entry.status,
357
+ completedCheckpoints: entry.completedCheckpoints,
358
+ pendingPrompts: entry.pendingPrompts,
359
+ ...(entry.label !== undefined ? { label: entry.label } : {}),
360
+ ...(entry.rootWorkflowId !== undefined ? { rootWorkflowId: entry.rootWorkflowId } : {}),
361
+ ...(entry.resumable !== undefined ? { resumable: entry.resumable } : {}),
362
+ });
363
+ }
364
+ }
365
+
366
+ const METADATA_STEP_PREFIX = "__atomic_metadata";
367
+ const METADATA_VERSION = 1;
368
+
369
+ function metadataStepName(ts: number): string {
370
+ return `${METADATA_STEP_PREFIX}:${ts}:${crypto.randomUUID()}`;
371
+ }
372
+
373
+ function isMetadataStep(stepName: string): boolean {
374
+ return stepName === METADATA_STEP_PREFIX || stepName.startsWith(`${METADATA_STEP_PREFIX}:`);
375
+ }
376
+
377
+ interface DbosMetadataEnvelope {
378
+ readonly __atomicDurableMetadata: true;
379
+ readonly version: 1;
380
+ readonly entry: DurableCheckpointEntry;
381
+ }
382
+
383
+ function encodeMetadata(entry: DurableCheckpointEntry): WorkflowSerializableValue {
384
+ return {
385
+ __atomicDurableMetadata: true,
386
+ version: METADATA_VERSION,
387
+ entry: {
388
+ type: entry.type,
389
+ workflowId: entry.workflowId,
390
+ name: entry.name,
391
+ inputs: entry.inputs,
392
+ status: entry.status,
393
+ completedCheckpoints: entry.completedCheckpoints,
394
+ pendingPrompts: entry.pendingPrompts,
395
+ ...(entry.label !== undefined ? { label: entry.label } : {}),
396
+ ...(entry.rootWorkflowId !== undefined ? { rootWorkflowId: entry.rootWorkflowId } : {}),
397
+ ...(entry.resumable !== undefined ? { resumable: entry.resumable } : {}),
398
+ ts: entry.ts,
399
+ },
400
+ };
401
+ }
402
+
403
+ function decodeMetadata(value: WorkflowSerializableValue): DurableCheckpointEntry | undefined {
404
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
405
+ const raw = value as Partial<DbosMetadataEnvelope>;
406
+ if (raw.__atomicDurableMetadata !== true || raw.version !== METADATA_VERSION) return undefined;
407
+ return raw.entry;
408
+ }
409
+
410
+ function dbosStatusToDurable(status: string): DurableWorkflowStatus {
411
+ switch (status) {
412
+ case "SUCCESS": return "completed";
413
+ case "ERROR": return "failed";
414
+ case "CANCELLED": return "cancelled";
415
+ case "PENDING":
416
+ case "ENQUEUED":
417
+ case "DELAYED":
418
+ return "running";
419
+ default: return "running";
420
+ }
421
+ }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * DBOS checkpoint envelope — a structured payload stored as DBOS step output so
3
+ * a fresh process can reconstruct full durable checkpoints from DBOS alone.
4
+ *
5
+ * Without the envelope, `recordStepOutput` would only persist the raw output
6
+ * value, losing checkpoint metadata (kind, checkpointId, argsHash, promptHash,
7
+ * replayKey, etc.). That makes cross-process DBOS hydration impossible because
8
+ * the synchronous replay reads (`getToolOutput`, `getUiResponse`,
9
+ * `getStageOutput`) cannot reconstruct their lookup keys.
10
+ *
11
+ * The envelope is forward-compatible: old/simple payloads (plain values without
12
+ * the envelope marker) are treated as generic stage outputs during hydration.
13
+ *
14
+ * cross-ref: issue #1498 — DBOS read-side hydration.
15
+ */
16
+
17
+ import type { WorkflowSerializableObject, WorkflowSerializableValue } from "../shared/types.js";
18
+ import type {
19
+ DurableCheckpoint,
20
+ DurableCheckpointKind,
21
+ DurableStageCheckpoint,
22
+ DurableToolCheckpoint,
23
+ DurableUiCheckpoint,
24
+ UiPromptKind,
25
+ } from "./types.js";
26
+
27
+ /** Envelope schema version. */
28
+ export const DBOS_ENVELOPE_VERSION = 1;
29
+
30
+ /** Marker key present on every envelope payload. */
31
+ const ENVELOPE_MARKER = "__dbos_checkpoint__";
32
+
33
+ /**
34
+ * Structured DBOS step-output payload containing all checkpoint metadata.
35
+ * Stored as the output of a DBOS checkpoint workflow so it round-trips through
36
+ * `getResult()` / `listWorkflows` on hydration.
37
+ */
38
+ export interface DbosCheckpointEnvelope extends WorkflowSerializableObject {
39
+ readonly __dbos_checkpoint__: typeof ENVELOPE_MARKER;
40
+ readonly v: typeof DBOS_ENVELOPE_VERSION;
41
+ readonly kind: DurableCheckpointKind;
42
+ readonly checkpointId: string;
43
+ readonly name?: string;
44
+ readonly argsHash?: string;
45
+ readonly promptKind?: UiPromptKind;
46
+ readonly message?: string;
47
+ readonly promptHash?: string;
48
+ readonly replayKey?: string;
49
+ readonly output?: WorkflowSerializableValue;
50
+ readonly hasOutput?: boolean;
51
+ readonly sessionId?: string;
52
+ readonly sessionFile?: string;
53
+ readonly completedAt: number;
54
+ }
55
+
56
+ /**
57
+ * Encode a durable checkpoint into a DBOS step-output envelope.
58
+ */
59
+ export function encodeCheckpoint(cp: DurableCheckpoint): DbosCheckpointEnvelope {
60
+ const output = checkpointOutputValue(cp);
61
+ const base: DbosCheckpointEnvelope = {
62
+ __dbos_checkpoint__: ENVELOPE_MARKER,
63
+ v: DBOS_ENVELOPE_VERSION,
64
+ kind: cp.kind,
65
+ checkpointId: cp.checkpointId,
66
+ ...(output !== undefined ? { output } : {}),
67
+ hasOutput: output !== undefined,
68
+ completedAt: cp.completedAt,
69
+ };
70
+ if (cp.kind === "tool") {
71
+ const t = cp as DurableToolCheckpoint;
72
+ return { ...base, name: t.name, argsHash: t.argsHash };
73
+ }
74
+ if (cp.kind === "ui") {
75
+ const u = cp as DurableUiCheckpoint;
76
+ return { ...base, promptKind: u.promptKind, message: u.message, promptHash: u.promptHash };
77
+ }
78
+ const s = cp as DurableStageCheckpoint;
79
+ return {
80
+ ...base,
81
+ name: s.name,
82
+ replayKey: s.replayKey,
83
+ ...(s.sessionId !== undefined ? { sessionId: s.sessionId } : {}),
84
+ ...(s.sessionFile !== undefined ? { sessionFile: s.sessionFile } : {}),
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Type guard: is this value a checkpoint envelope?
90
+ */
91
+ export function isCheckpointEnvelope(value: WorkflowSerializableValue | undefined): value is DbosCheckpointEnvelope {
92
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
93
+ const obj = value as Record<string, unknown>;
94
+ return obj[ENVELOPE_MARKER] === ENVELOPE_MARKER;
95
+ }
96
+
97
+ /**
98
+ * Decode a DBOS step output into a durable checkpoint.
99
+ *
100
+ * - Envelope payloads reconstruct the full checkpoint with original metadata.
101
+ * - Non-envelope payloads (legacy/simple) produce a generic stage checkpoint
102
+ * so the output is still discoverable during replay.
103
+ *
104
+ * Returns `undefined` if the payload cannot be interpreted.
105
+ */
106
+ export function decodeToCheckpoint(
107
+ workflowId: string,
108
+ stepName: string,
109
+ value: WorkflowSerializableValue,
110
+ ): DurableCheckpoint | undefined {
111
+ if (isCheckpointEnvelope(value)) return decodeEnvelope(workflowId, value);
112
+ if (value === undefined) return undefined;
113
+ return decodeLegacy(workflowId, stepName, value);
114
+ }
115
+
116
+ function decodeEnvelope(workflowId: string, env: DbosCheckpointEnvelope): DurableCheckpoint | undefined {
117
+ const common = { workflowId, checkpointId: env.checkpointId, completedAt: env.completedAt };
118
+ if (env.kind === "tool") {
119
+ if (env.argsHash === undefined || env.output === undefined) return undefined;
120
+ return {
121
+ kind: "tool",
122
+ ...common,
123
+ name: env.name ?? "tool",
124
+ argsHash: env.argsHash,
125
+ output: env.output,
126
+ } as DurableToolCheckpoint;
127
+ }
128
+ if (env.kind === "ui") {
129
+ if (env.promptHash === undefined || env.promptKind === undefined || env.output === undefined) return undefined;
130
+ return {
131
+ kind: "ui",
132
+ ...common,
133
+ promptKind: env.promptKind,
134
+ message: env.message ?? "",
135
+ promptHash: env.promptHash,
136
+ response: env.output,
137
+ } as DurableUiCheckpoint;
138
+ }
139
+ return {
140
+ kind: "stage",
141
+ ...common,
142
+ name: env.name ?? "stage",
143
+ replayKey: env.replayKey ?? env.checkpointId,
144
+ ...(env.hasOutput !== false && env.output !== undefined ? { output: env.output } : {}),
145
+ ...(env.sessionId !== undefined ? { sessionId: env.sessionId } : {}),
146
+ ...(env.sessionFile !== undefined ? { sessionFile: env.sessionFile } : {}),
147
+ } as DurableStageCheckpoint;
148
+ }
149
+
150
+ function decodeLegacy(
151
+ workflowId: string,
152
+ stepName: string,
153
+ output: WorkflowSerializableValue,
154
+ ): DurableStageCheckpoint {
155
+ return {
156
+ kind: "stage",
157
+ workflowId,
158
+ checkpointId: `stage:${stepName}`,
159
+ name: stepName,
160
+ replayKey: stepName,
161
+ output,
162
+ completedAt: Date.now(),
163
+ };
164
+ }
165
+
166
+ function checkpointOutputValue(cp: DurableCheckpoint): WorkflowSerializableValue | undefined {
167
+ if (cp.kind === "tool") return (cp as DurableToolCheckpoint).output;
168
+ if (cp.kind === "ui") return (cp as DurableUiCheckpoint).response;
169
+ const stage = cp as DurableStageCheckpoint;
170
+ return "output" in stage ? stage.output : undefined;
171
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Durable backend factory.
3
+ *
4
+ * Resolves which backend to use based on configuration:
5
+ * - Explicit override (for testing)
6
+ * - DBOS/Postgres when `DBOS_SYSTEM_DATABASE_URL` is set and durability is not opted out
7
+ * - File-backed fallback (default; zero infrastructure)
8
+ *
9
+ * cross-ref: issue #1498
10
+ */
11
+
12
+ import type { DurableWorkflowBackend } from "./backend.js";
13
+ import { InMemoryDurableBackend } from "./backend.js";
14
+ import { FileDurableBackend, WorkflowFileDurableBackend, defaultDurableStateDir, durableStateFileFor } from "./file-backend.js";
15
+ import { createDbosDurableBackend } from "./dbos-backend.js";
16
+
17
+ let globalBackend: DurableWorkflowBackend | undefined;
18
+ let dbosInit: Promise<DurableWorkflowBackend | undefined> | undefined;
19
+
20
+ const DURABLE_OPT_OUT_ENV = "ATOMIC_WORKFLOW_DURABLE";
21
+
22
+ /**
23
+ * Get the singleton durable backend. Creates one lazily on first call.
24
+ * - If a backend was explicitly set via {@link setDurableBackend}, returns it.
25
+ * - If `DBOS_SYSTEM_DATABASE_URL` is configured and durability was not opted
26
+ * out, the extension runtime upgrades to a DBOS-backed backend on launch.
27
+ * - Otherwise returns the zero-infrastructure per-workflow file backend rooted
28
+ * under `~/.atomic/workflow-durable`, so cross-session resume is available by
29
+ * default without an opt-in environment variable. Set
30
+ * `ATOMIC_WORKFLOW_DURABLE=0` (or `false`/`off`/`memory`) to fail closed to
31
+ * process-local in-memory durability for sensitive environments.
32
+ */
33
+ export function getDurableBackend(): DurableWorkflowBackend {
34
+ if (globalBackend) return globalBackend;
35
+ if (isDurabilityOptedOut()) {
36
+ globalBackend = createInMemoryBackend();
37
+ return globalBackend;
38
+ }
39
+ // Always enable cross-session durability by default. DBOS initialization is
40
+ // async because the SDK is optional; the file backend is the durable baseline
41
+ // and remains the safe fallback if DBOS is unavailable.
42
+ globalBackend = createDefaultFileBackend();
43
+ return globalBackend;
44
+ }
45
+
46
+ /**
47
+ * Explicitly set the durable backend. Used by tests and by the extension
48
+ * runtime when it initializes DBOS.
49
+ */
50
+ export function setDurableBackend(backend: DurableWorkflowBackend | undefined): void {
51
+ globalBackend = backend;
52
+ }
53
+
54
+ /**
55
+ * Create a fresh in-memory backend (for tests).
56
+ */
57
+ export function createInMemoryBackend(): InMemoryDurableBackend {
58
+ return new InMemoryDurableBackend();
59
+ }
60
+
61
+ /** Initialize and install the DBOS backend when DBOS_SYSTEM_DATABASE_URL is set. */
62
+ export async function initializeDbosDurableBackendFromEnv(): Promise<DurableWorkflowBackend | undefined> {
63
+ if (isDurabilityOptedOut()) return undefined;
64
+ const dbosUrl = process.env.DBOS_SYSTEM_DATABASE_URL;
65
+ if (dbosUrl === undefined || dbosUrl.length === 0) return undefined;
66
+ dbosInit ??= createDbosDurableBackend({ systemDatabaseUrl: dbosUrl }).then((backend) => {
67
+ setDurableBackend(backend);
68
+ return backend;
69
+ });
70
+ return dbosInit;
71
+ }
72
+
73
+ /**
74
+ * Create the default durable backend. If no user home directory can be resolved,
75
+ * fail closed to the process-local in-memory backend instead of writing to /tmp.
76
+ */
77
+ export function createDefaultFileBackend(): DurableWorkflowBackend {
78
+ const dir = defaultDurableStateDir();
79
+ if (dir === undefined) return createInMemoryBackend();
80
+ return new WorkflowFileDurableBackend(dir);
81
+ }
82
+
83
+ /**
84
+ * Create a file-backed backend for a specific workflow id.
85
+ * Each workflow gets its own state file for fast load/save.
86
+ */
87
+ export function createWorkflowFileBackend(workflowId: string): DurableWorkflowBackend {
88
+ const dir = defaultDurableStateDir();
89
+ if (dir === undefined) return createInMemoryBackend();
90
+ return new FileDurableBackend(durableStateFileFor(dir, workflowId));
91
+ }
92
+
93
+ function isDurabilityOptedOut(): boolean {
94
+ const value = process.env[DURABLE_OPT_OUT_ENV]?.toLowerCase();
95
+ return value === "0" || value === "false" || value === "off" || value === "memory" || value === "in-memory";
96
+ }