@bastani/atomic 0.9.8 → 0.9.9-alpha.1
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 +28 -0
- package/dist/builtin/cursor/CHANGELOG.md +6 -0
- package/dist/builtin/cursor/package.json +2 -2
- package/dist/builtin/intercom/CHANGELOG.md +6 -0
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/CHANGELOG.md +6 -0
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/CHANGELOG.md +6 -0
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/subagents/src/runs/background/async-execution-chain.ts +2 -0
- package/dist/builtin/subagents/src/runs/background/async-execution-common.ts +7 -1
- package/dist/builtin/subagents/src/runs/background/async-execution-single.ts +2 -0
- package/dist/builtin/subagents/src/runs/background/async-execution-types.ts +8 -2
- package/dist/builtin/subagents/src/runs/foreground/chain-execution-parallel-runner.ts +2 -0
- package/dist/builtin/subagents/src/runs/foreground/chain-execution-sequential-step.ts +2 -0
- package/dist/builtin/subagents/src/runs/foreground/execution-attempt.ts +2 -1
- package/dist/builtin/subagents/src/runs/foreground/subagent-executor-async.ts +2 -0
- package/dist/builtin/subagents/src/runs/foreground/subagent-executor-parallel-task.ts +2 -0
- package/dist/builtin/subagents/src/runs/foreground/subagent-executor-resume.ts +2 -0
- package/dist/builtin/subagents/src/runs/foreground/subagent-executor-single.ts +2 -0
- package/dist/builtin/subagents/src/runs/shared/pi-args.ts +6 -0
- package/dist/builtin/subagents/src/shared/types-config.ts +4 -0
- package/dist/builtin/subagents/src/shared/types-depth.ts +29 -2
- package/dist/builtin/web-access/CHANGELOG.md +6 -0
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +22 -0
- package/dist/builtin/workflows/README.md +17 -8
- package/dist/builtin/workflows/builtin/goal-prompts.ts +8 -2
- package/dist/builtin/workflows/builtin/open-claude-design-utils.ts +26 -9
- package/dist/builtin/workflows/builtin/ralph-forked-prompts.ts +1 -1
- package/dist/builtin/workflows/builtin/ralph-reviewer-prompt.ts +8 -3
- package/dist/builtin/workflows/builtin/ralph-runner.ts +2 -0
- package/dist/builtin/workflows/builtin/shared-prompts.ts +24 -1
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/src/durable/backend.ts +53 -15
- package/dist/builtin/workflows/src/durable/completed-catalog.ts +245 -0
- package/dist/builtin/workflows/src/durable/completed-inspection.ts +213 -0
- package/dist/builtin/workflows/src/durable/dbos-backend.ts +120 -170
- package/dist/builtin/workflows/src/durable/dbos-envelope.ts +84 -8
- package/dist/builtin/workflows/src/durable/dbos-metadata.ts +98 -0
- package/dist/builtin/workflows/src/durable/dbos-tombstone.ts +27 -0
- package/dist/builtin/workflows/src/durable/factory.ts +1 -1
- package/dist/builtin/workflows/src/durable/file-backend.ts +247 -241
- package/dist/builtin/workflows/src/durable/file-lock.ts +153 -0
- package/dist/builtin/workflows/src/durable/file-state.ts +104 -0
- package/dist/builtin/workflows/src/durable/format-version.ts +12 -0
- package/dist/builtin/workflows/src/durable/index.ts +12 -0
- package/dist/builtin/workflows/src/durable/resume-catalog.ts +16 -27
- package/dist/builtin/workflows/src/durable/resume-eligibility.ts +20 -0
- package/dist/builtin/workflows/src/durable/resume-runtime.ts +29 -29
- package/dist/builtin/workflows/src/durable/scoped-backend.ts +20 -3
- package/dist/builtin/workflows/src/durable/stage-primitive.ts +29 -8
- package/dist/builtin/workflows/src/durable/types.ts +3 -0
- package/dist/builtin/workflows/src/engine/primitives/task.ts +1 -0
- package/dist/builtin/workflows/src/engine/run.ts +14 -14
- package/dist/builtin/workflows/src/extension/extension-lifecycle.ts +39 -9
- package/dist/builtin/workflows/src/extension/extension-runtime-state.ts +10 -0
- package/dist/builtin/workflows/src/extension/runtime-durable-resume.ts +133 -0
- package/dist/builtin/workflows/src/extension/runtime.ts +15 -40
- package/dist/builtin/workflows/src/extension/wiring.ts +9 -6
- package/dist/builtin/workflows/src/extension/workflow-durable-resume-command.ts +226 -0
- package/dist/builtin/workflows/src/extension/workflow-prompts.ts +1 -0
- package/dist/builtin/workflows/src/extension/workflow-run-control-command.ts +66 -112
- package/dist/builtin/workflows/src/extension/workflow-schema.ts +5 -5
- package/dist/builtin/workflows/src/extension/workflow-tool-control.ts +16 -1
- package/dist/builtin/workflows/src/runs/foreground/executor-direct-helpers.ts +50 -43
- package/dist/builtin/workflows/src/runs/foreground/executor-direct-output.ts +91 -0
- package/dist/builtin/workflows/src/runs/foreground/executor-direct.ts +95 -46
- package/dist/builtin/workflows/src/runs/foreground/executor-stage-call.ts +3 -1
- package/dist/builtin/workflows/src/runs/foreground/executor-task-prompts.ts +1 -1
- package/dist/builtin/workflows/src/runs/foreground/executor-types.ts +3 -0
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-controller.ts +1 -1
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-options.ts +36 -7
- package/dist/builtin/workflows/src/runs/shared/worktree-cache-lifecycle.ts +25 -0
- package/dist/builtin/workflows/src/runs/shared/worktree-cwd.ts +103 -0
- package/dist/builtin/workflows/src/runs/shared/worktree-generation.ts +57 -0
- package/dist/builtin/workflows/src/runs/shared/worktree-git.ts +134 -8
- package/dist/builtin/workflows/src/runs/shared/worktree.ts +2 -0
- package/dist/builtin/workflows/src/shared/resumable-workflow-notices.ts +43 -0
- package/dist/builtin/workflows/src/shared/timing.ts +4 -0
- package/dist/builtin/workflows/src/shared/types.ts +5 -3
- package/dist/builtin/workflows/src/tui/graph-view-constants.ts +1 -0
- package/dist/builtin/workflows/src/tui/graph-view-input.ts +24 -14
- package/dist/builtin/workflows/src/tui/workflow-resume-selector.ts +43 -13
- package/dist/core/agent-session-auto-compaction.d.ts.map +1 -1
- package/dist/core/agent-session-auto-compaction.js +5 -7
- package/dist/core/agent-session-auto-compaction.js.map +1 -1
- package/dist/core/agent-session-compaction.d.ts +6 -34
- package/dist/core/agent-session-compaction.d.ts.map +1 -1
- package/dist/core/agent-session-compaction.js +95 -233
- package/dist/core/agent-session-compaction.js.map +1 -1
- package/dist/core/agent-session-message-queue.d.ts +0 -4
- package/dist/core/agent-session-message-queue.d.ts.map +1 -1
- package/dist/core/agent-session-message-queue.js +1 -5
- package/dist/core/agent-session-message-queue.js.map +1 -1
- package/dist/core/agent-session-methods.d.ts +5 -6
- package/dist/core/agent-session-methods.d.ts.map +1 -1
- package/dist/core/agent-session-methods.js.map +1 -1
- package/dist/core/agent-session-types.d.ts +2 -12
- package/dist/core/agent-session-types.d.ts.map +1 -1
- package/dist/core/agent-session-types.js.map +1 -1
- package/dist/core/compaction/branch-summarization.d.ts +1 -1
- package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
- package/dist/core/compaction/branch-summarization.js +1 -2
- package/dist/core/compaction/branch-summarization.js.map +1 -1
- package/dist/core/compaction/compaction-boundary.d.ts +9 -0
- package/dist/core/compaction/compaction-boundary.d.ts.map +1 -0
- package/dist/core/compaction/compaction-boundary.js +115 -0
- package/dist/core/compaction/compaction-boundary.js.map +1 -0
- package/dist/core/compaction/compaction-parameters.d.ts +5 -0
- package/dist/core/compaction/compaction-parameters.d.ts.map +1 -0
- package/dist/core/compaction/compaction-parameters.js +27 -0
- package/dist/core/compaction/compaction-parameters.js.map +1 -0
- package/dist/core/compaction/compaction-runner.d.ts +14 -0
- package/dist/core/compaction/compaction-runner.d.ts.map +1 -0
- package/dist/core/compaction/compaction-runner.js +29 -0
- package/dist/core/compaction/compaction-runner.js.map +1 -0
- package/dist/core/compaction/compaction-types.d.ts +79 -0
- package/dist/core/compaction/compaction-types.d.ts.map +1 -0
- package/dist/core/compaction/compaction-types.js +6 -0
- package/dist/core/compaction/compaction-types.js.map +1 -0
- package/dist/core/compaction/deleted-ranges.d.ts +6 -0
- package/dist/core/compaction/deleted-ranges.d.ts.map +1 -0
- package/dist/core/compaction/deleted-ranges.js +134 -0
- package/dist/core/compaction/deleted-ranges.js.map +1 -0
- package/dist/core/compaction/index.d.ts +7 -1
- package/dist/core/compaction/index.d.ts.map +1 -1
- package/dist/core/compaction/index.js +7 -1
- package/dist/core/compaction/index.js.map +1 -1
- package/dist/core/compaction/range-planner.d.ts +21 -0
- package/dist/core/compaction/range-planner.d.ts.map +1 -0
- package/dist/core/compaction/range-planner.js +145 -0
- package/dist/core/compaction/range-planner.js.map +1 -0
- package/dist/core/compaction/transcript-serialization.d.ts +11 -0
- package/dist/core/compaction/transcript-serialization.d.ts.map +1 -0
- package/dist/core/compaction/transcript-serialization.js +104 -0
- package/dist/core/compaction/transcript-serialization.js.map +1 -0
- package/dist/core/extensions/context-types.d.ts +2 -2
- package/dist/core/extensions/context-types.d.ts.map +1 -1
- package/dist/core/extensions/context-types.js.map +1 -1
- package/dist/core/extensions/event-results.d.ts +2 -2
- package/dist/core/extensions/event-results.d.ts.map +1 -1
- package/dist/core/extensions/event-results.js.map +1 -1
- package/dist/core/extensions/session-events.d.ts +7 -7
- package/dist/core/extensions/session-events.d.ts.map +1 -1
- package/dist/core/extensions/session-events.js.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/messages.d.ts +4 -0
- package/dist/core/messages.d.ts.map +1 -1
- package/dist/core/messages.js +11 -0
- package/dist/core/messages.js.map +1 -1
- package/dist/core/provider-context-usage.d.ts.map +1 -1
- package/dist/core/provider-context-usage.js +1 -2
- package/dist/core/provider-context-usage.js.map +1 -1
- package/dist/core/session-manager-archive.d.ts +2 -1
- package/dist/core/session-manager-archive.d.ts.map +1 -1
- package/dist/core/session-manager-archive.js +2 -2
- package/dist/core/session-manager-archive.js.map +1 -1
- package/dist/core/session-manager-classification.d.ts +9 -0
- package/dist/core/session-manager-classification.d.ts.map +1 -0
- package/dist/core/session-manager-classification.js +37 -0
- package/dist/core/session-manager-classification.js.map +1 -0
- package/dist/core/session-manager-core.d.ts +4 -4
- package/dist/core/session-manager-core.d.ts.map +1 -1
- package/dist/core/session-manager-core.js +13 -8
- package/dist/core/session-manager-core.js.map +1 -1
- package/dist/core/session-manager-entries.d.ts +4 -3
- package/dist/core/session-manager-entries.d.ts.map +1 -1
- package/dist/core/session-manager-entries.js +12 -10
- package/dist/core/session-manager-entries.js.map +1 -1
- package/dist/core/session-manager-history.d.ts +6 -23
- package/dist/core/session-manager-history.d.ts.map +1 -1
- package/dist/core/session-manager-history.js +23 -256
- package/dist/core/session-manager-history.js.map +1 -1
- package/dist/core/session-manager-list.d.ts.map +1 -1
- package/dist/core/session-manager-list.js +3 -3
- package/dist/core/session-manager-list.js.map +1 -1
- package/dist/core/session-manager-storage.d.ts +1 -1
- package/dist/core/session-manager-storage.d.ts.map +1 -1
- package/dist/core/session-manager-storage.js +3 -2
- package/dist/core/session-manager-storage.js.map +1 -1
- package/dist/core/session-manager-types.d.ts +14 -15
- 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 +3 -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/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +1 -1
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/main-session.d.ts +1 -0
- package/dist/main-session.d.ts.map +1 -1
- package/dist/main-session.js +7 -0
- package/dist/main-session.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +2 -2
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/chat-message-renderer.d.ts +4 -1
- package/dist/modes/interactive/components/chat-message-renderer.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-message-renderer.js +12 -22
- package/dist/modes/interactive/components/chat-message-renderer.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 +5 -11
- package/dist/modes/interactive/components/chat-session-host-events.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-rendering.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-rendering.js +3 -0
- package/dist/modes/interactive/components/chat-session-host-rendering.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-state.d.ts +1 -0
- package/dist/modes/interactive/components/chat-session-host-state.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-state.js +1 -0
- package/dist/modes/interactive/components/chat-session-host-state.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-utils.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-utils.js +1 -1
- package/dist/modes/interactive/components/chat-session-host-utils.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host.js +1 -0
- package/dist/modes/interactive/components/chat-session-host.js.map +1 -1
- package/dist/modes/interactive/components/compaction-boundary-message.d.ts +20 -0
- package/dist/modes/interactive/components/compaction-boundary-message.d.ts.map +1 -0
- package/dist/modes/interactive/components/compaction-boundary-message.js +48 -0
- package/dist/modes/interactive/components/compaction-boundary-message.js.map +1 -0
- package/dist/modes/interactive/components/index.d.ts +1 -1
- package/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/dist/modes/interactive/components/index.js +1 -1
- package/dist/modes/interactive/components/index.js.map +1 -1
- package/dist/modes/interactive/components/session-selector-list.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector-list.js +3 -0
- package/dist/modes/interactive/components/session-selector-list.js.map +1 -1
- package/dist/modes/interactive/interactive-agent-events.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-agent-events.js +3 -47
- package/dist/modes/interactive/interactive-agent-events.js.map +1 -1
- package/dist/modes/interactive/interactive-mode-deps.d.ts +2 -2
- package/dist/modes/interactive/interactive-mode-deps.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode-deps.js +1 -1
- package/dist/modes/interactive/interactive-mode-deps.js.map +1 -1
- package/dist/modes/interactive/interactive-mode-surface.d.ts +5 -3
- package/dist/modes/interactive/interactive-mode-surface.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode-surface.js.map +1 -1
- package/dist/modes/interactive/interactive-render-chat.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-render-chat.js +21 -12
- package/dist/modes/interactive/interactive-render-chat.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +3 -5
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +1 -6
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-command-handler.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-command-handler.js +0 -4
- package/dist/modes/rpc/rpc-command-handler.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +2 -11
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/docs/compaction.md +97 -772
- package/docs/extensions.md +25 -29
- package/docs/json.md +2 -2
- package/docs/rpc.md +32 -24
- package/docs/sdk.md +3 -3
- package/docs/session-format.md +49 -30
- package/docs/sessions.md +5 -3
- package/docs/settings.md +14 -4
- package/docs/usage.md +1 -1
- package/docs/workflows.md +30 -15
- package/examples/extensions/custom-compaction.ts +14 -58
- package/examples/extensions/handoff.ts +2 -3
- package/npm-shrinkwrap.json +23 -23
- package/package.json +2 -2
- package/dist/core/compaction/context-assistant-turns.d.ts +0 -42
- package/dist/core/compaction/context-assistant-turns.d.ts.map +0 -1
- package/dist/core/compaction/context-assistant-turns.js +0 -87
- package/dist/core/compaction/context-assistant-turns.js.map +0 -1
- package/dist/core/compaction/context-compaction-critical.d.ts +0 -15
- package/dist/core/compaction/context-compaction-critical.d.ts.map +0 -1
- package/dist/core/compaction/context-compaction-critical.js +0 -57
- package/dist/core/compaction/context-compaction-critical.js.map +0 -1
- package/dist/core/compaction/context-compaction-eviction-alternates.d.ts +0 -18
- package/dist/core/compaction/context-compaction-eviction-alternates.d.ts.map +0 -1
- package/dist/core/compaction/context-compaction-eviction-alternates.js +0 -186
- package/dist/core/compaction/context-compaction-eviction-alternates.js.map +0 -1
- package/dist/core/compaction/context-compaction-eviction.d.ts +0 -12
- package/dist/core/compaction/context-compaction-eviction.d.ts.map +0 -1
- package/dist/core/compaction/context-compaction-eviction.js +0 -222
- package/dist/core/compaction/context-compaction-eviction.js.map +0 -1
- package/dist/core/compaction/context-compaction-metrics.d.ts +0 -28
- package/dist/core/compaction/context-compaction-metrics.d.ts.map +0 -1
- package/dist/core/compaction/context-compaction-metrics.js +0 -107
- package/dist/core/compaction/context-compaction-metrics.js.map +0 -1
- package/dist/core/compaction/context-compaction-prompt.d.ts +0 -9
- package/dist/core/compaction/context-compaction-prompt.d.ts.map +0 -1
- package/dist/core/compaction/context-compaction-prompt.js +0 -163
- package/dist/core/compaction/context-compaction-prompt.js.map +0 -1
- package/dist/core/compaction/context-compaction-runner.d.ts +0 -11
- package/dist/core/compaction/context-compaction-runner.d.ts.map +0 -1
- package/dist/core/compaction/context-compaction-runner.js +0 -273
- package/dist/core/compaction/context-compaction-runner.js.map +0 -1
- package/dist/core/compaction/context-compaction-strategy.d.ts +0 -5
- package/dist/core/compaction/context-compaction-strategy.d.ts.map +0 -1
- package/dist/core/compaction/context-compaction-strategy.js +0 -27
- package/dist/core/compaction/context-compaction-strategy.js.map +0 -1
- package/dist/core/compaction/context-compaction-types.d.ts +0 -75
- package/dist/core/compaction/context-compaction-types.d.ts.map +0 -1
- package/dist/core/compaction/context-compaction-types.js +0 -6
- package/dist/core/compaction/context-compaction-types.js.map +0 -1
- package/dist/core/compaction/context-compaction.d.ts +0 -10
- package/dist/core/compaction/context-compaction.d.ts.map +0 -1
- package/dist/core/compaction/context-compaction.js +0 -8
- package/dist/core/compaction/context-compaction.js.map +0 -1
- package/dist/core/compaction/context-deletion-application.d.ts +0 -14
- package/dist/core/compaction/context-deletion-application.d.ts.map +0 -1
- package/dist/core/compaction/context-deletion-application.js +0 -261
- package/dist/core/compaction/context-deletion-application.js.map +0 -1
- package/dist/core/compaction/context-deletion-store.d.ts +0 -78
- package/dist/core/compaction/context-deletion-store.d.ts.map +0 -1
- package/dist/core/compaction/context-deletion-store.js +0 -162
- package/dist/core/compaction/context-deletion-store.js.map +0 -1
- package/dist/core/compaction/context-deletion-targets.d.ts +0 -43
- package/dist/core/compaction/context-deletion-targets.d.ts.map +0 -1
- package/dist/core/compaction/context-deletion-targets.js +0 -292
- package/dist/core/compaction/context-deletion-targets.js.map +0 -1
- package/dist/core/compaction/context-deletion-tool-definitions.d.ts +0 -193
- package/dist/core/compaction/context-deletion-tool-definitions.d.ts.map +0 -1
- package/dist/core/compaction/context-deletion-tool-definitions.js +0 -98
- package/dist/core/compaction/context-deletion-tool-definitions.js.map +0 -1
- package/dist/core/compaction/context-deletion-tool-helpers.d.ts +0 -19
- package/dist/core/compaction/context-deletion-tool-helpers.d.ts.map +0 -1
- package/dist/core/compaction/context-deletion-tool-helpers.js +0 -210
- package/dist/core/compaction/context-deletion-tool-helpers.js.map +0 -1
- package/dist/core/compaction/context-deletion-tools.d.ts +0 -4
- package/dist/core/compaction/context-deletion-tools.d.ts.map +0 -1
- package/dist/core/compaction/context-deletion-tools.js +0 -403
- package/dist/core/compaction/context-deletion-tools.js.map +0 -1
- package/dist/core/compaction/context-transcript-analysis.d.ts +0 -11
- package/dist/core/compaction/context-transcript-analysis.d.ts.map +0 -1
- package/dist/core/compaction/context-transcript-analysis.js +0 -228
- package/dist/core/compaction/context-transcript-analysis.js.map +0 -1
- package/dist/core/session-manager-tool-dependencies.d.ts +0 -10
- package/dist/core/session-manager-tool-dependencies.d.ts.map +0 -1
- package/dist/core/session-manager-tool-dependencies.js +0 -133
- package/dist/core/session-manager-tool-dependencies.js.map +0 -1
- package/dist/modes/interactive/components/context-compaction-summary-message.d.ts +0 -17
- package/dist/modes/interactive/components/context-compaction-summary-message.d.ts.map +0 -1
- package/dist/modes/interactive/components/context-compaction-summary-message.js +0 -83
- package/dist/modes/interactive/components/context-compaction-summary-message.js.map +0 -1
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { hostname } from "node:os";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import type { FileDurableState } from "./file-state.js";
|
|
5
|
+
|
|
6
|
+
interface LockOwner {
|
|
7
|
+
readonly pid: number;
|
|
8
|
+
readonly host: string;
|
|
9
|
+
readonly token: string;
|
|
10
|
+
readonly acquiredAt: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const LOCK_OWNER_FILE = "owner.json";
|
|
14
|
+
const STALE_LOCK_MS = 30_000;
|
|
15
|
+
|
|
16
|
+
export function writeDurableFileState(filePath: string, state: FileDurableState): void {
|
|
17
|
+
const dir = dirname(filePath);
|
|
18
|
+
ensureSecureDir(dir);
|
|
19
|
+
const tmp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
20
|
+
writeFileSync(tmp, JSON.stringify(state), { encoding: "utf-8", mode: 0o600 });
|
|
21
|
+
chmodBestEffort(tmp, 0o600);
|
|
22
|
+
renameSync(tmp, filePath);
|
|
23
|
+
chmodBestEffort(filePath, 0o600);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function withDurableFileLock<T>(filePath: string, fn: () => T): T {
|
|
27
|
+
const dir = dirname(filePath);
|
|
28
|
+
ensureSecureDir(dir);
|
|
29
|
+
const lockDir = `${filePath}.lock`;
|
|
30
|
+
const deadline = Date.now() + 5000;
|
|
31
|
+
while (true) {
|
|
32
|
+
try {
|
|
33
|
+
mkdirSync(lockDir, { mode: 0o700 });
|
|
34
|
+
try {
|
|
35
|
+
chmodBestEffort(lockDir, 0o700);
|
|
36
|
+
writeLockOwner(lockDir);
|
|
37
|
+
} catch (ownerErr) {
|
|
38
|
+
rmSync(lockDir, { recursive: true, force: true });
|
|
39
|
+
throw ownerErr;
|
|
40
|
+
}
|
|
41
|
+
break;
|
|
42
|
+
} catch (err) {
|
|
43
|
+
if (errorCode(err) !== "EEXIST") throw err;
|
|
44
|
+
if (tryReclaimStaleLock(lockDir, STALE_LOCK_MS)) continue;
|
|
45
|
+
if (Date.now() > deadline) throw new Error(`Timed out acquiring durable workflow state lock: ${lockDir}`);
|
|
46
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
return fn();
|
|
51
|
+
} finally {
|
|
52
|
+
rmSync(lockDir, { recursive: true, force: true });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function tryReclaimStaleLock(lockDir: string, staleMs: number): boolean {
|
|
57
|
+
if (!isStaleLock(lockDir, staleMs)) return false;
|
|
58
|
+
const owner = readLockOwner(lockDir);
|
|
59
|
+
if (owner === undefined || !isLockOwnerAbandoned(owner)) return false;
|
|
60
|
+
const quarantine = `${lockDir}.stale.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}`;
|
|
61
|
+
try {
|
|
62
|
+
renameSync(lockDir, quarantine);
|
|
63
|
+
} catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
const quarantinedOwner = readLockOwner(quarantine);
|
|
67
|
+
if (!sameLockOwner(owner, quarantinedOwner)) {
|
|
68
|
+
restoreQuarantinedLock(lockDir, quarantine);
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
rmSync(quarantine, { recursive: true, force: true });
|
|
73
|
+
} catch {
|
|
74
|
+
// The stale lock has already been moved away from the active lock path.
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isStaleLock(lockDir: string, staleMs: number): boolean {
|
|
80
|
+
try {
|
|
81
|
+
return Date.now() - statSync(lockDir).mtimeMs > staleMs;
|
|
82
|
+
} catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function writeLockOwner(lockDir: string): void {
|
|
88
|
+
const owner: LockOwner = {
|
|
89
|
+
pid: process.pid,
|
|
90
|
+
host: hostname(),
|
|
91
|
+
token: `${process.pid}:${Date.now()}:${Math.random().toString(36).slice(2)}`,
|
|
92
|
+
acquiredAt: Date.now(),
|
|
93
|
+
};
|
|
94
|
+
const ownerPath = `${lockDir}/${LOCK_OWNER_FILE}`;
|
|
95
|
+
writeFileSync(ownerPath, JSON.stringify(owner), { encoding: "utf-8", mode: 0o600 });
|
|
96
|
+
chmodBestEffort(ownerPath, 0o600);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function readLockOwner(lockDir: string): LockOwner | undefined {
|
|
100
|
+
try {
|
|
101
|
+
const parsed = JSON.parse(readFileSync(`${lockDir}/${LOCK_OWNER_FILE}`, "utf-8"));
|
|
102
|
+
return isLockOwner(parsed) ? parsed : undefined;
|
|
103
|
+
} catch {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isLockOwner(value: unknown): value is LockOwner {
|
|
109
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
110
|
+
const record = value as Partial<LockOwner>;
|
|
111
|
+
return typeof record.pid === "number" && typeof record.host === "string"
|
|
112
|
+
&& typeof record.token === "string" && typeof record.acquiredAt === "number";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isLockOwnerAbandoned(owner: LockOwner): boolean {
|
|
116
|
+
if (owner.host !== hostname() || !Number.isInteger(owner.pid) || owner.pid <= 0) return false;
|
|
117
|
+
try {
|
|
118
|
+
process.kill(owner.pid, 0);
|
|
119
|
+
return false;
|
|
120
|
+
} catch (err) {
|
|
121
|
+
return errorCode(err) === "ESRCH";
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function sameLockOwner(a: LockOwner, b: LockOwner | undefined): boolean {
|
|
126
|
+
return b !== undefined && a.pid === b.pid && a.host === b.host && a.token === b.token;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function restoreQuarantinedLock(lockDir: string, quarantine: string): void {
|
|
130
|
+
try {
|
|
131
|
+
if (!existsSync(lockDir)) renameSync(quarantine, lockDir);
|
|
132
|
+
else rmSync(quarantine, { recursive: true, force: true });
|
|
133
|
+
} catch {
|
|
134
|
+
// Best-effort: never delete the active lock path after a failed compare.
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function errorCode(err: unknown): string | undefined {
|
|
139
|
+
return typeof err === "object" && err !== null && "code" in err ? String(err.code) : undefined;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function ensureSecureDir(dir: string): void {
|
|
143
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
144
|
+
chmodBestEffort(dir, 0o700);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function chmodBestEffort(path: string, mode: number): void {
|
|
148
|
+
try {
|
|
149
|
+
chmodSync(path, mode);
|
|
150
|
+
} catch {
|
|
151
|
+
// chmod is unavailable or unsupported on some filesystems/platforms.
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import type { WorkflowSerializableValue } from "../shared/types.js";
|
|
3
|
+
import type { DurableCheckpoint, DurableWorkflowHandle } from "./types.js";
|
|
4
|
+
import { classifyDurableFormatVersion, DURABLE_FORMAT_VERSION } from "./format-version.js";
|
|
5
|
+
|
|
6
|
+
export interface FileDurableRecord {
|
|
7
|
+
readonly handle: DurableWorkflowHandle;
|
|
8
|
+
readonly checkpoints: readonly DurableCheckpoint[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface FileDurableState {
|
|
12
|
+
readonly version: number;
|
|
13
|
+
readonly workflows: readonly FileDurableRecord[];
|
|
14
|
+
readonly deletedWorkflowIds: readonly string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type FileStateReadResult =
|
|
18
|
+
| { readonly kind: "missing" }
|
|
19
|
+
| { readonly kind: "current"; readonly state: FileDurableState }
|
|
20
|
+
| { readonly kind: "legacy"; readonly workflowIds: readonly string[] }
|
|
21
|
+
| { readonly kind: "unknown" };
|
|
22
|
+
|
|
23
|
+
export function readDurableFileState(filePath: string): FileStateReadResult {
|
|
24
|
+
if (!existsSync(filePath)) return { kind: "missing" };
|
|
25
|
+
try {
|
|
26
|
+
const parsed = JSON.parse(readFileSync(filePath, "utf-8")) as Partial<FileDurableState> | null;
|
|
27
|
+
if (parsed === null || !Array.isArray(parsed.workflows)) return { kind: "unknown" };
|
|
28
|
+
const compatibility = classifyDurableFormatVersion(parsed.version);
|
|
29
|
+
if (compatibility === "legacy") {
|
|
30
|
+
const validRecords = parsed.workflows.filter(isFileDurableRecord);
|
|
31
|
+
return {
|
|
32
|
+
kind: "legacy",
|
|
33
|
+
workflowIds: validRecords.length === parsed.workflows.length
|
|
34
|
+
? validRecords.map((record) => record.handle.workflowId)
|
|
35
|
+
: [],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const deleted = parsed.deletedWorkflowIds ?? [];
|
|
39
|
+
if (compatibility === "unknown" || !isStringArray(deleted) || !parsed.workflows.every(isFileDurableRecord)) return { kind: "unknown" };
|
|
40
|
+
return { kind: "current", state: { version: DURABLE_FORMAT_VERSION, workflows: parsed.workflows, deletedWorkflowIds: deleted } };
|
|
41
|
+
} catch {
|
|
42
|
+
return { kind: "unknown" };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isFileDurableRecord(value: unknown): value is FileDurableRecord {
|
|
47
|
+
if (!isObject(value)) return false;
|
|
48
|
+
const handle = value["handle"];
|
|
49
|
+
const checkpoints = value["checkpoints"];
|
|
50
|
+
if (!isHandle(handle) || !Array.isArray(checkpoints)) return false;
|
|
51
|
+
return checkpoints.every((checkpoint) => isCheckpoint(checkpoint) && checkpoint.workflowId === handle.workflowId);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isHandle(value: unknown): value is DurableWorkflowHandle {
|
|
55
|
+
if (!isObject(value) || typeof value["workflowId"] !== "string" || typeof value["name"] !== "string"
|
|
56
|
+
|| !isSerializableObject(value["inputs"]) || typeof value["createdAt"] !== "number"
|
|
57
|
+
|| typeof value["updatedAt"] !== "number" || !isStatus(value["status"])
|
|
58
|
+
|| typeof value["completedCheckpoints"] !== "number" || typeof value["pendingPrompts"] !== "number") return false;
|
|
59
|
+
return optionalString(value, "invocationCwd") && optionalString(value, "workflowCwd")
|
|
60
|
+
&& optionalString(value, "repositoryRoot") && optionalString(value, "gitWorktreeRoot")
|
|
61
|
+
&& optionalString(value, "sessionFile") && optionalString(value, "label")
|
|
62
|
+
&& optionalString(value, "rootWorkflowId") && optionalBoolean(value, "resumable");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isCheckpoint(value: unknown): value is DurableCheckpoint {
|
|
66
|
+
if (!isObject(value) || typeof value["workflowId"] !== "string" || typeof value["checkpointId"] !== "string"
|
|
67
|
+
|| typeof value["completedAt"] !== "number") return false;
|
|
68
|
+
if (value["kind"] === "tool") return typeof value["name"] === "string" && typeof value["argsHash"] === "string" && isSerializable(value["output"]);
|
|
69
|
+
if (value["kind"] === "ui") return typeof value["promptKind"] === "string" && typeof value["message"] === "string"
|
|
70
|
+
&& typeof value["promptHash"] === "string" && isSerializable(value["response"]);
|
|
71
|
+
return value["kind"] === "stage" && typeof value["name"] === "string" && typeof value["replayKey"] === "string"
|
|
72
|
+
&& (!("output" in value) || isSerializable(value["output"]));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isStringArray(value: unknown): value is readonly string[] {
|
|
76
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
77
|
+
}
|
|
78
|
+
function isStatus(value: unknown): boolean {
|
|
79
|
+
return value === "running" || value === "paused" || value === "completed"
|
|
80
|
+
|| value === "failed" || value === "cancelled" || value === "blocked";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isSerializableObject(value: unknown): value is Readonly<Record<string, WorkflowSerializableValue>> {
|
|
84
|
+
return isObject(value) && Object.values(value).every(isSerializable);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isSerializable(value: unknown): value is WorkflowSerializableValue {
|
|
88
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
89
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
90
|
+
if (Array.isArray(value)) return value.every(isSerializable);
|
|
91
|
+
return isSerializableObject(value);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
95
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function optionalString(value: Record<string, unknown>, key: string): boolean {
|
|
99
|
+
return value[key] === undefined || typeof value[key] === "string";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function optionalBoolean(value: Record<string, unknown>, key: string): boolean {
|
|
103
|
+
return value[key] === undefined || typeof value[key] === "boolean";
|
|
104
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Shared schema version for durable workflow state and discovery metadata. */
|
|
2
|
+
export const DURABLE_FORMAT_VERSION = 2 as const;
|
|
3
|
+
export const LEGACY_DURABLE_FORMAT_VERSION = 1 as const;
|
|
4
|
+
|
|
5
|
+
export type DurableFormatCompatibility = "current" | "legacy" | "unknown";
|
|
6
|
+
|
|
7
|
+
/** Classify an explicit durable format version without guessing about malformed data. */
|
|
8
|
+
export function classifyDurableFormatVersion(value: unknown): DurableFormatCompatibility {
|
|
9
|
+
if (value === DURABLE_FORMAT_VERSION) return "current";
|
|
10
|
+
if (value === LEGACY_DURABLE_FORMAT_VERSION) return "legacy";
|
|
11
|
+
return "unknown";
|
|
12
|
+
}
|
|
@@ -49,6 +49,18 @@ export {
|
|
|
49
49
|
persistDurableCacheEntry,
|
|
50
50
|
formatResumableWorkflowList,
|
|
51
51
|
} from "./resume-catalog.js";
|
|
52
|
+
export {
|
|
53
|
+
completedWorkflowSnapshot,
|
|
54
|
+
listCompletedFromBackend,
|
|
55
|
+
listOpenableCompletedWorkflows,
|
|
56
|
+
resolveCompletedWorkflow,
|
|
57
|
+
type CompletedWorkflowResolution,
|
|
58
|
+
} from "./completed-catalog.js";
|
|
59
|
+
export {
|
|
60
|
+
openCompletedDurableWorkflow,
|
|
61
|
+
type OpenCompletedDurableDeps,
|
|
62
|
+
type OpenCompletedDurableResult,
|
|
63
|
+
} from "./completed-inspection.js";
|
|
52
64
|
export {
|
|
53
65
|
createToolPrimitive,
|
|
54
66
|
createCheckpointIdGenerator,
|
|
@@ -20,6 +20,8 @@ import { join } from "node:path";
|
|
|
20
20
|
import type { DurableCheckpointEntry, DurableWorkflowStatus, ResumableWorkflowEntry } from "./types.js";
|
|
21
21
|
import type { DurableWorkflowBackend } from "./backend.js";
|
|
22
22
|
import type { WorkflowSerializableValue } from "../shared/types.js";
|
|
23
|
+
import { isDurableWorkflowResumable } from "./resume-eligibility.js";
|
|
24
|
+
import { DURABLE_FORMAT_VERSION } from "./format-version.js";
|
|
23
25
|
|
|
24
26
|
// ---------------------------------------------------------------------------
|
|
25
27
|
// Session file scanning
|
|
@@ -52,7 +54,7 @@ export function scanResumableWorkflows(sessionDir: string): readonly ResumableWo
|
|
|
52
54
|
}
|
|
53
55
|
}
|
|
54
56
|
}
|
|
55
|
-
return [...entries.values()].filter(
|
|
57
|
+
return [...entries.values()].filter(isDurableWorkflowResumable).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
function readDurableEntriesFromFile(filePath: string): readonly DurableCheckpointEntry[] {
|
|
@@ -89,6 +91,8 @@ function durablePayloadFromJsonlEntry(parsed: Record<string, unknown>): Record<s
|
|
|
89
91
|
}
|
|
90
92
|
|
|
91
93
|
function parseDurableEntry(raw: Record<string, unknown>): DurableCheckpointEntry | undefined {
|
|
94
|
+
if (raw["formatVersion"] !== DURABLE_FORMAT_VERSION) return undefined;
|
|
95
|
+
|
|
92
96
|
const workflowId = raw["workflowId"];
|
|
93
97
|
const name = raw["name"];
|
|
94
98
|
const inputs = raw["inputs"];
|
|
@@ -97,6 +101,7 @@ function parseDurableEntry(raw: Record<string, unknown>): DurableCheckpointEntry
|
|
|
97
101
|
if (typeof workflowId !== "string" || typeof name !== "string" || typeof status !== "string" || typeof ts !== "number") return undefined;
|
|
98
102
|
if (!isWorkflowSerializableObject(inputs)) return undefined;
|
|
99
103
|
return {
|
|
104
|
+
formatVersion: DURABLE_FORMAT_VERSION,
|
|
100
105
|
type: "workflow.durable.checkpoint",
|
|
101
106
|
workflowId,
|
|
102
107
|
name,
|
|
@@ -155,24 +160,6 @@ function entryToResumable(entry: DurableCheckpointEntry, sessionFile: string): R
|
|
|
155
160
|
};
|
|
156
161
|
}
|
|
157
162
|
|
|
158
|
-
function isResumableStatus(status: DurableWorkflowStatus): boolean {
|
|
159
|
-
// `running`/`paused` are both resumable at the catalog level. A `running`
|
|
160
|
-
// durable handle may be a crashed process (cross-session crash recovery);
|
|
161
|
-
// same-session double-resume is filtered out by the command layer.
|
|
162
|
-
return status === "running" || status === "paused";
|
|
163
|
-
}
|
|
164
|
-
function hasResumeProgress(entry: ResumableWorkflowEntry): boolean {
|
|
165
|
-
return entry.completedCheckpoints > 0 || entry.pendingPrompts > 0;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
function isResumableEntry(entry: ResumableWorkflowEntry): boolean {
|
|
170
|
-
const isRoot = entry.rootWorkflowId === undefined || entry.rootWorkflowId === entry.workflowId;
|
|
171
|
-
if (!isRoot) return false;
|
|
172
|
-
if (entry.status === "failed" || entry.status === "blocked") return entry.resumable !== false;
|
|
173
|
-
return isResumableStatus(entry.status) && hasResumeProgress(entry);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
163
|
// ---------------------------------------------------------------------------
|
|
177
164
|
// Backend-backed catalog
|
|
178
165
|
// ---------------------------------------------------------------------------
|
|
@@ -196,6 +183,7 @@ export function persistDurableCacheEntry(
|
|
|
196
183
|
): void {
|
|
197
184
|
if (typeof persistence.appendEntry !== "function") return;
|
|
198
185
|
persistence.appendEntry("workflow.durable.checkpoint", {
|
|
186
|
+
formatVersion: entry.formatVersion,
|
|
199
187
|
workflowId: entry.workflowId,
|
|
200
188
|
name: entry.name,
|
|
201
189
|
inputs: entry.inputs as Record<string, unknown>,
|
|
@@ -217,13 +205,14 @@ export function persistDurableCacheEntry(
|
|
|
217
205
|
* Format the resumable workflow list for display in the selector.
|
|
218
206
|
*/
|
|
219
207
|
export function formatResumableWorkflowList(entries: readonly ResumableWorkflowEntry[]): string {
|
|
220
|
-
if (entries.length === 0) return "No resumable workflows found.";
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
const
|
|
224
|
-
const
|
|
225
|
-
const
|
|
226
|
-
|
|
208
|
+
if (entries.length === 0) return "No resumable or completed workflows found.";
|
|
209
|
+
const hasCompleted = entries.some((entry) => entry.status === "completed");
|
|
210
|
+
const lines = entries.map((entry, index) => {
|
|
211
|
+
const id = entry.workflowId.slice(0, 8);
|
|
212
|
+
const status = entry.status === "completed" ? "✓ completed" : entry.status.padEnd(8);
|
|
213
|
+
const checkpoints = `${entry.completedCheckpoints} checkpoint${entry.completedCheckpoints === 1 ? "" : "s"}`;
|
|
214
|
+
const label = entry.label ? ` "${entry.label}"` : "";
|
|
215
|
+
return ` ${index + 1}. ${id} ${status} ${entry.name}${label} (${checkpoints})`;
|
|
227
216
|
});
|
|
228
|
-
return
|
|
217
|
+
return `${hasCompleted ? "Workflow resume targets" : "Resumable workflows"}:\n${lines.join("\n")}`;
|
|
229
218
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { DurableWorkflowStatus } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/** Metadata shared by durable handles and session-cache resume entries. */
|
|
4
|
+
export interface DurableResumeCandidate {
|
|
5
|
+
readonly workflowId: string;
|
|
6
|
+
readonly status: DurableWorkflowStatus;
|
|
7
|
+
readonly completedCheckpoints: number;
|
|
8
|
+
readonly pendingPrompts: number;
|
|
9
|
+
readonly rootWorkflowId?: string;
|
|
10
|
+
readonly resumable?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Authoritative status/progress rules for durable workflow resume discovery. */
|
|
14
|
+
export function isDurableWorkflowResumable(candidate: DurableResumeCandidate): boolean {
|
|
15
|
+
const isRoot = candidate.rootWorkflowId === undefined || candidate.rootWorkflowId === candidate.workflowId;
|
|
16
|
+
if (!isRoot) return false;
|
|
17
|
+
if (candidate.status === "failed" || candidate.status === "blocked") return candidate.resumable !== false;
|
|
18
|
+
const hasResumeProgress = candidate.completedCheckpoints > 0 || candidate.pendingPrompts > 0;
|
|
19
|
+
return (candidate.status === "running" || candidate.status === "paused") && hasResumeProgress;
|
|
20
|
+
}
|
|
@@ -31,6 +31,7 @@ import { resolveAndValidateInputs } from "../runs/foreground/executor-inputs.js"
|
|
|
31
31
|
import { getDurableBackend } from "./factory.js";
|
|
32
32
|
import type { DurableWorkflowBackend } from "./backend.js";
|
|
33
33
|
import type { ResumableWorkflowEntry } from "./types.js";
|
|
34
|
+
import { isDurableWorkflowResumable } from "./resume-eligibility.js";
|
|
34
35
|
import { workflowDefinitionRequirementMessage } from "../runs/foreground/executor-child-helpers.js";
|
|
35
36
|
import { isWorkflowDefinition } from "../runs/foreground/executor-child-helpers.js";
|
|
36
37
|
import type { RunSnapshot } from "../shared/store-types.js";
|
|
@@ -109,6 +110,9 @@ export function resumeDurableWorkflow(
|
|
|
109
110
|
const resolvedCatalog = catalog ?? backend.listResumableWorkflows();
|
|
110
111
|
const resolved = resolveDurableEntry(workflowIdOrPrefix, resolvedCatalog);
|
|
111
112
|
if (resolved === undefined) {
|
|
113
|
+
if (!backend.isWorkflowLoadable(workflowIdOrPrefix)) {
|
|
114
|
+
return { ok: false, reason: "not_registered", message: `Durable workflow ${workflowIdOrPrefix.slice(0, 8)} uses an incompatible or unavailable persisted format and cannot be resumed.` };
|
|
115
|
+
}
|
|
112
116
|
// Not in the (filtered) resumable catalog. It may still be a workflow the
|
|
113
117
|
// backend knows about. Only surface "already running" when there is a live,
|
|
114
118
|
// actively-executing run for it in this session; otherwise a `running`
|
|
@@ -126,6 +130,9 @@ export function resumeDurableWorkflow(
|
|
|
126
130
|
message: `Ambiguous workflow prefix "${workflowIdOrPrefix}" matches: ${resolved.matches.map((m) => `${m.name} (${m.workflowId.slice(0, 8)})`).join(", ")}`,
|
|
127
131
|
};
|
|
128
132
|
}
|
|
133
|
+
if (!backend.isWorkflowLoadable(resolved.workflowId)) {
|
|
134
|
+
return { ok: false, reason: "not_registered", message: `Durable workflow ${resolved.workflowId.slice(0, 8)} uses an incompatible or unavailable persisted format and cannot be resumed.` };
|
|
135
|
+
}
|
|
129
136
|
// Authoritative backend-handle check: a cache-only entry (no handle) is
|
|
130
137
|
// "stale" regardless of its cached status. A `running` handle is only
|
|
131
138
|
// refused when there is a live, actively-executing run for it in THIS
|
|
@@ -140,15 +147,15 @@ export function resumeDurableWorkflow(
|
|
|
140
147
|
};
|
|
141
148
|
}
|
|
142
149
|
if (handle.status === "running" && hasActiveLiveRun(deps.baseRunOpts.store, resolved.workflowId)) {
|
|
143
|
-
return alreadyRunningResult(
|
|
150
|
+
return alreadyRunningResult(handle.name, resolved.workflowId, deps.baseRunOpts.store);
|
|
144
151
|
}
|
|
145
|
-
if (!
|
|
146
|
-
return { ok: false, reason: "not_resumable", message: `Workflow ${resolved.workflowId.slice(0, 8)} is ${
|
|
152
|
+
if (!isDurableWorkflowResumable(handle)) {
|
|
153
|
+
return { ok: false, reason: "not_resumable", message: `Workflow ${resolved.workflowId.slice(0, 8)} is ${handle.status}, not resumable.` };
|
|
147
154
|
}
|
|
148
155
|
|
|
149
|
-
const def = deps.registry.get(
|
|
156
|
+
const def = deps.registry.get(handle.name);
|
|
150
157
|
if (def === undefined) {
|
|
151
|
-
return { ok: false, reason: "workflow_not_found", message: `Workflow definition not found: ${
|
|
158
|
+
return { ok: false, reason: "workflow_not_found", message: `Workflow definition not found: ${handle.name}` };
|
|
152
159
|
}
|
|
153
160
|
if (!isWorkflowDefinition(def)) {
|
|
154
161
|
return { ok: false, reason: "workflow_not_found", message: workflowDefinitionRequirementMessage("resumeDurableWorkflow", def) };
|
|
@@ -180,8 +187,8 @@ export function resumeDurableWorkflow(
|
|
|
180
187
|
ok: true,
|
|
181
188
|
runId: accepted.runId,
|
|
182
189
|
workflowId: resolved.workflowId,
|
|
183
|
-
name:
|
|
184
|
-
message: `Resuming durable workflow "${
|
|
190
|
+
name: handle.name,
|
|
191
|
+
message: `Resuming durable workflow "${handle.name}" (${resolved.workflowId.slice(0, 8)}) — completed checkpoints will be replayed.`,
|
|
185
192
|
};
|
|
186
193
|
}
|
|
187
194
|
|
|
@@ -209,20 +216,6 @@ function alreadyRunningResult(name: string, workflowId: string, store: RunOpts["
|
|
|
209
216
|
};
|
|
210
217
|
}
|
|
211
218
|
|
|
212
|
-
function hasResumeProgress(entry: ResumableWorkflowEntry): boolean {
|
|
213
|
-
return entry.completedCheckpoints > 0 || entry.pendingPrompts > 0;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function isResumableEntry(entry: ResumableWorkflowEntry): boolean {
|
|
217
|
-
const isRoot = entry.rootWorkflowId === undefined || entry.rootWorkflowId === entry.workflowId;
|
|
218
|
-
if (!isRoot) return false;
|
|
219
|
-
if (entry.status === "failed" || entry.status === "blocked") return entry.resumable !== false;
|
|
220
|
-
// `running` is resumable at this layer: a `running` durable handle may be a
|
|
221
|
-
// crashed process. Same-session double-resume is blocked separately via
|
|
222
|
-
// `hasActiveLiveRun` before dispatch.
|
|
223
|
-
return (entry.status === "running" || entry.status === "paused") && hasResumeProgress(entry);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
219
|
/**
|
|
227
220
|
* True when the live run store has an actively-executing (not ended, not quit)
|
|
228
221
|
* run for `workflowId`. This is the only reliable signal that a durable
|
|
@@ -251,9 +244,6 @@ export function isBackendTerminal(backend: DurableWorkflowBackend, workflowId: s
|
|
|
251
244
|
return false;
|
|
252
245
|
}
|
|
253
246
|
|
|
254
|
-
function hasBackendResumeState(backend: DurableWorkflowBackend, workflowId: string): boolean {
|
|
255
|
-
return backend.getWorkflow(workflowId) !== undefined;
|
|
256
|
-
}
|
|
257
247
|
|
|
258
248
|
/**
|
|
259
249
|
* Runtime-facing async preparation: hydrate the durable backend from DBOS
|
|
@@ -266,6 +256,17 @@ function hasBackendResumeState(backend: DurableWorkflowBackend, workflowId: stri
|
|
|
266
256
|
* terminal (completed/cancelled/non-resumable), stale cache rows for that id
|
|
267
257
|
* are also suppressed. cross-ref: issue #1498.
|
|
268
258
|
*/
|
|
259
|
+
/** Remove restored/live snapshots whose authoritative durable format is non-loadable. */
|
|
260
|
+
export function purgeSuppressedWorkflowRuns(backend: DurableWorkflowBackend, store: RunOpts["store"]): readonly string[] {
|
|
261
|
+
if (store === undefined) return [];
|
|
262
|
+
const removed: string[] = [];
|
|
263
|
+
for (const run of store.runs()) {
|
|
264
|
+
if (backend.isWorkflowLoadable(run.id)) continue;
|
|
265
|
+
if (store.removeRun(run.id)) removed.push(run.id);
|
|
266
|
+
}
|
|
267
|
+
return removed;
|
|
268
|
+
}
|
|
269
|
+
|
|
269
270
|
export async function prepareRuntimeDurableResumable(
|
|
270
271
|
getBackend: () => DurableWorkflowBackend,
|
|
271
272
|
resolveSessionDir: () => string | undefined,
|
|
@@ -293,10 +294,9 @@ export async function prepareRuntimeDurableResumable(
|
|
|
293
294
|
// durable backend handle/checkpoint state, selecting the row can only fail
|
|
294
295
|
// as stale (or risk re-running from scratch), so it should not clutter the
|
|
295
296
|
// resume selector.
|
|
296
|
-
const suppressed = scanned.filter((
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
);
|
|
297
|
+
const suppressed = scanned.filter((entry) => {
|
|
298
|
+
const handle = backend.getWorkflow(entry.workflowId);
|
|
299
|
+
return !liveIds.has(entry.workflowId) && handle !== undefined && isDurableWorkflowResumable(handle);
|
|
300
|
+
});
|
|
301
301
|
return [...live, ...suppressed];
|
|
302
302
|
}
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
* child boundary key, so the same side effects are recovered on resume.
|
|
14
14
|
*
|
|
15
15
|
* Only checkpoint read/write methods are scoped. Lifecycle methods
|
|
16
|
-
* (`registerWorkflow`, `setWorkflowStatus`,
|
|
16
|
+
* (`registerWorkflow`, `setWorkflowStatus`, completed/resumable listing,
|
|
17
17
|
* `toCacheEntry`, `getWorkflow`) are no-ops for scoped children because child
|
|
18
|
-
* runs are never independently
|
|
18
|
+
* runs are never independently addressable — only the root workflow is.
|
|
19
19
|
*
|
|
20
20
|
* cross-ref: issue #1498 — child side effects under the root durable workflow.
|
|
21
21
|
*/
|
|
@@ -86,7 +86,12 @@ export class ScopedDurableBackend implements DurableWorkflowBackend {
|
|
|
86
86
|
return this.inner.getStageOutput(this.scope.rootWorkflowId, this.scopeKey(replayKey));
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
getStageSession(_workflowId: string, replayKey: string): {
|
|
89
|
+
getStageSession(_workflowId: string, replayKey: string): {
|
|
90
|
+
sessionId?: string;
|
|
91
|
+
sessionFile?: string;
|
|
92
|
+
startedAt?: number;
|
|
93
|
+
durationMs?: number;
|
|
94
|
+
} | undefined {
|
|
90
95
|
return this.inner.getStageSession(this.scope.rootWorkflowId, this.scopeKey(replayKey));
|
|
91
96
|
}
|
|
92
97
|
|
|
@@ -114,10 +119,22 @@ export class ScopedDurableBackend implements DurableWorkflowBackend {
|
|
|
114
119
|
return [];
|
|
115
120
|
}
|
|
116
121
|
|
|
122
|
+
listCompletedWorkflows(): readonly ResumableWorkflowEntry[] {
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
|
|
117
126
|
toCacheEntry(_workflowId: string): undefined {
|
|
118
127
|
return undefined;
|
|
119
128
|
}
|
|
120
129
|
|
|
130
|
+
async deleteWorkflow(_workflowId: string): Promise<void> {
|
|
131
|
+
// No-op: scoped children never own or delete root durable state.
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
isWorkflowLoadable(_workflowId: string): boolean {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
121
138
|
reset(): void {
|
|
122
139
|
// No-op: scoped backends never own root state.
|
|
123
140
|
}
|
|
@@ -7,6 +7,7 @@ import type { DurableWorkflowBackend } from "./backend.js";
|
|
|
7
7
|
import type { ParallelFailFastScope } from "../runs/foreground/executor-types.js";
|
|
8
8
|
import { durableHash } from "./backend.js";
|
|
9
9
|
import { recordCheckpointDurably } from "./tool-primitive.js";
|
|
10
|
+
import { elapsedStageMs } from "../shared/timing.js";
|
|
10
11
|
import { RESUME_CONTINUATION_PROMPT } from "../shared/resume-continuation.js";
|
|
11
12
|
import type { DurableStageCheckpoint } from "./types.js";
|
|
12
13
|
export type DurableCompletedStageCheckpoint = DurableStageCheckpoint & { readonly output: WorkflowSerializableValue };
|
|
@@ -17,6 +18,7 @@ export interface DurableStageDeps {
|
|
|
17
18
|
readonly nextCheckpointId: () => string;
|
|
18
19
|
readonly nextReplayKey: (stageName: string) => string;
|
|
19
20
|
readonly replayKeyForCompletedStage?: (stage: StageSnapshot) => string | undefined;
|
|
21
|
+
readonly now?: () => number;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
export async function recordStageCheckpoint(deps: DurableStageDeps, stage: StageSnapshot): Promise<boolean> {
|
|
@@ -51,17 +53,25 @@ export async function recordStageCheckpoint(deps: DurableStageDeps, stage: Stage
|
|
|
51
53
|
export async function recordStageSessionCheckpoint(deps: DurableStageDeps, stage: StageSnapshot): Promise<boolean> {
|
|
52
54
|
const replayKey = deps.replayKeyForCompletedStage?.(stage) ?? stage.replayKey ?? deps.nextReplayKey(stage.name);
|
|
53
55
|
if (stage.sessionFile === undefined) return false;
|
|
56
|
+
const checkpointNow = deps.now?.() ?? Date.now();
|
|
57
|
+
const durationMs = elapsedStageMs(stage, checkpointNow) ?? 0;
|
|
54
58
|
const current = deps.backend.getStageSession(deps.workflowId, replayKey);
|
|
55
|
-
if (current
|
|
59
|
+
if (current !== undefined
|
|
60
|
+
&& current.sessionId === stage.sessionId
|
|
61
|
+
&& current.sessionFile === stage.sessionFile
|
|
62
|
+
&& current.startedAt === stage.startedAt
|
|
63
|
+
&& current.durationMs === durationMs) return false;
|
|
56
64
|
const checkpoint: DurableStageCheckpoint = {
|
|
57
65
|
kind: "stage",
|
|
58
66
|
workflowId: deps.workflowId,
|
|
59
|
-
checkpointId: stageSessionCheckpointId(replayKey, stage),
|
|
67
|
+
checkpointId: stageSessionCheckpointId(replayKey, stage, durationMs),
|
|
60
68
|
name: stage.name,
|
|
61
69
|
replayKey,
|
|
62
70
|
...(stage.sessionId !== undefined ? { sessionId: stage.sessionId } : {}),
|
|
63
71
|
sessionFile: stage.sessionFile,
|
|
64
|
-
|
|
72
|
+
...(stage.startedAt !== undefined ? { startedAt: stage.startedAt } : {}),
|
|
73
|
+
durationMs,
|
|
74
|
+
completedAt: checkpointNow,
|
|
65
75
|
};
|
|
66
76
|
await recordCheckpointDurably(deps.backend, checkpoint);
|
|
67
77
|
return true;
|
|
@@ -107,7 +117,10 @@ export function createDurableStagePrimitive(input: {
|
|
|
107
117
|
const liveOptions: StageOptions | undefined = {
|
|
108
118
|
...(options ?? {}),
|
|
109
119
|
durableReplayKey: replayKey,
|
|
110
|
-
...(isMidSessionResume ? {
|
|
120
|
+
...(isMidSessionResume ? {
|
|
121
|
+
resumeFromSessionFile: session.sessionFile,
|
|
122
|
+
durableAccumulatedDurationMs: session.durationMs ?? 0,
|
|
123
|
+
} : {}),
|
|
111
124
|
};
|
|
112
125
|
const live = withMidSessionResumePrompt(input.stage(name, liveOptions, replayKey), isMidSessionResume);
|
|
113
126
|
if (options?.schema === undefined) return live;
|
|
@@ -139,7 +152,10 @@ export function createDurableTaskPrimitive(input: {
|
|
|
139
152
|
const taskOptions: WorkflowTaskOptions = {
|
|
140
153
|
...options,
|
|
141
154
|
durableReplayKey: replayKey,
|
|
142
|
-
...(session?.sessionFile !== undefined ? {
|
|
155
|
+
...(session?.sessionFile !== undefined ? {
|
|
156
|
+
resumeFromSessionFile: session.sessionFile,
|
|
157
|
+
durableAccumulatedDurationMs: session.durationMs ?? 0,
|
|
158
|
+
} : {}),
|
|
143
159
|
};
|
|
144
160
|
const result = await input.task(name, taskOptions, stageFailFastScope);
|
|
145
161
|
await recordCheckpointDurably(input.backend, {
|
|
@@ -299,8 +315,11 @@ function metadataValue<K extends keyof DurableStageCheckpoint>(
|
|
|
299
315
|
checkpoints: readonly DurableStageCheckpoint[],
|
|
300
316
|
key: K,
|
|
301
317
|
): Pick<DurableStageCheckpoint, K> | Record<string, never> {
|
|
302
|
-
|
|
303
|
-
|
|
318
|
+
for (let index = checkpoints.length - 1; index >= 0; index -= 1) {
|
|
319
|
+
const value = checkpoints[index]?.[key];
|
|
320
|
+
if (value !== undefined) return { [key]: value } as Pick<DurableStageCheckpoint, K>;
|
|
321
|
+
}
|
|
322
|
+
return {};
|
|
304
323
|
}
|
|
305
324
|
|
|
306
325
|
|
|
@@ -317,10 +336,12 @@ export function stableCheckpointId(kind: string, replayKey: string): string {
|
|
|
317
336
|
return `${kind}:${replayKey}`;
|
|
318
337
|
}
|
|
319
338
|
|
|
320
|
-
function stageSessionCheckpointId(replayKey: string, stage: StageSnapshot): string {
|
|
339
|
+
function stageSessionCheckpointId(replayKey: string, stage: StageSnapshot, durationMs: number): string {
|
|
321
340
|
return `${stableCheckpointId("stage-session", replayKey)}:${durableHash({
|
|
322
341
|
sessionId: stage.sessionId ?? "",
|
|
323
342
|
sessionFile: stage.sessionFile ?? "",
|
|
343
|
+
startedAt: stage.startedAt ?? 0,
|
|
344
|
+
durationMs,
|
|
324
345
|
})}`;
|
|
325
346
|
}
|
|
326
347
|
|