@bastani/atomic 0.9.10 → 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.
- package/CHANGELOG.md +37 -0
- package/dist/builtin/cursor/CHANGELOG.md +6 -0
- package/dist/builtin/cursor/package.json +3 -3
- package/dist/builtin/intercom/CHANGELOG.md +6 -0
- package/dist/builtin/intercom/package.json +3 -3
- package/dist/builtin/mcp/CHANGELOG.md +6 -0
- package/dist/builtin/mcp/package.json +3 -3
- package/dist/builtin/subagents/CHANGELOG.md +6 -0
- package/dist/builtin/subagents/package.json +4 -4
- package/dist/builtin/web-access/CHANGELOG.md +6 -0
- package/dist/builtin/web-access/package.json +2 -2
- package/dist/builtin/workflows/CHANGELOG.md +23 -0
- package/dist/builtin/workflows/README.md +8 -3
- package/dist/builtin/workflows/builtin/deep-research-codebase.ts +1 -1
- package/dist/builtin/workflows/package.json +2 -2
- package/dist/builtin/workflows/src/durable/backend.ts +2 -2
- package/dist/builtin/workflows/src/durable/child-primitive.ts +3 -0
- package/dist/builtin/workflows/src/durable/completed-catalog.ts +182 -33
- package/dist/builtin/workflows/src/durable/completed-inspection.ts +9 -4
- package/dist/builtin/workflows/src/durable/dbos-embedded-postgres-root.ts +173 -0
- package/dist/builtin/workflows/src/durable/dbos-embedded-postgres.ts +36 -12
- package/dist/builtin/workflows/src/durable/dbos-envelope.ts +31 -8
- package/dist/builtin/workflows/src/durable/dbos-lifecycle.ts +9 -3
- package/dist/builtin/workflows/src/durable/factory.ts +35 -6
- package/dist/builtin/workflows/src/durable/local-command.ts +11 -2
- package/dist/builtin/workflows/src/durable/resume-runtime.ts +6 -2
- package/dist/builtin/workflows/src/durable/stage-primitive.ts +48 -10
- package/dist/builtin/workflows/src/durable/types.ts +11 -0
- package/dist/builtin/workflows/src/engine/run-durable-stage-session.ts +9 -9
- package/dist/builtin/workflows/src/engine/run-durable-topology.ts +89 -0
- package/dist/builtin/workflows/src/engine/run.ts +18 -16
- package/dist/builtin/workflows/src/extension/extension-lifecycle.ts +14 -2
- package/dist/builtin/workflows/src/extension/public-types.ts +3 -4
- package/dist/builtin/workflows/src/extension/render-call.ts +4 -0
- package/dist/builtin/workflows/src/extension/render-result.ts +31 -1
- package/dist/builtin/workflows/src/extension/runtime-durable-resume.ts +3 -0
- package/dist/builtin/workflows/src/extension/workflow-prompts.ts +5 -2
- package/dist/builtin/workflows/src/extension/workflow-schema.ts +3 -2
- package/dist/builtin/workflows/src/extension/workflow-tool-content.ts +1 -0
- package/dist/builtin/workflows/src/extension/workflow-tool-control.ts +88 -2
- package/dist/builtin/workflows/src/extension/workflow-tool.ts +13 -0
- package/dist/builtin/workflows/src/runs/foreground/executor-stage-control.ts +3 -0
- package/dist/builtin/workflows/src/runs/foreground/executor-stage-factory.ts +6 -3
- package/dist/builtin/workflows/src/runs/foreground/executor-stage-types.ts +2 -2
- package/dist/builtin/workflows/src/runs/foreground/executor-types.ts +4 -1
- package/dist/builtin/workflows/src/tui/workflow-resume-selector.ts +2 -3
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +2 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/core/agent-session-compaction.d.ts.map +1 -1
- package/dist/core/agent-session-compaction.js +7 -1
- package/dist/core/agent-session-compaction.js.map +1 -1
- package/dist/core/agent-session-events.d.ts.map +1 -1
- package/dist/core/agent-session-events.js +2 -1
- package/dist/core/agent-session-events.js.map +1 -1
- package/dist/core/agent-session-retry.d.ts.map +1 -1
- package/dist/core/agent-session-retry.js +7 -0
- package/dist/core/agent-session-retry.js.map +1 -1
- package/dist/core/agent-session-runtime.d.ts +13 -0
- package/dist/core/agent-session-runtime.d.ts.map +1 -1
- package/dist/core/agent-session-runtime.js +12 -0
- package/dist/core/agent-session-runtime.js.map +1 -1
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +3 -0
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session-tree.d.ts.map +1 -1
- package/dist/core/agent-session-tree.js +4 -1
- package/dist/core/agent-session-tree.js.map +1 -1
- package/dist/core/agent-session-types.d.ts +15 -0
- package/dist/core/agent-session-types.d.ts.map +1 -1
- package/dist/core/agent-session-types.js.map +1 -1
- package/dist/core/atomic-guide-command.d.ts.map +1 -1
- package/dist/core/atomic-guide-command.js +2 -0
- package/dist/core/atomic-guide-command.js.map +1 -1
- package/dist/core/auth-storage-backends.d.ts +4 -0
- package/dist/core/auth-storage-backends.d.ts.map +1 -1
- package/dist/core/auth-storage-backends.js +48 -3
- package/dist/core/auth-storage-backends.js.map +1 -1
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +27 -11
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/codex-errors.d.ts +3 -0
- package/dist/core/codex-errors.d.ts.map +1 -0
- package/dist/core/codex-errors.js +12 -0
- package/dist/core/codex-errors.js.map +1 -0
- package/dist/core/compaction/branch-summarization.d.ts +5 -1
- package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
- package/dist/core/compaction/branch-summarization.js +7 -8
- package/dist/core/compaction/branch-summarization.js.map +1 -1
- package/dist/core/compaction/compaction-runner.d.ts +3 -1
- package/dist/core/compaction/compaction-runner.d.ts.map +1 -1
- package/dist/core/compaction/compaction-runner.js +6 -1
- package/dist/core/compaction/compaction-runner.js.map +1 -1
- package/dist/core/compaction/range-planner.d.ts +3 -0
- package/dist/core/compaction/range-planner.d.ts.map +1 -1
- package/dist/core/compaction/range-planner.js +2 -1
- package/dist/core/compaction/range-planner.js.map +1 -1
- package/dist/core/model-registry.js +2 -2
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-resolver-defaults.d.ts.map +1 -1
- package/dist/core/model-resolver-defaults.js +2 -0
- package/dist/core/model-resolver-defaults.js.map +1 -1
- package/dist/core/prompt-templates.d.ts +1 -0
- package/dist/core/prompt-templates.d.ts.map +1 -1
- package/dist/core/prompt-templates.js +6 -4
- package/dist/core/prompt-templates.js.map +1 -1
- package/dist/core/remote-catalog-provider.d.ts +1 -1
- package/dist/core/remote-catalog-provider.d.ts.map +1 -1
- package/dist/core/remote-catalog-provider.js +25 -6
- package/dist/core/remote-catalog-provider.js.map +1 -1
- package/dist/core/sdk-exports.d.ts +1 -1
- package/dist/core/sdk-exports.d.ts.map +1 -1
- package/dist/core/sdk-exports.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +4 -1
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager-core.d.ts +1 -0
- package/dist/core/session-manager-core.d.ts.map +1 -1
- package/dist/core/session-manager-core.js +7 -4
- package/dist/core/session-manager-core.js.map +1 -1
- package/dist/core/session-manager-validation.d.ts.map +1 -1
- package/dist/core/session-manager-validation.js +3 -16
- package/dist/core/session-manager-validation.js.map +1 -1
- package/dist/core/slash-commands.js +1 -1
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/summarization-retry.d.ts +11 -0
- package/dist/core/summarization-retry.d.ts.map +1 -0
- package/dist/core/summarization-retry.js +21 -0
- package/dist/core/summarization-retry.js.map +1 -0
- package/dist/core/tools/read.js +1 -1
- package/dist/core/tools/read.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +2 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-events.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-events.js +16 -0
- package/dist/modes/interactive/components/chat-session-host-events.js.map +1 -1
- package/dist/modes/interactive/components/footer.js +2 -2
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/interactive-agent-events.js +6 -0
- package/dist/modes/interactive/interactive-agent-events.js.map +1 -1
- package/dist/modes/interactive/interactive-auth-routing.d.ts +2 -1
- package/dist/modes/interactive/interactive-auth-routing.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-auth-routing.js +10 -5
- package/dist/modes/interactive/interactive-auth-routing.js.map +1 -1
- package/dist/modes/interactive/interactive-autocomplete.js +12 -2
- package/dist/modes/interactive/interactive-autocomplete.js.map +1 -1
- package/dist/modes/interactive/interactive-model-catalog-startup.d.ts +13 -0
- package/dist/modes/interactive/interactive-model-catalog-startup.d.ts.map +1 -0
- package/dist/modes/interactive/interactive-model-catalog-startup.js +24 -0
- package/dist/modes/interactive/interactive-model-catalog-startup.js.map +1 -0
- package/dist/modes/interactive/interactive-model-routing.js +4 -3
- package/dist/modes/interactive/interactive-model-routing.js.map +1 -1
- package/dist/modes/interactive/interactive-queueing.js +3 -2
- package/dist/modes/interactive/interactive-queueing.js.map +1 -1
- package/dist/modes/interactive/interactive-startup.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-startup.js +4 -7
- package/dist/modes/interactive/interactive-startup.js.map +1 -1
- package/dist/modes/interactive/interactive-summarization-retry-events.d.ts +8 -0
- package/dist/modes/interactive/interactive-summarization-retry-events.d.ts.map +1 -0
- package/dist/modes/interactive/interactive-summarization-retry-events.js +40 -0
- package/dist/modes/interactive/interactive-summarization-retry-events.js.map +1 -0
- package/dist/modes/interactive-engine/extension-ui-bridge.d.ts +3 -1
- package/dist/modes/interactive-engine/extension-ui-bridge.d.ts.map +1 -1
- package/dist/modes/interactive-engine/extension-ui-bridge.js +6 -0
- package/dist/modes/interactive-engine/extension-ui-bridge.js.map +1 -1
- package/dist/modes/interactive-engine/isolated-runtime.d.ts +4 -5
- package/dist/modes/interactive-engine/isolated-runtime.d.ts.map +1 -1
- package/dist/modes/interactive-engine/isolated-runtime.js +14 -24
- package/dist/modes/interactive-engine/isolated-runtime.js.map +1 -1
- package/dist/modes/interactive-engine/remote-model-catalog.d.ts +15 -0
- package/dist/modes/interactive-engine/remote-model-catalog.d.ts.map +1 -0
- package/dist/modes/interactive-engine/remote-model-catalog.js +67 -0
- package/dist/modes/interactive-engine/remote-model-catalog.js.map +1 -0
- package/dist/modes/rpc/rpc-client-api.d.ts +9 -1
- package/dist/modes/rpc/rpc-client-api.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client-api.js +12 -0
- package/dist/modes/rpc/rpc-client-api.js.map +1 -1
- package/dist/modes/rpc/rpc-command-handler.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-command-handler.js +35 -0
- package/dist/modes/rpc/rpc-command-handler.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +78 -7
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/docs/docs.json +9 -1
- package/docs/extensions.md +3 -3
- package/docs/json.md +7 -1
- package/docs/models/artificial-analysis-index.md +82 -0
- package/docs/models/model-selection.md +63 -0
- package/docs/models/pareto-efficiency.md +64 -0
- package/docs/prompt-templates.md +1 -0
- package/docs/providers.md +9 -1
- package/docs/quickstart.md +1 -1
- package/docs/rpc.md +76 -0
- package/docs/sdk.md +3 -0
- package/docs/tui.md +2 -2
- package/docs/workflows.md +26 -13
- package/examples/extensions/README.md +1 -1
- package/examples/extensions/sandbox/package-lock.json +3 -3
- package/npm-shrinkwrap.json +37 -37
- package/package.json +7 -7
|
@@ -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
|
|
92
|
+
if (handle === undefined || handle.status !== "completed") return [];
|
|
81
93
|
const checkpoints = backend.listCheckpoints(entry.workflowId);
|
|
82
|
-
if (checkpoints.length === 0) return
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return
|
|
87
|
-
|
|
88
|
-
|
|
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:
|
|
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
|
|
136
|
+
function runSnapshotsFromCheckpoints(
|
|
108
137
|
checkpoints: readonly DurableCheckpoint[],
|
|
138
|
+
rootRunId: string,
|
|
139
|
+
rootRunName: string,
|
|
109
140
|
fallbackCompletedAt: number,
|
|
110
|
-
|
|
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()]
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
...
|
|
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
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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,
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Root-execution support for the embedded DBOS Postgres.
|
|
3
|
+
*
|
|
4
|
+
* PostgreSQL categorically refuses to run `initdb`/`postgres` as UID 0, so a
|
|
5
|
+
* root Atomic process (common in containers, CI sandboxes, and eval harnesses)
|
|
6
|
+
* cannot provision the embedded cluster directly. On Linux we instead resolve
|
|
7
|
+
* an unprivileged system account, keep the cluster under a root-safe base
|
|
8
|
+
* directory (`/root` is mode 0700 and untraversable by that account), and run
|
|
9
|
+
* every Postgres command with dropped privileges.
|
|
10
|
+
*
|
|
11
|
+
* Privilege dropping is strategy-probed at runtime: Node honors the child
|
|
12
|
+
* process `uid`/`gid` spawn options, but Bun currently ignores them, so we
|
|
13
|
+
* verify the drop actually happens (`id -u` must report the owner) and fall
|
|
14
|
+
* back to `setpriv`, `runuser`, or `su` wrappers when it does not.
|
|
15
|
+
*
|
|
16
|
+
* The embedded binaries themselves may also live under an untraversable
|
|
17
|
+
* prefix (for example a root-owned `~/.nvm` global install), so the caller can
|
|
18
|
+
* probe them as the unprivileged owner and fall back to a one-time copy into
|
|
19
|
+
* the cluster base directory.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { cpSync, existsSync } from "node:fs";
|
|
23
|
+
import { homedir } from "node:os";
|
|
24
|
+
import { dirname, join } from "node:path";
|
|
25
|
+
import { runLocalCommand, type LocalCommandResult } from "./local-command.js";
|
|
26
|
+
|
|
27
|
+
export interface EmbeddedPostgresOwner {
|
|
28
|
+
readonly uid: number;
|
|
29
|
+
readonly gid: number;
|
|
30
|
+
readonly name: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface EmbeddedPostgresRunContext {
|
|
34
|
+
/** Directory that holds the cluster, log file, and setup locks. */
|
|
35
|
+
readonly baseDir: string;
|
|
36
|
+
/** Present only when commands must drop privileges (Linux root). */
|
|
37
|
+
readonly owner?: EmbeddedPostgresOwner;
|
|
38
|
+
/** Runs a command as the owner; identity pass-through when no owner. */
|
|
39
|
+
readonly runAsOwner: LocalCommandRunner;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface EmbeddedPostgresBinaryPaths {
|
|
43
|
+
readonly pg_ctl: string;
|
|
44
|
+
readonly initdb: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type LocalCommandRunner = (
|
|
48
|
+
command: string,
|
|
49
|
+
args: readonly string[],
|
|
50
|
+
options?: { readonly uid?: number; readonly gid?: number },
|
|
51
|
+
) => Promise<LocalCommandResult>;
|
|
52
|
+
|
|
53
|
+
/** Root-safe cluster location: system path, traversable by system accounts. */
|
|
54
|
+
export const ROOT_EMBEDDED_BASE_DIR = "/var/lib/atomic-postgres";
|
|
55
|
+
|
|
56
|
+
/** Unprivileged accounts tried in order; `postgres` wins when present. */
|
|
57
|
+
const OWNER_CANDIDATES = ["postgres", "nobody", "daemon"] as const;
|
|
58
|
+
|
|
59
|
+
export function defaultEmbeddedBaseDir(): string {
|
|
60
|
+
return join(homedir(), ".atomic", "postgres");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Resolve where and as whom the embedded cluster should run. Non-root (and
|
|
65
|
+
* every non-Linux platform) keeps the historical home-directory layout. Linux
|
|
66
|
+
* root without a resolvable unprivileged account, or without any working
|
|
67
|
+
* privilege-drop mechanism, also falls through to the default context so
|
|
68
|
+
* PostgreSQL's own root refusal surfaces with full detail.
|
|
69
|
+
*/
|
|
70
|
+
export async function resolveEmbeddedRunContext(
|
|
71
|
+
runner: LocalCommandRunner = runLocalCommand,
|
|
72
|
+
euid: number | undefined = process.getuid?.(),
|
|
73
|
+
platform: NodeJS.Platform = process.platform,
|
|
74
|
+
): Promise<EmbeddedPostgresRunContext> {
|
|
75
|
+
if (platform !== "linux" || euid !== 0) {
|
|
76
|
+
return { baseDir: defaultEmbeddedBaseDir(), runAsOwner: runner };
|
|
77
|
+
}
|
|
78
|
+
for (const name of OWNER_CANDIDATES) {
|
|
79
|
+
const owner = await lookupOwner(runner, name);
|
|
80
|
+
if (owner === undefined) continue;
|
|
81
|
+
const runAsOwner = await resolvePrivilegeDrop(runner, owner);
|
|
82
|
+
if (runAsOwner === undefined) continue;
|
|
83
|
+
return { baseDir: ROOT_EMBEDDED_BASE_DIR, owner, runAsOwner };
|
|
84
|
+
}
|
|
85
|
+
return { baseDir: defaultEmbeddedBaseDir(), runAsOwner: runner };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Return a runner that verifiably executes commands as the owner, or
|
|
90
|
+
* `undefined` when no drop mechanism works. Each strategy is validated by
|
|
91
|
+
* running `id -u` through it and requiring the owner's uid on stdout — the
|
|
92
|
+
* spawn `uid`/`gid` options in particular are silently ignored by Bun.
|
|
93
|
+
*/
|
|
94
|
+
export async function resolvePrivilegeDrop(
|
|
95
|
+
runner: LocalCommandRunner,
|
|
96
|
+
owner: EmbeddedPostgresOwner,
|
|
97
|
+
): Promise<LocalCommandRunner | undefined> {
|
|
98
|
+
const strategies: readonly LocalCommandRunner[] = [
|
|
99
|
+
(command, args) => runner(command, args, { uid: owner.uid, gid: owner.gid }),
|
|
100
|
+
(command, args) => runner("setpriv", [
|
|
101
|
+
`--reuid=${owner.uid}`, `--regid=${owner.gid}`, "--clear-groups", "--", command, ...args,
|
|
102
|
+
]),
|
|
103
|
+
(command, args) => runner("runuser", ["-u", owner.name, "--", command, ...args]),
|
|
104
|
+
(command, args) => runner("su", ["-s", "/bin/sh", "-c", shellCommand(command, args), owner.name]),
|
|
105
|
+
];
|
|
106
|
+
for (const strategy of strategies) {
|
|
107
|
+
const probe = await strategy("id", ["-u"]).catch(() => undefined);
|
|
108
|
+
if (probe !== undefined && probe.exitCode === 0 && probe.stdout.trim() === String(owner.uid)) {
|
|
109
|
+
return strategy;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Ensure the embedded binaries are executable by the drop-privilege owner.
|
|
117
|
+
* Probes `initdb --version` as the owner; on failure (typically an
|
|
118
|
+
* untraversable ancestor such as `/root`) copies the package's `native` tree
|
|
119
|
+
* into the cluster base directory once and reuses it afterwards.
|
|
120
|
+
*/
|
|
121
|
+
export async function prepareBinariesForOwner(
|
|
122
|
+
binaries: EmbeddedPostgresBinaryPaths,
|
|
123
|
+
context: EmbeddedPostgresRunContext,
|
|
124
|
+
runner: LocalCommandRunner = runLocalCommand,
|
|
125
|
+
): Promise<EmbeddedPostgresBinaryPaths> {
|
|
126
|
+
const owner = context.owner;
|
|
127
|
+
if (owner === undefined) return binaries;
|
|
128
|
+
|
|
129
|
+
const probe = await context.runAsOwner(binaries.initdb, ["--version"]).catch(() => undefined);
|
|
130
|
+
if (probe !== undefined && probe.exitCode === 0) return binaries;
|
|
131
|
+
|
|
132
|
+
// `<packageRoot>/native/bin/initdb` → copy the whole `native` tree so the
|
|
133
|
+
// binaries keep their relative `../lib` runtime library references.
|
|
134
|
+
const nativeDir = dirname(dirname(binaries.initdb));
|
|
135
|
+
const copiedNativeDir = join(context.baseDir, "pg-runtime", "native");
|
|
136
|
+
const copied: EmbeddedPostgresBinaryPaths = {
|
|
137
|
+
pg_ctl: join(copiedNativeDir, "bin", "pg_ctl"),
|
|
138
|
+
initdb: join(copiedNativeDir, "bin", "initdb"),
|
|
139
|
+
};
|
|
140
|
+
if (!existsSync(copied.initdb)) {
|
|
141
|
+
cpSync(nativeDir, copiedNativeDir, { recursive: true });
|
|
142
|
+
}
|
|
143
|
+
const chown = await runner("chown", ["-R", `${owner.uid}:${owner.gid}`, join(context.baseDir, "pg-runtime")]);
|
|
144
|
+
if (chown.exitCode !== 0) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`Could not hand the copied embedded Postgres runtime to ${owner.name}: ${chown.stderr.trim() || chown.stdout.trim() || `exit ${chown.exitCode}`}`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
return copied;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function lookupOwner(runner: LocalCommandRunner, name: string): Promise<EmbeddedPostgresOwner | undefined> {
|
|
153
|
+
const uid = await lookupId(runner, ["-u", name]);
|
|
154
|
+
const gid = await lookupId(runner, ["-g", name]);
|
|
155
|
+
if (uid === undefined || gid === undefined || uid === 0) return undefined;
|
|
156
|
+
return { uid, gid, name };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function lookupId(runner: LocalCommandRunner, args: readonly string[]): Promise<number | undefined> {
|
|
160
|
+
try {
|
|
161
|
+
const result = await runner("id", args);
|
|
162
|
+
if (result.exitCode !== 0) return undefined;
|
|
163
|
+
const value = Number.parseInt(result.stdout.trim(), 10);
|
|
164
|
+
return Number.isInteger(value) && value > 0 ? value : undefined;
|
|
165
|
+
} catch {
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Single-quote a command line for `su -c`; arguments never embed user input. */
|
|
171
|
+
function shellCommand(command: string, args: readonly string[]): string {
|
|
172
|
+
return [command, ...args].map((part) => `'${part.replaceAll("'", "'\\''")}'`).join(" ");
|
|
173
|
+
}
|
|
@@ -10,12 +10,23 @@
|
|
|
10
10
|
* is started with `pg_ctl`, which daemonizes the server into its own session:
|
|
11
11
|
* it survives Atomic exiting and is shared by every concurrent Atomic session.
|
|
12
12
|
* Atomic never stops it.
|
|
13
|
+
*
|
|
14
|
+
* PostgreSQL refuses to run as UID 0, so a root Atomic process (containers,
|
|
15
|
+
* CI sandboxes, eval harnesses) resolves an unprivileged system account, keeps
|
|
16
|
+
* the cluster under `/var/lib/atomic-postgres` instead (a root home directory
|
|
17
|
+
* is untraversable for that account), and runs every Postgres command with
|
|
18
|
+
* dropped privileges. See dbos-embedded-postgres-root.ts.
|
|
13
19
|
*/
|
|
14
20
|
|
|
15
|
-
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
|
16
|
-
import {
|
|
21
|
+
import { chmodSync, chownSync, copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
|
22
|
+
import { tmpdir } from "node:os";
|
|
17
23
|
import { dirname, join, relative } from "node:path";
|
|
18
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
prepareBinariesForOwner,
|
|
26
|
+
resolveEmbeddedRunContext,
|
|
27
|
+
type EmbeddedPostgresRunContext,
|
|
28
|
+
} from "./dbos-embedded-postgres-root.js";
|
|
29
|
+
import { commandFailureDetail, delay, tcpReachable } from "./local-command.js";
|
|
19
30
|
|
|
20
31
|
const EMBEDDED_HOST = "127.0.0.1";
|
|
21
32
|
const EMBEDDED_PORT = 5439;
|
|
@@ -47,17 +58,20 @@ export function ensureEmbeddedDbosPostgres(): Promise<void> {
|
|
|
47
58
|
|
|
48
59
|
async function ensure(): Promise<void> {
|
|
49
60
|
if (await tcpReachable(EMBEDDED_HOST, EMBEDDED_PORT)) return;
|
|
50
|
-
const
|
|
51
|
-
hydrateBinaryLibraryLinks(
|
|
52
|
-
const
|
|
61
|
+
const loaded = await loadEmbeddedPostgresBinaries();
|
|
62
|
+
hydrateBinaryLibraryLinks(loaded.pg_ctl);
|
|
63
|
+
const context = await resolveEmbeddedRunContext();
|
|
64
|
+
const root = context.baseDir;
|
|
53
65
|
const dataDir = join(root, `v${EMBEDDED_PG_MAJOR}`);
|
|
54
66
|
const logFile = join(root, `v${EMBEDDED_PG_MAJOR}.log`);
|
|
55
67
|
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
68
|
+
if (context.owner !== undefined) chownSync(root, context.owner.uid, context.owner.gid);
|
|
69
|
+
const binaries = await prepareBinariesForOwner(loaded, context);
|
|
56
70
|
|
|
57
71
|
await withSetupLock(join(root, `v${EMBEDDED_PG_MAJOR}.setup-lock`), async () => {
|
|
58
72
|
if (await tcpReachable(EMBEDDED_HOST, EMBEDDED_PORT)) return;
|
|
59
|
-
if (!existsSync(join(dataDir, "PG_VERSION"))) await initializeCluster(binaries.initdb, dataDir);
|
|
60
|
-
await startCluster(binaries.pg_ctl, dataDir, logFile);
|
|
73
|
+
if (!existsSync(join(dataDir, "PG_VERSION"))) await initializeCluster(binaries.initdb, dataDir, context);
|
|
74
|
+
await startCluster(binaries.pg_ctl, dataDir, logFile, context);
|
|
61
75
|
});
|
|
62
76
|
|
|
63
77
|
for (let attempt = 0; attempt < READY_ATTEMPTS; attempt += 1) {
|
|
@@ -67,11 +81,16 @@ async function ensure(): Promise<void> {
|
|
|
67
81
|
throw new Error(`Embedded Postgres started but never accepted connections on ${EMBEDDED_HOST}:${EMBEDDED_PORT}; see ${logFile}.`);
|
|
68
82
|
}
|
|
69
83
|
|
|
70
|
-
async function initializeCluster(
|
|
84
|
+
async function initializeCluster(
|
|
85
|
+
initdb: string,
|
|
86
|
+
dataDir: string,
|
|
87
|
+
context: EmbeddedPostgresRunContext,
|
|
88
|
+
): Promise<void> {
|
|
71
89
|
const passwordFile = join(tmpdir(), `atomic-pg-pw-${process.pid}-${crypto.randomUUID().slice(0, 8)}`);
|
|
72
90
|
writeFileSync(passwordFile, `${EMBEDDED_PASSWORD}\n`, { mode: 0o600 });
|
|
73
91
|
try {
|
|
74
|
-
|
|
92
|
+
if (context.owner !== undefined) chownSync(passwordFile, context.owner.uid, context.owner.gid);
|
|
93
|
+
const result = await context.runAsOwner(initdb, [
|
|
75
94
|
"-D", dataDir,
|
|
76
95
|
"-U", EMBEDDED_USER,
|
|
77
96
|
"-A", "password",
|
|
@@ -87,8 +106,13 @@ async function initializeCluster(initdb: string, dataDir: string): Promise<void>
|
|
|
87
106
|
}
|
|
88
107
|
}
|
|
89
108
|
|
|
90
|
-
async function startCluster(
|
|
91
|
-
|
|
109
|
+
async function startCluster(
|
|
110
|
+
pgCtl: string,
|
|
111
|
+
dataDir: string,
|
|
112
|
+
logFile: string,
|
|
113
|
+
context: EmbeddedPostgresRunContext,
|
|
114
|
+
): Promise<void> {
|
|
115
|
+
const result = await context.runAsOwner(pgCtl, [
|
|
92
116
|
"-D", dataDir,
|
|
93
117
|
"-l", logFile,
|
|
94
118
|
"-o", `-p ${EMBEDDED_PORT} -c listen_addresses=${EMBEDDED_HOST}`,
|
|
@@ -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: {
|
|
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
|
-
|
|
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 {
|
|
@@ -119,10 +119,16 @@ export function getReadyDbosBackendSync(): DbosDurableBackend | undefined {
|
|
|
119
119
|
|
|
120
120
|
export async function shutdownDbos(): Promise<void> {
|
|
121
121
|
if (shutdownPromise !== undefined) return await shutdownPromise;
|
|
122
|
-
|
|
122
|
+
const configuredPromise = configured;
|
|
123
|
+
if (configuredPromise === undefined) return;
|
|
123
124
|
shutdownPromise = (async () => {
|
|
124
|
-
|
|
125
|
-
|
|
125
|
+
// A backend that never reached "ready" has nothing to flush or stop.
|
|
126
|
+
// `configured`/`launchPromise` memoize rejections, so re-awaiting them
|
|
127
|
+
// unguarded would rethrow the original provisioning failure out of every
|
|
128
|
+
// session dispose — crashing otherwise-successful runs at process exit.
|
|
129
|
+
const durability = await configuredPromise.catch(() => undefined);
|
|
130
|
+
if (durability === undefined) return;
|
|
131
|
+
if (launchPromise !== undefined) await launchPromise.catch(() => undefined);
|
|
126
132
|
if (state !== "ready") return;
|
|
127
133
|
state = "shutting_down";
|
|
128
134
|
await durability.backend.flush();
|