@bastani/atomic 0.9.3-alpha.1 → 0.9.3-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.
- package/CHANGELOG.md +9 -0
- package/dist/builtin/cursor/CHANGELOG.md +15 -0
- package/dist/builtin/cursor/README.md +2 -1
- package/dist/builtin/cursor/package.json +2 -2
- package/dist/builtin/cursor/src/cursor-models-raw.json +2 -9
- package/dist/builtin/cursor/src/model-mapper.ts +14 -3
- package/dist/builtin/cursor/src/proto/protobuf-codec-base64.ts +22 -0
- package/dist/builtin/cursor/src/proto/protobuf-codec-request.ts +53 -13
- package/dist/builtin/cursor/src/proto/protobuf-codec-wire.ts +24 -7
- package/dist/builtin/cursor/src/proto/protobuf-codec.ts +3 -2
- package/dist/builtin/cursor/src/stream.ts +5 -11
- package/dist/builtin/cursor/src/transport-types.ts +3 -0
- package/dist/builtin/cursor/src/transport.ts +1 -0
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/CHANGELOG.md +9 -0
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/subagents/src/extension/fanout-child.ts +1 -0
- package/dist/builtin/subagents/src/extension/index.ts +6 -3
- package/dist/builtin/subagents/src/extension/schemas.ts +0 -5
- package/dist/builtin/subagents/src/runs/background/async-job-tracker.ts +1 -4
- package/dist/builtin/subagents/src/runs/foreground/subagent-executor-single.ts +15 -1
- package/dist/builtin/subagents/src/runs/foreground/subagent-executor.ts +35 -1
- package/dist/builtin/subagents/src/runs/shared/subagent-prompt-runtime.ts +4 -2
- package/dist/builtin/subagents/src/shared/types-async.ts +1 -0
- package/dist/builtin/subagents/src/slash/prompt-template-bridge.ts +27 -5
- package/dist/builtin/subagents/src/tui/render-layout.ts +27 -4
- package/dist/builtin/subagents/src/tui/render-result-animation.ts +22 -31
- package/dist/builtin/subagents/src/tui/render-result-compact.ts +6 -6
- package/dist/builtin/subagents/src/tui/render-result.ts +20 -19
- package/dist/builtin/subagents/src/tui/render-status-progress.ts +3 -3
- package/dist/builtin/subagents/src/tui/render-widget.ts +46 -7
- package/dist/builtin/subagents/src/tui/render.ts +2 -2
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +43 -0
- package/dist/builtin/workflows/README.md +1 -1
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/src/authoring.d.ts +1 -1
- package/dist/builtin/workflows/src/durable/backend.ts +343 -0
- package/dist/builtin/workflows/src/durable/child-primitive.ts +79 -0
- package/dist/builtin/workflows/src/durable/dbos-backend.ts +421 -0
- package/dist/builtin/workflows/src/durable/dbos-envelope.ts +171 -0
- package/dist/builtin/workflows/src/durable/factory.ts +96 -0
- package/dist/builtin/workflows/src/durable/file-backend.ts +433 -0
- package/dist/builtin/workflows/src/durable/index.ts +73 -0
- package/dist/builtin/workflows/src/durable/resume-catalog.ts +217 -0
- package/dist/builtin/workflows/src/durable/resume-runtime.ts +299 -0
- package/dist/builtin/workflows/src/durable/scoped-backend.ts +171 -0
- package/dist/builtin/workflows/src/durable/stage-primitive.ts +284 -0
- package/dist/builtin/workflows/src/durable/tool-primitive.ts +180 -0
- package/dist/builtin/workflows/src/durable/types.ts +168 -0
- package/dist/builtin/workflows/src/durable/ui-primitive.ts +96 -0
- package/dist/builtin/workflows/src/engine/options.ts +3 -0
- package/dist/builtin/workflows/src/engine/primitives/parallel.ts +2 -2
- package/dist/builtin/workflows/src/engine/primitives/task.ts +4 -4
- package/dist/builtin/workflows/src/engine/primitives/ui.ts +22 -8
- package/dist/builtin/workflows/src/engine/primitives/workflow.ts +8 -0
- package/dist/builtin/workflows/src/engine/run-durable-finalize.ts +69 -0
- package/dist/builtin/workflows/src/engine/run-durable-stage-session.ts +31 -0
- package/dist/builtin/workflows/src/engine/run.ts +148 -6
- package/dist/builtin/workflows/src/engine/runtime.ts +8 -2
- package/dist/builtin/workflows/src/extension/extension-factory.ts +6 -12
- package/dist/builtin/workflows/src/extension/extension-lifecycle.ts +5 -1
- package/dist/builtin/workflows/src/extension/extension-runtime-state.ts +3 -0
- package/dist/builtin/workflows/src/extension/runtime.ts +48 -9
- package/dist/builtin/workflows/src/extension/workflow-run-control-command.ts +143 -4
- package/dist/builtin/workflows/src/runs/background/quit.ts +61 -0
- package/dist/builtin/workflows/src/runs/background/status.ts +1 -0
- package/dist/builtin/workflows/src/runs/foreground/executor-direct-helpers.ts +5 -5
- package/dist/builtin/workflows/src/runs/foreground/executor-stage-call.ts +74 -33
- package/dist/builtin/workflows/src/runs/foreground/executor-stage-context.ts +20 -1
- package/dist/builtin/workflows/src/runs/foreground/executor-stage-factory.ts +8 -7
- package/dist/builtin/workflows/src/runs/foreground/executor-stage-replay.ts +1 -0
- package/dist/builtin/workflows/src/runs/foreground/executor-stage-types.ts +1 -1
- package/dist/builtin/workflows/src/runs/foreground/executor-types.ts +19 -2
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-context.ts +4 -0
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-controller.ts +10 -10
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-options.ts +5 -1
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-send-user-message.ts +25 -0
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-types.ts +3 -0
- package/dist/builtin/workflows/src/shared/authoring-contract-stage.d.ts +16 -0
- package/dist/builtin/workflows/src/shared/authoring-contract-stage.ts +20 -0
- package/dist/builtin/workflows/src/shared/authoring-contract-ui.d.ts +23 -1
- package/dist/builtin/workflows/src/shared/authoring-contract-ui.ts +30 -1
- package/dist/builtin/workflows/src/shared/store-public-types.ts +6 -2
- package/dist/builtin/workflows/src/shared/store-run-methods.ts +12 -6
- package/dist/builtin/workflows/src/shared/types.ts +55 -0
- package/dist/builtin/workflows/src/tui/graph-view-constants.ts +1 -1
- package/dist/builtin/workflows/src/tui/graph-view-graph-render.ts +41 -0
- package/dist/builtin/workflows/src/tui/graph-view-input.ts +82 -24
- package/dist/builtin/workflows/src/tui/graph-view-render.ts +7 -0
- package/dist/builtin/workflows/src/tui/graph-view-state.ts +22 -2
- package/dist/builtin/workflows/src/tui/graph-view-types.ts +4 -5
- package/dist/builtin/workflows/src/tui/overlay-adapter.ts +9 -11
- package/dist/builtin/workflows/src/tui/stage-chat-view-footer-status.ts +9 -3
- package/dist/builtin/workflows/src/tui/stage-chat-view-input.ts +11 -2
- package/dist/builtin/workflows/src/tui/stage-chat-view-live-events.ts +35 -0
- package/dist/builtin/workflows/src/tui/stage-chat-view-state.ts +51 -17
- package/dist/builtin/workflows/src/tui/stage-chat-view-status.ts +36 -0
- package/dist/builtin/workflows/src/tui/stage-chat-view-types.ts +5 -1
- package/dist/builtin/workflows/src/tui/stage-chat-view.ts +3 -1
- package/dist/builtin/workflows/src/tui/status-list.ts +14 -2
- package/dist/builtin/workflows/src/tui/widget.ts +23 -8
- package/dist/builtin/workflows/src/tui/workflow-attach-pane-types.ts +5 -4
- package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +8 -8
- package/dist/builtin/workflows/src/tui/workflow-resume-selector.ts +151 -0
- package/dist/core/extensions/loader-virtual-modules.d.ts.map +1 -1
- package/dist/core/extensions/loader-virtual-modules.js +47 -30
- package/dist/core/extensions/loader-virtual-modules.js.map +1 -1
- package/dist/core/messages.d.ts +1 -0
- package/dist/core/messages.d.ts.map +1 -1
- package/dist/core/messages.js +46 -1
- package/dist/core/messages.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +12 -0
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager-core.d.ts +15 -7
- package/dist/core/session-manager-core.d.ts.map +1 -1
- package/dist/core/session-manager-core.js +20 -9
- package/dist/core/session-manager-core.js.map +1 -1
- package/dist/core/session-manager-entries.d.ts +2 -2
- package/dist/core/session-manager-entries.d.ts.map +1 -1
- package/dist/core/session-manager-entries.js +9 -3
- package/dist/core/session-manager-entries.js.map +1 -1
- package/dist/core/session-manager-history.d.ts.map +1 -1
- package/dist/core/session-manager-history.js +2 -1
- package/dist/core/session-manager-history.js.map +1 -1
- package/dist/core/session-manager-list.d.ts +3 -3
- package/dist/core/session-manager-list.d.ts.map +1 -1
- package/dist/core/session-manager-list.js +27 -8
- package/dist/core/session-manager-list.js.map +1 -1
- package/dist/core/session-manager-storage.d.ts +3 -1
- package/dist/core/session-manager-storage.d.ts.map +1 -1
- package/dist/core/session-manager-storage.js +55 -12
- package/dist/core/session-manager-storage.js.map +1 -1
- package/dist/core/session-manager-tool-dependencies.d.ts +10 -0
- package/dist/core/session-manager-tool-dependencies.d.ts.map +1 -0
- package/dist/core/session-manager-tool-dependencies.js +133 -0
- package/dist/core/session-manager-tool-dependencies.js.map +1 -0
- package/dist/core/session-manager-types.d.ts +22 -0
- package/dist/core/session-manager-types.d.ts.map +1 -1
- package/dist/core/session-manager-types.js.map +1 -1
- package/dist/core/session-manager.d.ts +2 -2
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +1 -1
- package/dist/core/session-manager.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-runtime.d.ts +1 -0
- package/dist/modes/interactive/components/chat-session-host-runtime.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-runtime.js +12 -0
- package/dist/modes/interactive/components/chat-session-host-runtime.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-terminal-cleanup.d.ts +4 -0
- package/dist/modes/interactive/components/chat-session-host-terminal-cleanup.d.ts.map +1 -0
- package/dist/modes/interactive/components/chat-session-host-terminal-cleanup.js +131 -0
- package/dist/modes/interactive/components/chat-session-host-terminal-cleanup.js.map +1 -0
- package/dist/modes/interactive/components/chat-session-host.d.ts +2 -0
- package/dist/modes/interactive/components/chat-session-host.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host.js +7 -1
- package/dist/modes/interactive/components/chat-session-host.js.map +1 -1
- package/dist/modes/interactive/components/chat-transcript.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-transcript.js +15 -4
- package/dist/modes/interactive/components/chat-transcript.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +3 -0
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +26 -0
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/docs/compaction.md +2 -0
- package/docs/models.md +1 -1
- package/docs/providers.md +2 -1
- package/docs/session-format.md +6 -0
- package/docs/sessions.md +6 -0
- package/docs/workflows.md +105 -3
- package/package.json +4 -3
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-backed durable backend.
|
|
3
|
+
*
|
|
4
|
+
* Persists durable checkpoints to JSON files so a new Atomic session/process
|
|
5
|
+
* can resume a workflow started in a prior session without requiring Postgres.
|
|
6
|
+
* The default directory backend stores one state file per root workflow to keep
|
|
7
|
+
* checkpoint writes bounded to that workflow. Each state file still uses a
|
|
8
|
+
* small lock directory plus read-merge-write to avoid lost updates when multiple
|
|
9
|
+
* Atomic processes update the same workflow.
|
|
10
|
+
*
|
|
11
|
+
* cross-ref: issue #1498 — durable fallback when DBOS/Postgres is unavailable.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
chmodSync,
|
|
16
|
+
existsSync,
|
|
17
|
+
mkdirSync,
|
|
18
|
+
readFileSync,
|
|
19
|
+
readdirSync,
|
|
20
|
+
renameSync,
|
|
21
|
+
rmSync,
|
|
22
|
+
statSync,
|
|
23
|
+
writeFileSync,
|
|
24
|
+
} from "node:fs";
|
|
25
|
+
import { hostname } from "node:os";
|
|
26
|
+
import { dirname } from "node:path";
|
|
27
|
+
import type { DurableCheckpoint, DurableWorkflowHandle, DurableWorkflowStatus } from "./types.js";
|
|
28
|
+
import { InMemoryDurableBackend, type DurableWorkflowBackend } from "./backend.js";
|
|
29
|
+
|
|
30
|
+
interface FileDurableRecord {
|
|
31
|
+
readonly handle: DurableWorkflowHandle;
|
|
32
|
+
readonly checkpoints: readonly DurableCheckpoint[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface FileDurableState {
|
|
36
|
+
readonly version: number;
|
|
37
|
+
readonly workflows: readonly FileDurableRecord[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface LockOwner {
|
|
41
|
+
readonly pid: number;
|
|
42
|
+
readonly host: string;
|
|
43
|
+
readonly token: string;
|
|
44
|
+
readonly acquiredAt: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const FILE_FORMAT_VERSION = 1;
|
|
48
|
+
const LOCK_OWNER_FILE = "owner.json";
|
|
49
|
+
const STALE_LOCK_MS = 30_000;
|
|
50
|
+
|
|
51
|
+
export class FileDurableBackend implements DurableWorkflowBackend {
|
|
52
|
+
public readonly persistent = true;
|
|
53
|
+
private readonly mem = new InMemoryDurableBackend();
|
|
54
|
+
private readonly filePath: string;
|
|
55
|
+
private loaded = false;
|
|
56
|
+
|
|
57
|
+
constructor(filePath: string) {
|
|
58
|
+
this.filePath = filePath;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private ensureLoaded(): void {
|
|
62
|
+
if (this.loaded) return;
|
|
63
|
+
this.loaded = true;
|
|
64
|
+
this.mem.importAll(readState(this.filePath).workflows);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private persist(): void {
|
|
68
|
+
withFileLock(this.filePath, () => {
|
|
69
|
+
const merged = mergeRecords(readState(this.filePath).workflows, this.mem.exportAll());
|
|
70
|
+
this.mem.reset();
|
|
71
|
+
this.mem.importAll(merged);
|
|
72
|
+
writeState(this.filePath, { version: FILE_FORMAT_VERSION, workflows: merged });
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
registerWorkflow(handle: Parameters<DurableWorkflowBackend["registerWorkflow"]>[0]): void {
|
|
77
|
+
this.ensureLoaded();
|
|
78
|
+
this.mem.registerWorkflow(handle);
|
|
79
|
+
this.persist();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
recordCheckpoint(checkpoint: DurableCheckpoint): void {
|
|
83
|
+
this.ensureLoaded();
|
|
84
|
+
this.mem.recordCheckpoint(checkpoint);
|
|
85
|
+
this.persist();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
getToolOutput(workflowId: string, argsHash: string) {
|
|
89
|
+
this.ensureLoaded();
|
|
90
|
+
return this.mem.getToolOutput(workflowId, argsHash);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
getUiResponse(workflowId: string, promptHash: string) {
|
|
94
|
+
this.ensureLoaded();
|
|
95
|
+
return this.mem.getUiResponse(workflowId, promptHash);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
getStageOutput(workflowId: string, replayKey: string) {
|
|
99
|
+
this.ensureLoaded();
|
|
100
|
+
return this.mem.getStageOutput(workflowId, replayKey);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
getStageSession(workflowId: string, replayKey: string) {
|
|
104
|
+
this.ensureLoaded();
|
|
105
|
+
return this.mem.getStageSession(workflowId, replayKey);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
listCheckpoints(workflowId: string): readonly DurableCheckpoint[] {
|
|
109
|
+
this.ensureLoaded();
|
|
110
|
+
return this.mem.listCheckpoints(workflowId);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
getWorkflow(workflowId: string) {
|
|
114
|
+
this.ensureLoaded();
|
|
115
|
+
return this.mem.getWorkflow(workflowId);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
setWorkflowStatus(workflowId: string, status: Parameters<DurableWorkflowBackend["setWorkflowStatus"]>[1], pendingPrompts?: number, resumable?: boolean): void {
|
|
119
|
+
this.ensureLoaded();
|
|
120
|
+
this.mem.setWorkflowStatus(workflowId, status, pendingPrompts, resumable);
|
|
121
|
+
this.persist();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
listResumableWorkflows() {
|
|
125
|
+
this.ensureLoaded();
|
|
126
|
+
return this.mem.listResumableWorkflows();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
toCacheEntry(workflowId: string) {
|
|
130
|
+
this.ensureLoaded();
|
|
131
|
+
return this.mem.toCacheEntry(workflowId);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
reset(): void {
|
|
135
|
+
this.mem.reset();
|
|
136
|
+
withFileLock(this.filePath, () => writeState(this.filePath, { version: FILE_FORMAT_VERSION, workflows: [] }));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Directory-backed durable backend that stores each root workflow in its own
|
|
142
|
+
* JSON file while preserving the same DurableWorkflowBackend interface.
|
|
143
|
+
*/
|
|
144
|
+
export class WorkflowFileDurableBackend implements DurableWorkflowBackend {
|
|
145
|
+
public readonly persistent = true;
|
|
146
|
+
private readonly dir: string;
|
|
147
|
+
private readonly fileBackends = new Map<string, FileDurableBackend>();
|
|
148
|
+
|
|
149
|
+
constructor(dir: string) {
|
|
150
|
+
this.dir = dir;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
registerWorkflow(handle: Parameters<DurableWorkflowBackend["registerWorkflow"]>[0]): void {
|
|
154
|
+
this.backendFor(handle.workflowId).registerWorkflow(handle);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
recordCheckpoint(checkpoint: DurableCheckpoint): void {
|
|
158
|
+
this.backendFor(checkpoint.workflowId).recordCheckpoint(checkpoint);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
getToolOutput(workflowId: string, argsHash: string) {
|
|
162
|
+
return this.backendFor(workflowId).getToolOutput(workflowId, argsHash);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
getUiResponse(workflowId: string, promptHash: string) {
|
|
166
|
+
return this.backendFor(workflowId).getUiResponse(workflowId, promptHash);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
getStageOutput(workflowId: string, replayKey: string) {
|
|
170
|
+
return this.backendFor(workflowId).getStageOutput(workflowId, replayKey);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
getStageSession(workflowId: string, replayKey: string) {
|
|
174
|
+
return this.backendFor(workflowId).getStageSession(workflowId, replayKey);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
listCheckpoints(workflowId: string): readonly DurableCheckpoint[] {
|
|
178
|
+
return this.backendFor(workflowId).listCheckpoints(workflowId);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
getWorkflow(workflowId: string) {
|
|
182
|
+
return this.backendFor(workflowId).getWorkflow(workflowId);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
setWorkflowStatus(workflowId: string, status: DurableWorkflowStatus, pendingPrompts?: number, resumable?: boolean): void {
|
|
186
|
+
const backend = this.backendFor(workflowId);
|
|
187
|
+
backend.setWorkflowStatus(workflowId, status, pendingPrompts, resumable);
|
|
188
|
+
if (isPrunableTerminalStatus(status, resumable)) this.removeWorkflowFile(workflowId);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
listResumableWorkflows() {
|
|
192
|
+
const mem = new InMemoryDurableBackend();
|
|
193
|
+
mem.importAll(mergeRecords([], this.readAllRecords()));
|
|
194
|
+
return mem.listResumableWorkflows();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
toCacheEntry(workflowId: string) {
|
|
198
|
+
return this.backendFor(workflowId).toCacheEntry(workflowId);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
reset(): void {
|
|
202
|
+
this.fileBackends.clear();
|
|
203
|
+
for (const filePath of this.stateFiles()) this.removeStateFile(filePath);
|
|
204
|
+
for (const lockPath of this.lockDirs()) rmSync(lockPath, { recursive: true, force: true });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private backendFor(workflowId: string): FileDurableBackend {
|
|
208
|
+
return this.backendForFile(durableStateFileFor(this.dir, workflowId));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private backendForFile(filePath: string): FileDurableBackend {
|
|
212
|
+
const existing = this.fileBackends.get(filePath);
|
|
213
|
+
if (existing !== undefined) return existing;
|
|
214
|
+
const backend = new FileDurableBackend(filePath);
|
|
215
|
+
this.fileBackends.set(filePath, backend);
|
|
216
|
+
return backend;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private stateFiles(): readonly string[] {
|
|
220
|
+
try {
|
|
221
|
+
return readdirSync(this.dir, { withFileTypes: true })
|
|
222
|
+
.filter((entry) => entry.isFile() && entry.name.startsWith("workflow-") && entry.name.endsWith(".json"))
|
|
223
|
+
.map((entry) => `${this.dir}/${entry.name}`);
|
|
224
|
+
} catch {
|
|
225
|
+
return [];
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private lockDirs(): readonly string[] {
|
|
230
|
+
try {
|
|
231
|
+
return readdirSync(this.dir, { withFileTypes: true })
|
|
232
|
+
.filter((entry) => entry.isDirectory() && entry.name.startsWith("workflow-") && entry.name.endsWith(".json.lock"))
|
|
233
|
+
.map((entry) => `${this.dir}/${entry.name}`);
|
|
234
|
+
} catch {
|
|
235
|
+
return [];
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private readAllRecords(): readonly FileDurableRecord[] {
|
|
240
|
+
return this.stateFiles().flatMap((filePath) => readState(filePath).workflows);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private removeWorkflowFile(workflowId: string): void {
|
|
244
|
+
this.removeStateFile(durableStateFileFor(this.dir, workflowId));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private removeStateFile(filePath: string): void {
|
|
248
|
+
this.fileBackends.delete(filePath);
|
|
249
|
+
rmSync(filePath, { force: true });
|
|
250
|
+
rmSync(`${filePath}.lock`, { recursive: true, force: true });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function readState(filePath: string): FileDurableState {
|
|
255
|
+
if (!existsSync(filePath)) return { version: FILE_FORMAT_VERSION, workflows: [] };
|
|
256
|
+
try {
|
|
257
|
+
const parsed = JSON.parse(readFileSync(filePath, "utf-8")) as FileDurableState;
|
|
258
|
+
return parsed && Array.isArray(parsed.workflows) ? parsed : { version: FILE_FORMAT_VERSION, workflows: [] };
|
|
259
|
+
} catch {
|
|
260
|
+
return { version: FILE_FORMAT_VERSION, workflows: [] };
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function writeState(filePath: string, state: FileDurableState): void {
|
|
265
|
+
const dir = dirname(filePath);
|
|
266
|
+
ensureSecureDir(dir);
|
|
267
|
+
const tmp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
268
|
+
writeFileSync(tmp, JSON.stringify(state), { encoding: "utf-8", mode: 0o600 });
|
|
269
|
+
chmodBestEffort(tmp, 0o600);
|
|
270
|
+
renameSync(tmp, filePath);
|
|
271
|
+
chmodBestEffort(filePath, 0o600);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function withFileLock<T>(filePath: string, fn: () => T): T {
|
|
275
|
+
const dir = dirname(filePath);
|
|
276
|
+
ensureSecureDir(dir);
|
|
277
|
+
const lockDir = `${filePath}.lock`;
|
|
278
|
+
const deadline = Date.now() + 5000;
|
|
279
|
+
while (true) {
|
|
280
|
+
try {
|
|
281
|
+
mkdirSync(lockDir, { mode: 0o700 });
|
|
282
|
+
try {
|
|
283
|
+
chmodBestEffort(lockDir, 0o700);
|
|
284
|
+
writeLockOwner(lockDir);
|
|
285
|
+
} catch (ownerErr) {
|
|
286
|
+
rmSync(lockDir, { recursive: true, force: true });
|
|
287
|
+
throw ownerErr;
|
|
288
|
+
}
|
|
289
|
+
break;
|
|
290
|
+
} catch (err) {
|
|
291
|
+
if (errorCode(err) !== "EEXIST") throw err;
|
|
292
|
+
if (tryReclaimStaleLock(lockDir, STALE_LOCK_MS)) continue;
|
|
293
|
+
if (Date.now() > deadline) throw new Error(`Timed out acquiring durable workflow state lock: ${lockDir}`);
|
|
294
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
try {
|
|
298
|
+
return fn();
|
|
299
|
+
} finally {
|
|
300
|
+
rmSync(lockDir, { recursive: true, force: true });
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function tryReclaimStaleLock(lockDir: string, staleMs: number): boolean {
|
|
305
|
+
if (!isStaleLock(lockDir, staleMs)) return false;
|
|
306
|
+
const owner = readLockOwner(lockDir);
|
|
307
|
+
if (owner === undefined || !isLockOwnerAbandoned(owner)) return false;
|
|
308
|
+
const quarantine = `${lockDir}.stale.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}`;
|
|
309
|
+
try {
|
|
310
|
+
renameSync(lockDir, quarantine);
|
|
311
|
+
} catch {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
const quarantinedOwner = readLockOwner(quarantine);
|
|
315
|
+
if (!sameLockOwner(owner, quarantinedOwner)) {
|
|
316
|
+
restoreQuarantinedLock(lockDir, quarantine);
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
try {
|
|
320
|
+
rmSync(quarantine, { recursive: true, force: true });
|
|
321
|
+
} catch {
|
|
322
|
+
// The stale lock has already been moved away from the active lock path.
|
|
323
|
+
}
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function isStaleLock(lockDir: string, staleMs: number): boolean {
|
|
328
|
+
try {
|
|
329
|
+
const stat = statSync(lockDir);
|
|
330
|
+
return Date.now() - stat.mtimeMs > staleMs;
|
|
331
|
+
} catch {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function writeLockOwner(lockDir: string): void {
|
|
337
|
+
const owner: LockOwner = {
|
|
338
|
+
pid: process.pid,
|
|
339
|
+
host: hostname(),
|
|
340
|
+
token: `${process.pid}:${Date.now()}:${Math.random().toString(36).slice(2)}`,
|
|
341
|
+
acquiredAt: Date.now(),
|
|
342
|
+
};
|
|
343
|
+
const ownerPath = `${lockDir}/${LOCK_OWNER_FILE}`;
|
|
344
|
+
writeFileSync(ownerPath, JSON.stringify(owner), { encoding: "utf-8", mode: 0o600 });
|
|
345
|
+
chmodBestEffort(ownerPath, 0o600);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function readLockOwner(lockDir: string): LockOwner | undefined {
|
|
349
|
+
try {
|
|
350
|
+
const parsed = JSON.parse(readFileSync(`${lockDir}/${LOCK_OWNER_FILE}`, "utf-8"));
|
|
351
|
+
if (!isLockOwner(parsed)) return undefined;
|
|
352
|
+
return parsed;
|
|
353
|
+
} catch {
|
|
354
|
+
return undefined;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function isLockOwner(value: unknown): value is LockOwner {
|
|
359
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
360
|
+
const record = value as Partial<LockOwner>;
|
|
361
|
+
return typeof record.pid === "number"
|
|
362
|
+
&& typeof record.host === "string"
|
|
363
|
+
&& typeof record.token === "string"
|
|
364
|
+
&& typeof record.acquiredAt === "number";
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function isLockOwnerAbandoned(owner: LockOwner): boolean {
|
|
368
|
+
if (owner.host !== hostname()) return false;
|
|
369
|
+
if (!Number.isInteger(owner.pid) || owner.pid <= 0) return false;
|
|
370
|
+
try {
|
|
371
|
+
process.kill(owner.pid, 0);
|
|
372
|
+
return false;
|
|
373
|
+
} catch (err) {
|
|
374
|
+
const code = errorCode(err);
|
|
375
|
+
return code === "ESRCH";
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function sameLockOwner(a: LockOwner, b: LockOwner | undefined): boolean {
|
|
380
|
+
return b !== undefined && a.pid === b.pid && a.host === b.host && a.token === b.token;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function restoreQuarantinedLock(lockDir: string, quarantine: string): void {
|
|
384
|
+
try {
|
|
385
|
+
if (!existsSync(lockDir)) renameSync(quarantine, lockDir);
|
|
386
|
+
else rmSync(quarantine, { recursive: true, force: true });
|
|
387
|
+
} catch {
|
|
388
|
+
// Best-effort: never delete the active lock path after a failed compare.
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function errorCode(err: unknown): string | undefined {
|
|
393
|
+
return typeof err === "object" && err !== null && "code" in err ? String(err.code) : undefined;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function ensureSecureDir(dir: string): void {
|
|
397
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
398
|
+
chmodBestEffort(dir, 0o700);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function chmodBestEffort(path: string, mode: number): void {
|
|
402
|
+
try {
|
|
403
|
+
chmodSync(path, mode);
|
|
404
|
+
} catch {
|
|
405
|
+
// chmod is unavailable or unsupported on some filesystems/platforms.
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function isPrunableTerminalStatus(status: DurableWorkflowStatus, resumable?: boolean): boolean {
|
|
410
|
+
if (status === "completed" || status === "cancelled") return true;
|
|
411
|
+
return (status === "failed" || status === "blocked") && resumable === false;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function mergeRecords(a: readonly FileDurableRecord[], b: readonly FileDurableRecord[]): readonly FileDurableRecord[] {
|
|
415
|
+
const byWorkflow = new Map<string, { handle: DurableWorkflowHandle; checkpoints: Map<string, DurableCheckpoint> }>();
|
|
416
|
+
for (const rec of [...a, ...b]) {
|
|
417
|
+
const existing = byWorkflow.get(rec.handle.workflowId);
|
|
418
|
+
const handle = existing === undefined || rec.handle.updatedAt >= existing.handle.updatedAt ? rec.handle : existing.handle;
|
|
419
|
+
const checkpoints = existing?.checkpoints ?? new Map<string, DurableCheckpoint>();
|
|
420
|
+
for (const cp of rec.checkpoints) checkpoints.set(`${cp.kind}:${cp.checkpointId}`, cp);
|
|
421
|
+
byWorkflow.set(rec.handle.workflowId, { handle, checkpoints });
|
|
422
|
+
}
|
|
423
|
+
return [...byWorkflow.values()].map((rec) => ({ handle: rec.handle, checkpoints: [...rec.checkpoints.values()] }));
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export function defaultDurableStateDir(): string | undefined {
|
|
427
|
+
const home = process.env.HOME ?? process.env.USERPROFILE;
|
|
428
|
+
return home === undefined || home.length === 0 ? undefined : `${home}/.atomic/workflow-durable`;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export function durableStateFileFor(dir: string, workflowId: string): string {
|
|
432
|
+
return `${dir}/workflow-${encodeURIComponent(workflowId)}.json`;
|
|
433
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable workflow backend — barrel export.
|
|
3
|
+
*
|
|
4
|
+
* cross-ref: issue #1498 — DBOS-backed cross-session resumability.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type {
|
|
8
|
+
DurableCheckpoint,
|
|
9
|
+
DurableCheckpointEntry,
|
|
10
|
+
DurableStageCheckpoint,
|
|
11
|
+
DurableToolCheckpoint,
|
|
12
|
+
DurableUiCheckpoint,
|
|
13
|
+
DurableWorkflowHandle,
|
|
14
|
+
DurableWorkflowStatus,
|
|
15
|
+
ResumableWorkflowEntry,
|
|
16
|
+
UiPromptKind,
|
|
17
|
+
WorkflowSerializableObject,
|
|
18
|
+
} from "./types.js";
|
|
19
|
+
|
|
20
|
+
export type { DurableWorkflowBackend } from "./backend.js";
|
|
21
|
+
export { InMemoryDurableBackend, durableHash } from "./backend.js";
|
|
22
|
+
export { FileDurableBackend, WorkflowFileDurableBackend, defaultDurableStateDir, durableStateFileFor } from "./file-backend.js";
|
|
23
|
+
export {
|
|
24
|
+
isDbosConfigured,
|
|
25
|
+
DbosDurableBackend,
|
|
26
|
+
createDbosDurableBackend,
|
|
27
|
+
type DbosSdkHandle,
|
|
28
|
+
type DbosWorkflowInfo,
|
|
29
|
+
type DbosStepRecord,
|
|
30
|
+
} from "./dbos-backend.js";
|
|
31
|
+
export {
|
|
32
|
+
encodeCheckpoint,
|
|
33
|
+
decodeToCheckpoint,
|
|
34
|
+
isCheckpointEnvelope,
|
|
35
|
+
DBOS_ENVELOPE_VERSION,
|
|
36
|
+
type DbosCheckpointEnvelope,
|
|
37
|
+
} from "./dbos-envelope.js";
|
|
38
|
+
export {
|
|
39
|
+
getDurableBackend,
|
|
40
|
+
setDurableBackend,
|
|
41
|
+
createInMemoryBackend,
|
|
42
|
+
createDefaultFileBackend,
|
|
43
|
+
createWorkflowFileBackend,
|
|
44
|
+
initializeDbosDurableBackendFromEnv,
|
|
45
|
+
} from "./factory.js";
|
|
46
|
+
export {
|
|
47
|
+
scanResumableWorkflows,
|
|
48
|
+
listResumableFromBackend,
|
|
49
|
+
persistDurableCacheEntry,
|
|
50
|
+
formatResumableWorkflowList,
|
|
51
|
+
} from "./resume-catalog.js";
|
|
52
|
+
export {
|
|
53
|
+
createToolPrimitive,
|
|
54
|
+
createCheckpointIdGenerator,
|
|
55
|
+
type WorkflowToolPrimitive,
|
|
56
|
+
type WorkflowToolOptions,
|
|
57
|
+
} from "./tool-primitive.js";
|
|
58
|
+
export { wrapUiWithDurable, type DurableUiDeps } from "./ui-primitive.js";
|
|
59
|
+
export {
|
|
60
|
+
recordStageCheckpoint,
|
|
61
|
+
createDurableStagePrimitive,
|
|
62
|
+
createDurableTaskPrimitive,
|
|
63
|
+
createStageReplayKeyGenerator,
|
|
64
|
+
stableCheckpointId,
|
|
65
|
+
type DurableStageDeps,
|
|
66
|
+
} from "./stage-primitive.js";
|
|
67
|
+
export {
|
|
68
|
+
resumeDurableWorkflow,
|
|
69
|
+
resolveDurableEntry,
|
|
70
|
+
prepareDurableResume,
|
|
71
|
+
type ResumeDurableDeps,
|
|
72
|
+
type ResumeDurableResult,
|
|
73
|
+
} from "./resume-runtime.js";
|