@gajae-code/coding-agent 0.11.1 → 0.11.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +64 -0
- package/dist/types/cli/config-cli.d.ts +11 -1
- package/dist/types/cli/launch-disposition.d.ts +23 -0
- package/dist/types/cli/notify-cli.d.ts +1 -0
- package/dist/types/cli/session-picker.d.ts +2 -1
- package/dist/types/cli/setup-cli.d.ts +1 -1
- package/dist/types/commands/mcp-serve.d.ts +33 -0
- package/dist/types/commands/mcp.d.ts +1 -1
- package/dist/types/commands/notify.d.ts +0 -2
- package/dist/types/commands/sdk.d.ts +1 -1
- package/dist/types/config/atomic-yaml-patch.d.ts +18 -0
- package/dist/types/config/file-lock.d.ts +14 -4
- package/dist/types/config/keybindings.d.ts +42 -1
- package/dist/types/config/model-auth.d.ts +3 -0
- package/dist/types/config/model-bindings-applier.d.ts +13 -0
- package/dist/types/config/model-discovery-manager.d.ts +49 -0
- package/dist/types/config/model-equivalence.d.ts +20 -0
- package/dist/types/config/model-registry.d.ts +16 -15
- package/dist/types/config/model-resolver.d.ts +28 -18
- package/dist/types/config/models-config-schema.d.ts +11 -0
- package/dist/types/config/settings-schema.d.ts +141 -11
- package/dist/types/config/settings.d.ts +14 -11
- package/dist/types/coordinator-mcp/policy.d.ts +5 -0
- package/dist/types/coordinator-mcp/question-gate-codec.d.ts +129 -0
- package/dist/types/coordinator-mcp/question-state.d.ts +356 -0
- package/dist/types/coordinator-mcp/server.d.ts +4 -0
- package/dist/types/discovery/helpers.d.ts +0 -6
- package/dist/types/edit/index.d.ts +3 -0
- package/dist/types/eval/py/env.d.ts +9 -0
- package/dist/types/extensibility/extensions/loader.d.ts +2 -1
- package/dist/types/extensibility/extensions/runner.d.ts +2 -2
- package/dist/types/extensibility/extensions/types.d.ts +34 -3
- package/dist/types/extensibility/hooks/runner.d.ts +1 -1
- package/dist/types/extensibility/shared-events.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +10 -1
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +44 -0
- package/dist/types/gjc-runtime/goal-mode-request.d.ts +11 -1
- package/dist/types/gjc-runtime/launch-worktree.d.ts +4 -2
- package/dist/types/gjc-runtime/linux-proc.d.ts +29 -0
- package/dist/types/gjc-runtime/session-resolution.d.ts +2 -2
- package/dist/types/gjc-runtime/state-migrations.d.ts +1 -1
- package/dist/types/gjc-runtime/state-writer.d.ts +2 -1
- package/dist/types/gjc-runtime/team-launch.d.ts +59 -0
- package/dist/types/gjc-runtime/team-notify.d.ts +45 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +29 -121
- package/dist/types/gjc-runtime/team-store.d.ts +160 -0
- package/dist/types/gjc-runtime/team-workers.d.ts +78 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +8 -0
- package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +13 -0
- package/dist/types/gjc-runtime/ultragoal-evidence.d.ts +38 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +7 -0
- package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +23 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +55 -33
- package/dist/types/gjc-runtime/workflow-cli-common.d.ts +8 -0
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -0
- package/dist/types/goals/continuation-timeout-guard.d.ts +22 -0
- package/dist/types/goals/runtime.d.ts +1 -0
- package/dist/types/goals/state.d.ts +8 -0
- package/dist/types/harness-control-plane/lifecycle-reconciliation.d.ts +17 -0
- package/dist/types/harness-control-plane/phase-rollup.d.ts +4 -7
- package/dist/types/harness-control-plane/receipt-ingest.d.ts +5 -0
- package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
- package/dist/types/hindsight/state.d.ts +11 -4
- package/dist/types/lsp/lspmux.d.ts +5 -3
- package/dist/types/lsp/path-trust.d.ts +1 -0
- package/dist/types/main.d.ts +4 -2
- package/dist/types/memories/storage.d.ts +1 -0
- package/dist/types/modes/action-registry.d.ts +29 -0
- package/dist/types/modes/components/chrome.d.ts +14 -0
- package/dist/types/modes/components/command-palette.d.ts +27 -0
- package/dist/types/modes/components/custom-provider-wizard.d.ts +1 -0
- package/dist/types/modes/components/index.d.ts +2 -1
- package/dist/types/modes/components/pet-selector.d.ts +1 -1
- package/dist/types/modes/components/plan-preview-overlay.d.ts +43 -0
- package/dist/types/modes/components/queue-mode-selector.d.ts +1 -1
- package/dist/types/modes/components/queue-pane.d.ts +12 -0
- package/dist/types/modes/components/read-tool-group.d.ts +6 -0
- package/dist/types/modes/components/session-observer-overlay.d.ts +3 -4
- package/dist/types/modes/components/sessions-dashboard.d.ts +32 -0
- package/dist/types/modes/components/settings-selector.d.ts +1 -1
- package/dist/types/modes/components/show-images-selector.d.ts +1 -1
- package/dist/types/modes/components/status-line/types.d.ts +1 -1
- package/dist/types/modes/components/status-line.d.ts +1 -94
- package/dist/types/modes/components/tasks-pane.d.ts +15 -0
- package/dist/types/modes/components/theme-selector.d.ts +1 -1
- package/dist/types/modes/components/tool-execution.d.ts +15 -0
- package/dist/types/modes/components/tool-status-header.d.ts +111 -0
- package/dist/types/modes/components/transcript-viewer-overlay.d.ts +42 -0
- package/dist/types/modes/controllers/goal-mode-controller.d.ts +62 -0
- package/dist/types/modes/controllers/input-controller.d.ts +8 -1
- package/dist/types/modes/controllers/mode-gate.d.ts +8 -0
- package/dist/types/modes/controllers/plan-mode-controller.d.ts +65 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +15 -3
- package/dist/types/modes/controllers/stt-controller.d.ts +26 -0
- package/dist/types/modes/interactive-mode.d.ts +19 -17
- package/dist/types/modes/jobs-observer.d.ts +2 -0
- package/dist/types/modes/prompt-action-autocomplete.d.ts +4 -0
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +4 -11
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +20 -2
- package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +31 -0
- package/dist/types/modes/tasks-aggregator.d.ts +39 -0
- package/dist/types/modes/transcript-item-registry.d.ts +60 -0
- package/dist/types/modes/types.d.ts +20 -13
- package/dist/types/modes/utils/ui-helpers.d.ts +2 -2
- package/dist/types/runtime-mcp/manager.d.ts +1 -1
- package/dist/types/sdk/broker/broker.d.ts +78 -12
- package/dist/types/sdk/broker/lifecycle-codec.d.ts +4 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +15 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +6 -2
- package/dist/types/sdk/broker/process-incarnation.d.ts +2 -0
- package/dist/types/sdk/broker/session-index.d.ts +0 -1
- package/dist/types/sdk/broker/state-version.d.ts +2 -0
- package/dist/types/sdk/bus/chat-effect-journal.d.ts +1 -0
- package/dist/types/sdk/bus/config-commands.d.ts +1 -1
- package/dist/types/sdk/bus/config.d.ts +11 -0
- package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
- package/dist/types/sdk/bus/html-format.d.ts +3 -2
- package/dist/types/sdk/bus/index.d.ts +29 -0
- package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
- package/dist/types/sdk/bus/notification-service.d.ts +20 -0
- package/dist/types/sdk/bus/rate-limit-pool.d.ts +13 -2
- package/dist/types/sdk/bus/recent-activity.d.ts +21 -12
- package/dist/types/sdk/bus/rich-document.d.ts +7 -0
- package/dist/types/sdk/bus/rich-render.d.ts +18 -41
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +9 -4
- package/dist/types/sdk/bus/telegram-daemon.d.ts +46 -3
- package/dist/types/sdk/bus/threaded-inbound.d.ts +1 -1
- package/dist/types/sdk/bus/threaded-render.d.ts +13 -0
- package/dist/types/sdk/bus/topic-registry.d.ts +24 -2
- package/dist/types/sdk/host/control/operations.d.ts +1 -1
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/session-directory.d.ts +56 -0
- package/dist/types/sdk/session.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +82 -47
- package/dist/types/session/agent-storage.d.ts +7 -0
- package/dist/types/session/fallback-chain-controller.d.ts +10 -2
- package/dist/types/session/internal/managed-session-scope.d.ts +102 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +38 -0
- package/dist/types/session/messages.d.ts +16 -0
- package/dist/types/session/session-manager-internal.d.ts +14 -0
- package/dist/types/session/session-manager.d.ts +82 -12
- package/dist/types/session/session-storage.d.ts +43 -3
- package/dist/types/session/volatile-context-pruning.d.ts +15 -0
- package/dist/types/skill-state/{deep-interview-mutation-guard.d.ts → workflow-mutation-guard.d.ts} +24 -8
- package/dist/types/skill-state/workflow-state-contract.d.ts +1 -3
- package/dist/types/slash-commands/builtin-registry.d.ts +4 -1
- package/dist/types/system-prompt.d.ts +2 -11
- package/dist/types/task/executor.d.ts +3 -1
- package/dist/types/thinking.d.ts +7 -0
- package/dist/types/tool-discovery/tool-index.d.ts +1 -50
- package/dist/types/tools/ask.d.ts +64 -2
- package/dist/types/tools/bash.d.ts +3 -0
- package/dist/types/tools/browser/dead-tab-recovery.d.ts +24 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +21 -0
- package/dist/types/tools/cron.d.ts +2 -0
- package/dist/types/tools/fetch.d.ts +1 -0
- package/dist/types/tools/index.d.ts +30 -15
- package/dist/types/tools/output-meta.d.ts +1 -0
- package/dist/types/tools/read.d.ts +5 -0
- package/dist/types/tools/search-tool-bm25.d.ts +3 -4
- package/dist/types/tools/subagent.d.ts +1 -0
- package/dist/types/utils/event-bus.d.ts +1 -1
- package/dist/types/utils/file-mentions.d.ts +3 -3
- package/dist/types/web/search/index.d.ts +3 -1
- package/package.json +21 -9
- package/scripts/build-sdk-package-smoke.ts +34 -1
- package/scripts/generate-hotkeys-docs.ts +74 -0
- package/scripts/generate-sdk-operation-inventory.ts +64 -20
- package/scripts/verify-gjc-sdk-canonicalization.ts +182 -2
- package/src/async/job-manager.ts +49 -6
- package/src/cli/config-cli.ts +75 -2
- package/src/cli/launch-disposition.ts +42 -0
- package/src/cli/notify-cli.ts +92 -26
- package/src/cli/session-picker.ts +20 -3
- package/src/cli/setup-cli.ts +1 -1
- package/src/cli.ts +1 -1
- package/src/commands/config.ts +1 -1
- package/src/commands/harness.ts +34 -106
- package/src/commands/launch.ts +6 -4
- package/src/commands/mcp-serve.ts +85 -8
- package/src/commands/mcp.ts +2 -2
- package/src/commands/notify.ts +8 -4
- package/src/commands/sdk.ts +61 -40
- package/src/commands/ultragoal.ts +4 -1
- package/src/config/atomic-yaml-patch.ts +80 -13
- package/src/config/file-lock.ts +105 -43
- package/src/config/keybindings.ts +64 -20
- package/src/config/model-auth.ts +9 -0
- package/src/config/model-bindings-applier.ts +139 -0
- package/src/config/model-discovery-manager.ts +224 -0
- package/src/config/model-equivalence.ts +66 -0
- package/src/config/model-registry.ts +169 -302
- package/src/config/model-resolver.ts +361 -369
- package/src/config/models-config-schema.ts +5 -4
- package/src/config/settings-schema.ts +214 -12
- package/src/config/settings.ts +458 -482
- package/src/coordinator-mcp/policy.ts +76 -11
- package/src/coordinator-mcp/question-gate-codec.ts +274 -0
- package/src/coordinator-mcp/question-state.ts +778 -0
- package/src/coordinator-mcp/server.ts +1702 -238
- package/src/debug/index.ts +1 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +13 -1
- package/src/defaults/gjc/skills/team/SKILL.md +16 -0
- package/src/discovery/helpers.ts +1 -50
- package/src/edit/index.ts +64 -0
- package/src/eval/py/env.ts +26 -0
- package/src/eval/py/kernel.ts +5 -3
- package/src/extensibility/extensions/loader.ts +9 -1
- package/src/extensibility/extensions/runner.ts +12 -3
- package/src/extensibility/extensions/types.ts +39 -1
- package/src/extensibility/gjc-plugins/installer.ts +16 -16
- package/src/extensibility/hooks/runner.ts +2 -2
- package/src/extensibility/runtime-skill-discovery.ts +33 -13
- package/src/extensibility/shared-events.ts +2 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +63 -3
- package/src/gjc-runtime/deep-interview-runtime.ts +76 -27
- package/src/gjc-runtime/deep-interview-state.ts +317 -5
- package/src/gjc-runtime/goal-mode-request.ts +55 -8
- package/src/gjc-runtime/launch-tmux.ts +2 -5
- package/src/gjc-runtime/launch-worktree.ts +5 -5
- package/src/gjc-runtime/linux-proc.ts +71 -0
- package/src/gjc-runtime/ralplan-runtime.ts +74 -90
- package/src/gjc-runtime/session-resolution.ts +18 -3
- package/src/gjc-runtime/session-state-sidecar.ts +6 -107
- package/src/gjc-runtime/state-migrations.ts +1 -1
- package/src/gjc-runtime/state-runtime.ts +327 -249
- package/src/gjc-runtime/state-writer.ts +36 -40
- package/src/gjc-runtime/team-launch.ts +319 -0
- package/src/gjc-runtime/team-notify.ts +326 -0
- package/src/gjc-runtime/team-runtime.ts +1099 -1482
- package/src/gjc-runtime/team-store.ts +828 -0
- package/src/gjc-runtime/team-workers.ts +962 -0
- package/src/gjc-runtime/tmux-owner-isolation-cli.ts +2 -11
- package/src/gjc-runtime/tmux-owner-isolation.ts +14 -9
- package/src/gjc-runtime/tmux-sessions.ts +4 -21
- package/src/gjc-runtime/ultragoal-change-set.ts +172 -0
- package/src/gjc-runtime/ultragoal-evidence.ts +874 -0
- package/src/gjc-runtime/ultragoal-guard.ts +29 -5
- package/src/gjc-runtime/ultragoal-pipeline.ts +306 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +84 -1311
- package/src/gjc-runtime/workflow-cli-common.ts +30 -0
- package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
- package/src/gjc-runtime/workflow-manifest.ts +18 -0
- package/src/goals/continuation-timeout-guard.ts +79 -0
- package/src/goals/runtime.ts +7 -5
- package/src/goals/state.ts +18 -0
- package/src/harness-control-plane/lifecycle-reconciliation.ts +64 -0
- package/src/harness-control-plane/phase-rollup.ts +4 -7
- package/src/harness-control-plane/receipt-ingest.ts +5 -0
- package/src/harness-control-plane/receipt-spool.ts +51 -7
- package/src/harness-control-plane/session-lease.ts +12 -130
- package/src/hindsight/backend.ts +26 -27
- package/src/hindsight/state.ts +101 -32
- package/src/hooks/skill-state.ts +17 -11
- package/src/internal-urls/docs-index.generated.ts +21 -18
- package/src/lsp/client.ts +1 -1
- package/src/lsp/config.ts +145 -26
- package/src/lsp/index.ts +1 -1
- package/src/lsp/lspmux.ts +39 -10
- package/src/lsp/path-trust.ts +81 -0
- package/src/main.ts +127 -30
- package/src/memories/index.ts +1 -0
- package/src/memories/storage.ts +6 -2
- package/src/modes/action-registry.ts +165 -0
- package/src/modes/components/chrome.ts +40 -0
- package/src/modes/components/command-palette.ts +168 -0
- package/src/modes/components/custom-editor.ts +1 -1
- package/src/modes/components/custom-provider-wizard.ts +51 -14
- package/src/modes/components/index.ts +2 -1
- package/src/modes/components/pet-selector.ts +11 -17
- package/src/modes/components/plan-preview-overlay.ts +295 -0
- package/src/modes/components/queue-mode-selector.ts +10 -27
- package/src/modes/components/queue-pane.ts +79 -0
- package/src/modes/components/read-tool-group.ts +13 -0
- package/src/modes/components/session-observer-overlay.ts +207 -772
- package/src/modes/components/sessions-dashboard.ts +181 -0
- package/src/modes/components/settings-selector.ts +1 -1
- package/src/modes/components/show-images-selector.ts +10 -24
- package/src/modes/components/status-line/types.ts +1 -1
- package/src/modes/components/status-line.ts +1 -969
- package/src/modes/components/tasks-pane.ts +73 -0
- package/src/modes/components/theme-selector.ts +11 -31
- package/src/modes/components/tool-execution.ts +22 -0
- package/src/modes/components/tool-status-header.ts +1056 -0
- package/src/modes/components/transcript-viewer-overlay.ts +384 -0
- package/src/modes/controllers/btw-controller.ts +1 -0
- package/src/modes/controllers/event-controller.ts +3 -2
- package/src/modes/controllers/extension-ui-controller.ts +24 -1
- package/src/modes/controllers/goal-mode-controller.ts +474 -0
- package/src/modes/controllers/input-controller.ts +764 -229
- package/src/modes/controllers/mode-gate.ts +24 -0
- package/src/modes/controllers/plan-mode-controller.ts +517 -0
- package/src/modes/controllers/selector-controller.ts +225 -10
- package/src/modes/controllers/stt-controller.ts +125 -0
- package/src/modes/interactive-mode.ts +289 -1150
- package/src/modes/jobs-observer.ts +3 -0
- package/src/modes/prompt-action-autocomplete.ts +7 -0
- package/src/modes/runtime-init.ts +8 -0
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -106
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +71 -16
- package/src/modes/shared/agent-wire/workflow-gate-types.ts +177 -0
- package/src/modes/tasks-aggregator.ts +199 -0
- package/src/modes/transcript-item-registry.ts +229 -0
- package/src/modes/types.ts +45 -18
- package/src/modes/utils/ui-helpers.ts +17 -9
- package/src/prompts/agent-fragments/ralplan-persistence.md +6 -0
- package/src/prompts/agent-fragments/restricted-bash.md +1 -0
- package/src/prompts/agents/architect.md +2 -8
- package/src/prompts/agents/critic.md +2 -8
- package/src/prompts/agents/executor.md +3 -0
- package/src/prompts/agents/planner.md +4 -12
- package/src/prompts/system/custom-system-prompt.md +1 -23
- package/src/prompts/system/plan-mode-approved.md +8 -0
- package/src/prompts/system/system-prompt.md +17 -96
- package/src/prompts/tools/bash.md +4 -4
- package/src/prompts/tools/browser.md +15 -52
- package/src/prompts/tools/find.md +2 -2
- package/src/prompts/tools/hashline.md +13 -55
- package/src/prompts/tools/read.md +3 -6
- package/src/prompts/tools/search-tool-bm25.md +0 -2
- package/src/prompts/tools/search.md +2 -4
- package/src/prompts/tools/skill-discovery.md +1 -2
- package/src/prompts/tools/task.md +1 -1
- package/src/runtime-mcp/manager.ts +1 -1
- package/src/sdk/broker/broker.ts +206 -78
- package/src/sdk/broker/discovery.ts +1 -1
- package/src/sdk/broker/ensure.ts +3 -1
- package/src/sdk/broker/lifecycle-codec.ts +11 -0
- package/src/sdk/broker/lifecycle-ledger.ts +371 -74
- package/src/sdk/broker/lifecycle.ts +1399 -142
- package/src/sdk/broker/process-incarnation.ts +5 -13
- package/src/sdk/broker/session-index.ts +145 -56
- package/src/sdk/broker/state-version.ts +21 -0
- package/src/sdk/broker/transport.ts +1 -1
- package/src/sdk/bus/chat-daemon-cli.ts +2 -0
- package/src/sdk/bus/chat-daemon-control.ts +8 -28
- package/src/sdk/bus/chat-effect-journal.ts +10 -2
- package/src/sdk/bus/config-commands.ts +5 -4
- package/src/sdk/bus/config.ts +98 -0
- package/src/sdk/bus/conversation-store.ts +5 -29
- package/src/sdk/bus/daemon-paths.ts +2 -0
- package/src/sdk/bus/html-format.ts +20 -3
- package/src/sdk/bus/index.ts +782 -94
- package/src/sdk/bus/lifecycle-control-runtime.ts +91 -33
- package/src/sdk/bus/natives-augment.d.ts +26 -1
- package/src/sdk/bus/notification-service.ts +168 -20
- package/src/sdk/bus/rate-limit-pool.ts +58 -14
- package/src/sdk/bus/recent-activity.ts +188 -74
- package/src/sdk/bus/rich-document.ts +258 -0
- package/src/sdk/bus/rich-render.ts +62 -70
- package/src/sdk/bus/telegram-daemon-cli.ts +15 -59
- package/src/sdk/bus/telegram-daemon-contract.ts +9 -4
- package/src/sdk/bus/telegram-daemon.ts +1389 -237
- package/src/sdk/bus/threaded-inbound.ts +8 -3
- package/src/sdk/bus/threaded-render.ts +57 -1
- package/src/sdk/bus/topic-registry.ts +120 -13
- package/src/sdk/host/control/dispatch.ts +14 -2
- package/src/sdk/host/control/operations.ts +6 -1
- package/src/sdk/host/host.ts +44 -2
- package/src/sdk/host/query/handlers.ts +20 -22
- package/src/sdk/host/query/revision-store.ts +80 -57
- package/src/sdk/index.ts +1 -0
- package/src/sdk/protocol/operation-inventory.generated.json +943 -1907
- package/src/sdk/session-directory.ts +141 -0
- package/src/sdk/session.ts +462 -480
- package/src/session/agent-session.ts +1700 -583
- package/src/session/agent-storage.ts +18 -0
- package/src/session/cache-economics.ts +25 -8
- package/src/session/fallback-chain-controller.ts +53 -3
- package/src/session/internal/managed-session-scope.ts +2226 -0
- package/src/session/internal/managed-session-storage.ts +453 -0
- package/src/session/messages.ts +91 -2
- package/src/session/session-manager-internal.ts +33 -0
- package/src/session/session-manager.ts +1112 -333
- package/src/session/session-storage.ts +444 -39
- package/src/session/volatile-context-pruning.ts +78 -0
- package/src/setup/credential-auto-import.ts +2 -1
- package/src/setup/hermes/templates/operator-instructions.v1.md +1 -1
- package/src/setup/provider-onboarding.ts +36 -61
- package/src/setup/provider-presets.json +5 -0
- package/src/skill-state/active-state.ts +3 -13
- package/src/skill-state/{deep-interview-mutation-guard.ts → workflow-mutation-guard.ts} +47 -29
- package/src/skill-state/workflow-state-contract.ts +6 -41
- package/src/slash-commands/builtin-registry.ts +46 -11
- package/src/system-prompt.ts +37 -44
- package/src/task/agents.ts +19 -1
- package/src/task/executor.ts +90 -6
- package/src/task/index.ts +36 -21
- package/src/thinking.ts +21 -6
- package/src/tool-discovery/tool-index.ts +2 -134
- package/src/tools/ask.ts +157 -21
- package/src/tools/ast-edit.ts +1 -1
- package/src/tools/bash.ts +38 -0
- package/src/tools/browser/dead-tab-recovery.ts +78 -0
- package/src/tools/browser/tab-supervisor.ts +282 -36
- package/src/tools/computer.ts +10 -3
- package/src/tools/cron.ts +30 -16
- package/src/tools/eval.ts +1 -2
- package/src/tools/fetch.ts +11 -4
- package/src/tools/index.ts +76 -24
- package/src/tools/output-meta.ts +19 -7
- package/src/tools/read.ts +219 -64
- package/src/tools/resource-gc.ts +2 -2
- package/src/tools/search-tool-bm25.ts +11 -74
- package/src/tools/subagent.ts +43 -2
- package/src/utils/event-bus.ts +9 -4
- package/src/utils/file-mentions.ts +4 -4
- package/src/web/search/index.ts +3 -3
- package/dist/types/discovery/plugin-dir-roots.d.ts +0 -15
- package/dist/types/runtime-mcp/discoverable-tool-metadata.d.ts +0 -7
- package/src/discovery/plugin-dir-roots.ts +0 -28
- package/src/runtime-mcp/discoverable-tool-metadata.ts +0 -25
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import * as fsSync from "node:fs";
|
|
2
3
|
import * as fs from "node:fs/promises";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import type { SdkStartupFailure, SdkStartupRollbackResult } from "../startup-capability";
|
|
6
|
+
import { parseLifecycleJson } from "./lifecycle-codec";
|
|
5
7
|
import { assertSupportedStateVersion, SDK_STATE_VERSION } from "./state-version";
|
|
6
8
|
|
|
7
9
|
export type LifecycleState =
|
|
@@ -87,6 +89,55 @@ export type BeginResult =
|
|
|
87
89
|
| { kind: "terminal_uncertain"; entry: LifecycleLedgerEntry }
|
|
88
90
|
| { kind: "in_progress"; entry: LifecycleLedgerEntry };
|
|
89
91
|
const terminal = (s: LifecycleState) => s === "terminal_ok" || s === "terminal_error";
|
|
92
|
+
const final = (s: LifecycleState) => terminal(s) || s === "terminal_uncertain";
|
|
93
|
+
export interface LifecycleLedgerLimits {
|
|
94
|
+
maxBytes?: number;
|
|
95
|
+
maxLineBytes?: number;
|
|
96
|
+
maxRows?: number;
|
|
97
|
+
}
|
|
98
|
+
const DEFAULT_LIFECYCLE_LEDGER_LIMITS: Required<LifecycleLedgerLimits> = {
|
|
99
|
+
maxBytes: 64 * 1024 * 1024,
|
|
100
|
+
maxLineBytes: 8 * 1024 * 1024,
|
|
101
|
+
maxRows: 10_000,
|
|
102
|
+
};
|
|
103
|
+
const MAX_LIFECYCLE_LEDGER_JSON_DEPTH = 64;
|
|
104
|
+
const MAX_LIFECYCLE_LEDGER_JSON_FIELDS = 1024;
|
|
105
|
+
|
|
106
|
+
function isBoundedLedgerJson(value: unknown, depth = 0, budget = { fields: 0 }): boolean {
|
|
107
|
+
if (depth > MAX_LIFECYCLE_LEDGER_JSON_DEPTH) return false;
|
|
108
|
+
if (value === null || typeof value !== "object") return true;
|
|
109
|
+
if (Array.isArray(value)) {
|
|
110
|
+
if (value.length > MAX_LIFECYCLE_LEDGER_JSON_FIELDS) return false;
|
|
111
|
+
return value.every(item => isBoundedLedgerJson(item, depth + 1, budget));
|
|
112
|
+
}
|
|
113
|
+
const record = value as Record<string, unknown>;
|
|
114
|
+
const keys = Object.keys(record);
|
|
115
|
+
budget.fields += keys.length;
|
|
116
|
+
return (
|
|
117
|
+
budget.fields <= MAX_LIFECYCLE_LEDGER_JSON_FIELDS &&
|
|
118
|
+
keys.every(key => isBoundedLedgerJson(record[key], depth + 1, budget))
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isLifecycleLedgerEntry(value: unknown): value is LifecycleLedgerEntry {
|
|
123
|
+
if (typeof value !== "object" || value === null || Array.isArray(value) || !isBoundedLedgerJson(value)) return false;
|
|
124
|
+
const entry = value as Partial<LifecycleLedgerEntry>;
|
|
125
|
+
return (
|
|
126
|
+
entry.version === SDK_STATE_VERSION &&
|
|
127
|
+
typeof entry.identity === "string" &&
|
|
128
|
+
entry.identity.length > 0 &&
|
|
129
|
+
typeof entry.requestHash === "string" &&
|
|
130
|
+
entry.requestHash.length > 0 &&
|
|
131
|
+
(entry.state === "accepted" ||
|
|
132
|
+
entry.state === "effect_started" ||
|
|
133
|
+
entry.state === "awaiting_ready" ||
|
|
134
|
+
entry.state === "terminal_ok" ||
|
|
135
|
+
entry.state === "terminal_error" ||
|
|
136
|
+
entry.state === "terminal_uncertain") &&
|
|
137
|
+
typeof entry.ts === "number" &&
|
|
138
|
+
Number.isSafeInteger(entry.ts)
|
|
139
|
+
);
|
|
140
|
+
}
|
|
90
141
|
function canonicalJson(value: unknown): string {
|
|
91
142
|
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
92
143
|
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
@@ -115,111 +166,356 @@ export class LifecycleLedger {
|
|
|
115
166
|
#corruptFile: string;
|
|
116
167
|
#entries: LifecycleLedgerEntry[] = [];
|
|
117
168
|
#byIdentity = new Map<string, LifecycleLedgerEntry>();
|
|
169
|
+
#limits: Required<LifecycleLedgerLimits>;
|
|
170
|
+
#rowCount = 0;
|
|
171
|
+
#byteCount = 0;
|
|
118
172
|
#warnings: string[] = [];
|
|
119
|
-
|
|
173
|
+
#mutationTail: Promise<void> = Promise.resolve();
|
|
174
|
+
|
|
175
|
+
async #mutate<T>(operation: () => Promise<T>): Promise<T> {
|
|
176
|
+
const previous = this.#mutationTail;
|
|
177
|
+
const completion = Promise.withResolvers<void>();
|
|
178
|
+
this.#mutationTail = previous.then(() => completion.promise);
|
|
179
|
+
await previous;
|
|
180
|
+
try {
|
|
181
|
+
return await operation();
|
|
182
|
+
} finally {
|
|
183
|
+
completion.resolve();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
constructor(agentDir: string, limits: LifecycleLedgerLimits = {}) {
|
|
120
188
|
this.#file = path.join(agentDir, "sdk", "lifecycle-ledger.jsonl");
|
|
121
189
|
this.#corruptFile = `${this.#file}.corrupt`;
|
|
190
|
+
this.#limits = {
|
|
191
|
+
maxBytes: limits.maxBytes ?? DEFAULT_LIFECYCLE_LEDGER_LIMITS.maxBytes,
|
|
192
|
+
maxLineBytes: limits.maxLineBytes ?? DEFAULT_LIFECYCLE_LEDGER_LIMITS.maxLineBytes,
|
|
193
|
+
maxRows: limits.maxRows ?? DEFAULT_LIFECYCLE_LEDGER_LIMITS.maxRows,
|
|
194
|
+
};
|
|
122
195
|
}
|
|
123
196
|
async open(): Promise<this> {
|
|
124
|
-
|
|
197
|
+
return this.#mutate(async () => this.#open());
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async #open(): Promise<this> {
|
|
125
201
|
await fs.mkdir(path.dirname(this.#file), { recursive: true, mode: 0o700 });
|
|
126
202
|
this.#entries = [];
|
|
127
|
-
const quarantinedTerminal = new Set<string>();
|
|
128
203
|
this.#byIdentity.clear();
|
|
129
204
|
this.#warnings = [];
|
|
205
|
+
this.#rowCount = 0;
|
|
206
|
+
this.#byteCount = 0;
|
|
207
|
+
const invalidIdentities = new Set<string>();
|
|
208
|
+
const syntheticUncertain = new Map<string, LifecycleLedgerEntry>();
|
|
130
209
|
const uncertainAfterCorruption = new Set<string>();
|
|
210
|
+
const source = await this.#readBoundedSource();
|
|
131
211
|
let tornTail = false;
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
tornTail = source.length > 0 &&
|
|
135
|
-
|
|
136
|
-
for (
|
|
137
|
-
if (
|
|
212
|
+
if (source) {
|
|
213
|
+
this.#byteCount = source.length;
|
|
214
|
+
tornTail = source.length > 0 && source.at(-1) !== 0x0a;
|
|
215
|
+
let lineStart = 0;
|
|
216
|
+
for (let offset = 0; offset <= source.length; offset += 1) {
|
|
217
|
+
if (offset !== source.length && source[offset] !== 0x0a) continue;
|
|
218
|
+
const line = source.subarray(lineStart, offset);
|
|
219
|
+
lineStart = offset + 1;
|
|
220
|
+
if (line.length === 0) continue;
|
|
221
|
+
this.#rowCount += 1;
|
|
222
|
+
if (this.#rowCount > this.#limits.maxRows)
|
|
223
|
+
await this.#rejectOversizedSource("Lifecycle ledger exceeds the maximum row count.");
|
|
224
|
+
if (line.length > this.#limits.maxLineBytes)
|
|
225
|
+
await this.#rejectOversizedSource("Lifecycle ledger row exceeds the maximum byte length.");
|
|
138
226
|
try {
|
|
139
|
-
const
|
|
140
|
-
assertSupportedStateVersion(this.#file,
|
|
141
|
-
if (!
|
|
142
|
-
|
|
227
|
+
const value = parseLifecycleJson(line);
|
|
228
|
+
assertSupportedStateVersion(this.#file, value);
|
|
229
|
+
if (!isLifecycleLedgerEntry(value)) throw new Error("invalid ledger entry");
|
|
230
|
+
const entry = value;
|
|
231
|
+
const prior = this.#byIdentity.get(entry.identity);
|
|
232
|
+
const invalidHistory =
|
|
233
|
+
invalidIdentities.has(entry.identity) ||
|
|
234
|
+
uncertainAfterCorruption.has(entry.identity) ||
|
|
235
|
+
!hasValidTerminalDigests(entry) ||
|
|
236
|
+
!this.#isValidHistoryContinuation(prior, entry);
|
|
237
|
+
if (invalidHistory) {
|
|
143
238
|
await this.#quarantine(line);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
durableEffects: _durableEffects,
|
|
148
|
-
...uncertain
|
|
149
|
-
} = e;
|
|
150
|
-
const quarantined = { ...uncertain, state: "terminal_uncertain" as const, ts: Date.now() };
|
|
151
|
-
this.#entries.push(quarantined);
|
|
152
|
-
this.#byIdentity.set(quarantined.identity, quarantined);
|
|
153
|
-
quarantinedTerminal.add(quarantined.identity);
|
|
239
|
+
invalidIdentities.add(entry.identity);
|
|
240
|
+
if (!syntheticUncertain.has(entry.identity))
|
|
241
|
+
syntheticUncertain.set(entry.identity, this.#uncertainFrom(prior ?? entry, prior !== undefined));
|
|
154
242
|
continue;
|
|
155
243
|
}
|
|
156
|
-
this.#entries.push(
|
|
157
|
-
this.#byIdentity.set(
|
|
158
|
-
|
|
159
|
-
|
|
244
|
+
this.#entries.push(entry);
|
|
245
|
+
this.#byIdentity.set(entry.identity, entry);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
if (error instanceof Error && "code" in error && error.code === "unsupported_state_version") throw error;
|
|
160
248
|
for (const [identity, latest] of this.#byIdentity) {
|
|
161
|
-
if (!
|
|
162
|
-
uncertainAfterCorruption.add(identity);
|
|
249
|
+
if (!final(latest.state)) uncertainAfterCorruption.add(identity);
|
|
163
250
|
}
|
|
164
251
|
await this.#quarantine(line);
|
|
165
252
|
}
|
|
166
253
|
}
|
|
167
|
-
} catch (e) {
|
|
168
|
-
if ((e as NodeJS.ErrnoException).code !== "ENOENT") throw e;
|
|
169
254
|
}
|
|
170
255
|
if (tornTail) await this.#sealTornTail();
|
|
171
|
-
for (const identity of
|
|
172
|
-
|
|
173
|
-
if (
|
|
256
|
+
for (const [identity, uncertain] of syntheticUncertain) {
|
|
257
|
+
this.#byIdentity.set(identity, uncertain);
|
|
258
|
+
if (this.#entries.some(entry => entry.identity === identity && entry.state === "accepted"))
|
|
259
|
+
await this.#append(uncertain);
|
|
174
260
|
}
|
|
175
261
|
for (const identity of uncertainAfterCorruption) {
|
|
176
262
|
const entry = this.#byIdentity.get(identity);
|
|
177
|
-
if (entry && !
|
|
178
|
-
await this.#append({ ...entry, state: "terminal_uncertain", ts: Date.now() });
|
|
179
|
-
}
|
|
263
|
+
if (entry && !final(entry.state)) await this.#append(this.#uncertainFrom(entry));
|
|
180
264
|
}
|
|
181
265
|
// Effects may have completed after the last durable marker; do not retry them after a restart.
|
|
182
266
|
for (const entry of [...this.#byIdentity.values()]) {
|
|
183
|
-
if (entry.state === "effect_started" || entry.state === "awaiting_ready")
|
|
184
|
-
await this.#append(
|
|
267
|
+
if ((entry.state === "effect_started" && !this.#isCleanupPending(entry)) || entry.state === "awaiting_ready")
|
|
268
|
+
await this.#append(this.#uncertainFrom(entry));
|
|
185
269
|
}
|
|
186
270
|
return this;
|
|
187
271
|
}
|
|
272
|
+
/**
|
|
273
|
+
* Reads terminal proof for one request without recovering or changing the ledger.
|
|
274
|
+
*
|
|
275
|
+
* This intentionally accepts an unrelated unterminated final write: concurrent
|
|
276
|
+
* appenders may have started a different row after this request's synced terminal
|
|
277
|
+
* row. Complete rows are still decoded and validated strictly, and any incomplete
|
|
278
|
+
* tail that identifies this request withholds proof.
|
|
279
|
+
*/
|
|
280
|
+
async readTerminal(identity: string, requestHash: string): Promise<LifecycleLedgerEntry | undefined> {
|
|
281
|
+
const source = await this.#readBoundedSourceReadOnly();
|
|
282
|
+
if (!source) return undefined;
|
|
283
|
+
let prior: LifecycleLedgerEntry | undefined;
|
|
284
|
+
let latest: LifecycleLedgerEntry | undefined;
|
|
285
|
+
let rows = 0;
|
|
286
|
+
let lineStart = 0;
|
|
287
|
+
const finalNewline = source.length > 0 && source.at(-1) === 0x0a;
|
|
288
|
+
const completeLength = finalNewline ? source.length : source.lastIndexOf(0x0a) + 1;
|
|
289
|
+
for (let offset = 0; offset < completeLength; offset += 1) {
|
|
290
|
+
if (source[offset] !== 0x0a) continue;
|
|
291
|
+
const line = source.subarray(lineStart, offset);
|
|
292
|
+
lineStart = offset + 1;
|
|
293
|
+
if (line.length === 0) continue;
|
|
294
|
+
rows += 1;
|
|
295
|
+
if (rows > this.#limits.maxRows || line.length > this.#limits.maxLineBytes) return undefined;
|
|
296
|
+
let entry: LifecycleLedgerEntry;
|
|
297
|
+
try {
|
|
298
|
+
const value = parseLifecycleJson(line);
|
|
299
|
+
assertSupportedStateVersion(this.#file, value);
|
|
300
|
+
if (!isLifecycleLedgerEntry(value)) return undefined;
|
|
301
|
+
entry = value;
|
|
302
|
+
} catch {
|
|
303
|
+
return undefined;
|
|
304
|
+
}
|
|
305
|
+
if (entry.identity !== identity) continue;
|
|
306
|
+
if (
|
|
307
|
+
entry.requestHash !== requestHash ||
|
|
308
|
+
!hasValidTerminalDigests(entry) ||
|
|
309
|
+
!this.#isValidHistoryContinuation(prior, entry)
|
|
310
|
+
)
|
|
311
|
+
return undefined;
|
|
312
|
+
prior = entry;
|
|
313
|
+
latest = entry;
|
|
314
|
+
}
|
|
315
|
+
if (!finalNewline) {
|
|
316
|
+
const tail = source.subarray(completeLength);
|
|
317
|
+
// LifecycleLedger writes JSON.stringify entries, so this marker is exact for
|
|
318
|
+
// a partially persisted row from this identity without inspecting arbitrary
|
|
319
|
+
// malformed data as a valid record.
|
|
320
|
+
const identityMarker = Buffer.from(`"identity":${JSON.stringify(identity)}`);
|
|
321
|
+
if (tail.includes(identityMarker)) return undefined;
|
|
322
|
+
}
|
|
323
|
+
return latest && terminal(latest.state) ? latest : undefined;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
#isValidHistoryContinuation(previous: LifecycleLedgerEntry | undefined, next: LifecycleLedgerEntry): boolean {
|
|
327
|
+
if (!previous) return next.state === "accepted";
|
|
328
|
+
if (previous.requestHash !== next.requestHash || final(previous.state)) return false;
|
|
329
|
+
if (previous.state === "accepted") return true;
|
|
330
|
+
return next.state === "effect_started" || next.state === "awaiting_ready" || final(next.state);
|
|
331
|
+
}
|
|
332
|
+
#isCleanupPending(entry: LifecycleLedgerEntry): boolean {
|
|
333
|
+
if (!entry.response || typeof entry.response !== "object") return false;
|
|
334
|
+
const response = entry.response as { ok?: unknown; error?: { code?: unknown; cleanup?: unknown } };
|
|
335
|
+
return (
|
|
336
|
+
response.ok === false && response.error?.code === "cleanup_pending" && response.error.cleanup !== undefined
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
#uncertainFrom(entry: LifecycleLedgerEntry, trusted = true): LifecycleLedgerEntry {
|
|
341
|
+
if (trusted) return { ...entry, state: "terminal_uncertain", ts: Date.now() };
|
|
342
|
+
return {
|
|
343
|
+
version: SDK_STATE_VERSION,
|
|
344
|
+
identity: entry.identity,
|
|
345
|
+
requestHash: entry.requestHash,
|
|
346
|
+
state: "terminal_uncertain",
|
|
347
|
+
ts: Date.now(),
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
async #readBoundedSource(): Promise<Buffer | undefined> {
|
|
351
|
+
let handle: fs.FileHandle | undefined;
|
|
352
|
+
try {
|
|
353
|
+
handle = await fs.open(this.#file, fsSync.constants.O_RDONLY | fsSync.constants.O_NOFOLLOW);
|
|
354
|
+
const stat = await handle.stat({ bigint: true });
|
|
355
|
+
if (!stat.isFile() || stat.size > BigInt(this.#limits.maxBytes))
|
|
356
|
+
await this.#rejectOversizedSource("Lifecycle ledger exceeds the maximum file byte length.");
|
|
357
|
+
const bytes = Buffer.alloc(Number(stat.size) + 1);
|
|
358
|
+
const { bytesRead } = await handle.read(bytes, 0, bytes.length, 0);
|
|
359
|
+
if (bytesRead > this.#limits.maxBytes)
|
|
360
|
+
await this.#rejectOversizedSource("Lifecycle ledger exceeds the maximum file byte length.");
|
|
361
|
+
return bytes.subarray(0, bytesRead);
|
|
362
|
+
} catch (error) {
|
|
363
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
364
|
+
throw error;
|
|
365
|
+
} finally {
|
|
366
|
+
if (handle) await handle.close();
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
async #readBoundedSourceReadOnly(): Promise<Buffer | undefined> {
|
|
370
|
+
let handle: fs.FileHandle | undefined;
|
|
371
|
+
try {
|
|
372
|
+
handle = await fs.open(this.#file, fsSync.constants.O_RDONLY | fsSync.constants.O_NOFOLLOW);
|
|
373
|
+
const stat = await handle.stat({ bigint: true });
|
|
374
|
+
if (!stat.isFile() || stat.size > BigInt(this.#limits.maxBytes)) return undefined;
|
|
375
|
+
const bytes = Buffer.alloc(Number(stat.size) + 1);
|
|
376
|
+
const { bytesRead } = await handle.read(bytes, 0, bytes.length, 0);
|
|
377
|
+
if (bytesRead > this.#limits.maxBytes) return undefined;
|
|
378
|
+
return bytes.subarray(0, bytesRead);
|
|
379
|
+
} catch (error) {
|
|
380
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
381
|
+
return undefined;
|
|
382
|
+
} finally {
|
|
383
|
+
if (handle) await handle.close();
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
async #rejectOversizedSource(reason: string): Promise<never> {
|
|
387
|
+
await this.#quarantine(reason);
|
|
388
|
+
throw new Error(reason);
|
|
389
|
+
}
|
|
390
|
+
async #openAppendRegular(file: string): Promise<fs.FileHandle> {
|
|
391
|
+
const handle = await fs.open(
|
|
392
|
+
file,
|
|
393
|
+
fsSync.constants.O_WRONLY | fsSync.constants.O_APPEND | fsSync.constants.O_CREAT | fsSync.constants.O_NOFOLLOW,
|
|
394
|
+
0o600,
|
|
395
|
+
);
|
|
396
|
+
try {
|
|
397
|
+
if (!(await handle.stat()).isFile()) throw new Error("Lifecycle ledger write target is not a regular file.");
|
|
398
|
+
return handle;
|
|
399
|
+
} catch (error) {
|
|
400
|
+
await handle.close();
|
|
401
|
+
throw error;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
188
404
|
async #sealTornTail(): Promise<void> {
|
|
189
|
-
const h = await
|
|
405
|
+
const h = await this.#openAppendRegular(this.#file);
|
|
190
406
|
try {
|
|
191
407
|
await h.writeFile("\n");
|
|
192
408
|
await h.sync();
|
|
409
|
+
this.#byteCount += 1;
|
|
193
410
|
} finally {
|
|
194
411
|
await h.close();
|
|
195
412
|
}
|
|
196
413
|
}
|
|
197
|
-
async #quarantine(line: string): Promise<void> {
|
|
198
|
-
const h = await
|
|
414
|
+
async #quarantine(line: string | Uint8Array): Promise<void> {
|
|
415
|
+
const h = await this.#openAppendRegular(this.#corruptFile);
|
|
199
416
|
try {
|
|
200
|
-
await h.writeFile(
|
|
417
|
+
await h.writeFile(line);
|
|
418
|
+
await h.writeFile("\n");
|
|
201
419
|
await h.sync();
|
|
202
420
|
} finally {
|
|
203
421
|
await h.close();
|
|
204
422
|
}
|
|
205
423
|
this.#warnings.push("Malformed lifecycle ledger entry quarantined");
|
|
206
424
|
}
|
|
425
|
+
async #syncDirectory(): Promise<void> {
|
|
426
|
+
const directory = await fs.open(path.dirname(this.#file), fsSync.constants.O_RDONLY);
|
|
427
|
+
try {
|
|
428
|
+
await directory.sync();
|
|
429
|
+
} finally {
|
|
430
|
+
await directory.close();
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
async #compact(replacement?: LifecycleLedgerEntry): Promise<boolean> {
|
|
434
|
+
const anchors = new Map<string, LifecycleLedgerEntry>();
|
|
435
|
+
for (const entry of this.#entries) {
|
|
436
|
+
const latest = replacement?.identity === entry.identity ? replacement : this.#byIdentity.get(entry.identity);
|
|
437
|
+
if (entry.state === "accepted" && !anchors.has(entry.identity) && entry.requestHash === latest?.requestHash)
|
|
438
|
+
anchors.set(entry.identity, entry);
|
|
439
|
+
}
|
|
440
|
+
const snapshot: LifecycleLedgerEntry[] = [];
|
|
441
|
+
const compacted = new Map(this.#byIdentity);
|
|
442
|
+
if (replacement) compacted.set(replacement.identity, replacement);
|
|
443
|
+
for (const [identity, latest] of compacted) {
|
|
444
|
+
const anchor = anchors.get(identity);
|
|
445
|
+
if (!anchor) throw new Error("Lifecycle ledger compaction requires an accepted identity anchor.");
|
|
446
|
+
snapshot.push(anchor);
|
|
447
|
+
if (latest.state !== "accepted") snapshot.push(latest);
|
|
448
|
+
}
|
|
449
|
+
const contents = Buffer.from(snapshot.map(entry => `${JSON.stringify(entry)}\n`).join(""));
|
|
450
|
+
if (
|
|
451
|
+
snapshot.length > this.#limits.maxRows ||
|
|
452
|
+
contents.length > this.#limits.maxBytes ||
|
|
453
|
+
snapshot.some(entry => Buffer.byteLength(JSON.stringify(entry)) > this.#limits.maxLineBytes)
|
|
454
|
+
)
|
|
455
|
+
throw new Error("Lifecycle ledger compaction exceeds configured bounds.");
|
|
456
|
+
const temporary = path.join(
|
|
457
|
+
path.dirname(this.#file),
|
|
458
|
+
`.lifecycle-ledger.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`,
|
|
459
|
+
);
|
|
460
|
+
let renamed = false;
|
|
461
|
+
try {
|
|
462
|
+
const h = await fs.open(
|
|
463
|
+
temporary,
|
|
464
|
+
fsSync.constants.O_WRONLY |
|
|
465
|
+
fsSync.constants.O_CREAT |
|
|
466
|
+
fsSync.constants.O_EXCL |
|
|
467
|
+
fsSync.constants.O_NOFOLLOW,
|
|
468
|
+
0o600,
|
|
469
|
+
);
|
|
470
|
+
try {
|
|
471
|
+
await h.writeFile(contents);
|
|
472
|
+
await h.sync();
|
|
473
|
+
} finally {
|
|
474
|
+
await h.close();
|
|
475
|
+
}
|
|
476
|
+
await fs.rename(temporary, this.#file);
|
|
477
|
+
renamed = true;
|
|
478
|
+
await this.#syncDirectory();
|
|
479
|
+
this.#entries = snapshot;
|
|
480
|
+
this.#byIdentity = compacted;
|
|
481
|
+
this.#rowCount = snapshot.length;
|
|
482
|
+
this.#byteCount = contents.length;
|
|
483
|
+
} finally {
|
|
484
|
+
if (!renamed) await fs.unlink(temporary).catch(() => {});
|
|
485
|
+
}
|
|
486
|
+
return replacement !== undefined;
|
|
487
|
+
}
|
|
207
488
|
get warnings(): readonly string[] {
|
|
208
489
|
return this.#warnings;
|
|
209
490
|
}
|
|
210
491
|
async #append(entry: LifecycleLedgerEntry): Promise<LifecycleLedgerEntry> {
|
|
211
|
-
const
|
|
492
|
+
const line = Buffer.from(`${JSON.stringify(entry)}\n`);
|
|
493
|
+
if (line.length - 1 > this.#limits.maxLineBytes)
|
|
494
|
+
throw new Error("Lifecycle ledger row exceeds the maximum byte length.");
|
|
495
|
+
let replacementCompacted = false;
|
|
496
|
+
if (this.#rowCount + 1 > this.#limits.maxRows || this.#byteCount + line.length > this.#limits.maxBytes)
|
|
497
|
+
replacementCompacted = await this.#compact(this.#byIdentity.has(entry.identity) ? entry : undefined);
|
|
498
|
+
if (replacementCompacted) return entry;
|
|
499
|
+
if (this.#rowCount + 1 > this.#limits.maxRows || this.#byteCount + line.length > this.#limits.maxBytes)
|
|
500
|
+
throw new Error("Lifecycle ledger append exceeds configured bounds.");
|
|
501
|
+
const h = await this.#openAppendRegular(this.#file);
|
|
212
502
|
try {
|
|
213
|
-
await h.writeFile(
|
|
503
|
+
await h.writeFile(line);
|
|
214
504
|
await h.sync();
|
|
215
505
|
} finally {
|
|
216
506
|
await h.close();
|
|
217
507
|
}
|
|
218
508
|
this.#entries.push(entry);
|
|
219
509
|
this.#byIdentity.set(entry.identity, entry);
|
|
510
|
+
this.#rowCount += 1;
|
|
511
|
+
this.#byteCount += line.length;
|
|
220
512
|
return entry;
|
|
221
513
|
}
|
|
222
514
|
async begin(identity: string, requestHash: string): Promise<BeginResult> {
|
|
515
|
+
return this.#mutate(async () => this.#begin(identity, requestHash));
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
async #begin(identity: string, requestHash: string): Promise<BeginResult> {
|
|
223
519
|
const prior = this.#byIdentity.get(identity);
|
|
224
520
|
if (!prior)
|
|
225
521
|
return {
|
|
@@ -233,7 +529,8 @@ export class LifecycleLedger {
|
|
|
233
529
|
}),
|
|
234
530
|
};
|
|
235
531
|
if (prior.requestHash !== requestHash) return { kind: "idempotency_conflict" };
|
|
236
|
-
if (terminal(prior.state)
|
|
532
|
+
if (terminal(prior.state) || (prior.state === "effect_started" && this.#isCleanupPending(prior)))
|
|
533
|
+
return { kind: "replay", entry: prior };
|
|
237
534
|
if (prior.state === "terminal_uncertain") return { kind: "terminal_uncertain", entry: prior };
|
|
238
535
|
// An accepted row has no durable side effect. Target serialization makes retrying it safe.
|
|
239
536
|
if (prior.state === "accepted") return { kind: "new", entry: prior };
|
|
@@ -244,42 +541,42 @@ export class LifecycleLedger {
|
|
|
244
541
|
state: LifecycleState,
|
|
245
542
|
fields: Omit<Partial<LifecycleLedgerEntry>, "identity" | "requestHash" | "state" | "ts"> = {},
|
|
246
543
|
): Promise<LifecycleLedgerEntry> {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
(
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
544
|
+
return this.#mutate(async () => {
|
|
545
|
+
const previous = this.#byIdentity.get(identity);
|
|
546
|
+
if (!previous) throw new Error("Unknown lifecycle identity");
|
|
547
|
+
const next = { ...previous, ...fields, state, ts: Date.now() };
|
|
548
|
+
if (
|
|
549
|
+
(terminal(state) || state === "terminal_uncertain") &&
|
|
550
|
+
next.response !== undefined &&
|
|
551
|
+
next.responseDigest === undefined
|
|
552
|
+
)
|
|
553
|
+
next.responseDigest = createHash("sha256").update(canonicalJson(next.response)).digest("hex");
|
|
554
|
+
if (next.durableEffects && next.durableEffects.digest === undefined) {
|
|
555
|
+
const { digest: _digest, ...body } = next.durableEffects;
|
|
556
|
+
next.durableEffects = {
|
|
557
|
+
...body,
|
|
558
|
+
digest: createHash("sha256").update(canonicalJson(body)).digest("hex"),
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
return this.#append(next);
|
|
562
|
+
});
|
|
264
563
|
}
|
|
265
564
|
async assertSupportedStateVersions(): Promise<void> {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
if (
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
if (!line) continue;
|
|
565
|
+
const source = await this.#readBoundedSource();
|
|
566
|
+
if (!source) return;
|
|
567
|
+
let lineStart = 0;
|
|
568
|
+
for (let offset = 0; offset <= source.length; offset += 1) {
|
|
569
|
+
if (offset !== source.length && source[offset] !== 0x0a) continue;
|
|
570
|
+
const line = source.subarray(lineStart, offset);
|
|
571
|
+
lineStart = offset + 1;
|
|
572
|
+
if (line.length === 0 || line.length > this.#limits.maxLineBytes) continue;
|
|
275
573
|
try {
|
|
276
|
-
assertSupportedStateVersion(this.#file,
|
|
574
|
+
assertSupportedStateVersion(this.#file, parseLifecycleJson(line));
|
|
277
575
|
} catch (error) {
|
|
278
576
|
if (error instanceof Error && "code" in error && error.code === "unsupported_state_version") throw error;
|
|
279
577
|
}
|
|
280
578
|
}
|
|
281
579
|
}
|
|
282
|
-
|
|
283
580
|
get(identity: string): LifecycleLedgerEntry | undefined {
|
|
284
581
|
return this.#byIdentity.get(identity);
|
|
285
582
|
}
|