@gajae-code/coding-agent 0.11.0 → 0.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +63 -1
- package/dist/types/async/job-manager.d.ts +46 -7
- package/dist/types/cli/config-cli.d.ts +11 -1
- 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 +56 -1
- 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-receipt-freshness.d.ts +11 -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 +3 -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/command-controller.d.ts +2 -2
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -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 +21 -19
- package/dist/types/modes/jobs-observer.d.ts +2 -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 +21 -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 +22 -15
- 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/ensure.d.ts +9 -0
- 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/discord-daemon.d.ts +5 -0
- package/dist/types/sdk/bus/html-format.d.ts +3 -2
- package/dist/types/sdk/bus/index.d.ts +27 -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 +69 -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/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 +3 -0
- package/dist/types/tools/search-tool-bm25.d.ts +3 -4
- package/dist/types/tools/subagent-render.d.ts +1 -0
- package/dist/types/tools/subagent.d.ts +6 -1
- package/dist/types/utils/event-bus.d.ts +1 -1
- package/dist/types/web/search/index.d.ts +3 -1
- package/package.json +21 -9
- package/scripts/build-sdk-package-smoke.ts +39 -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 +455 -69
- package/src/cli/config-cli.ts +75 -2
- 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 +159 -2
- 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 +320 -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 +83 -12
- package/src/gjc-runtime/ultragoal-pipeline.ts +306 -0
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +102 -1
- package/src/gjc-runtime/ultragoal-runtime.ts +156 -1319
- 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 +28 -8
- 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/command-controller.ts +9 -15
- package/src/modes/controllers/event-controller.ts +3 -2
- package/src/modes/controllers/extension-ui-controller.ts +72 -25
- 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 +302 -1151
- package/src/modes/jobs-observer.ts +3 -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 +78 -19
- 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 +47 -20
- 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 +21 -2
- package/src/sdk/broker/ensure.ts +85 -25
- 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 +14 -4
- 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/discord-daemon.ts +25 -8
- package/src/sdk/bus/html-format.ts +20 -3
- package/src/sdk/bus/index.ts +834 -113
- package/src/sdk/bus/lifecycle-control-runtime.ts +91 -33
- package/src/sdk/bus/native-runtime-compatibility.ts +1 -1
- 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 +1491 -247
- 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 +31 -1
- 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 +458 -479
- package/src/session/agent-session.ts +1901 -637
- 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 +62 -37
- 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 +13 -5
- package/src/tools/read.ts +37 -1
- package/src/tools/resource-gc.ts +2 -2
- package/src/tools/search-tool-bm25.ts +11 -74
- package/src/tools/subagent-render.ts +19 -8
- package/src/tools/subagent.ts +84 -32
- package/src/utils/event-bus.ts +9 -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
|
@@ -0,0 +1,962 @@
|
|
|
1
|
+
/** File-backed worker lifecycle, heartbeat, stale-claim recovery, and shutdown state. */
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import * as fs from "node:fs/promises";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import type {
|
|
6
|
+
GjcTeamConfig,
|
|
7
|
+
GjcTeamLivenessRecoveryReason,
|
|
8
|
+
GjcTeamLivenessRecoveryResult,
|
|
9
|
+
GjcTeamPhase,
|
|
10
|
+
GjcTeamRecoveredClaim,
|
|
11
|
+
GjcTeamShutdownMode,
|
|
12
|
+
GjcTeamSnapshot,
|
|
13
|
+
GjcTeamWorker,
|
|
14
|
+
GjcTeamWorkerLifecycle,
|
|
15
|
+
GjcTeamWorkerLifecycleState,
|
|
16
|
+
GjcWorkerStatusState,
|
|
17
|
+
WorkerHeartbeatFile,
|
|
18
|
+
WorkerStatusFile,
|
|
19
|
+
} from "./team-runtime";
|
|
20
|
+
import type { GjcTeamTask, GjcTeamTaskClaim, GjcTeamTaskMutationCapability } from "./team-store";
|
|
21
|
+
import {
|
|
22
|
+
getGjcTeamTaskCompletionEvidenceFailure,
|
|
23
|
+
isCanonicalPersistedGjcTeamTask,
|
|
24
|
+
isCanonicalPersistedGjcTeamTaskClaim,
|
|
25
|
+
isGjcTeamTaskCompletionVerified,
|
|
26
|
+
normalizeGjcTeamTask,
|
|
27
|
+
} from "./team-store";
|
|
28
|
+
|
|
29
|
+
export interface GjcTeamWorkerLifecycleContext {
|
|
30
|
+
config: GjcTeamConfig;
|
|
31
|
+
worker: GjcTeamWorker;
|
|
32
|
+
cwd: string;
|
|
33
|
+
env: NodeJS.ProcessEnv;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface GjcTeamWorkerRuntime {
|
|
37
|
+
findTeamDir(teamName: string, cwd: string, env: NodeJS.ProcessEnv): Promise<string>;
|
|
38
|
+
readConfig(dir: string): Promise<GjcTeamConfig>;
|
|
39
|
+
assertKnownWorker(config: GjcTeamConfig, worker: string): void;
|
|
40
|
+
assertKnownParticipant(config: GjcTeamConfig, worker: string): void;
|
|
41
|
+
findKnownWorker(config: GjcTeamConfig, worker: string): GjcTeamWorker;
|
|
42
|
+
workerDir(dir: string, worker: string): string;
|
|
43
|
+
readJson<T>(filePath: string): Promise<T | null>;
|
|
44
|
+
writeJson(filePath: string, value: unknown): Promise<void>;
|
|
45
|
+
appendEvent(
|
|
46
|
+
dir: string,
|
|
47
|
+
event: { type: string; worker?: string; task_id?: string; message: string; data?: Record<string, unknown> },
|
|
48
|
+
): Promise<unknown>;
|
|
49
|
+
now(): string;
|
|
50
|
+
nowMs(): number;
|
|
51
|
+
stableHash(value: string): string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface GjcTeamWorkerOrchestrationRuntime extends GjcTeamWorkerRuntime {
|
|
55
|
+
readTasks(dir: string): Promise<GjcTeamTask[]>;
|
|
56
|
+
withTaskMutation<T>(dir: string, fn: (capability: GjcTeamTaskMutationCapability) => Promise<T>): Promise<T>;
|
|
57
|
+
appendTelemetry(
|
|
58
|
+
dir: string,
|
|
59
|
+
event: { type: string; message: string; data?: Record<string, unknown> },
|
|
60
|
+
): Promise<unknown>;
|
|
61
|
+
paneBelongsToTeamTarget(config: GjcTeamConfig, paneId: string): boolean;
|
|
62
|
+
parseDurationEnv(env: NodeJS.ProcessEnv, name: string, fallback: number): number;
|
|
63
|
+
parseHeartbeatStaleMs(env: NodeJS.ProcessEnv): number;
|
|
64
|
+
killWorkerPanes(config: GjcTeamConfig): void;
|
|
65
|
+
removeCleanCreatedWorktrees(workers: GjcTeamWorker[]): Promise<void>;
|
|
66
|
+
readMonitorSnapshot(dir: string): Promise<unknown>;
|
|
67
|
+
hasPendingIntegration(dir: string, config: GjcTeamConfig, monitor: unknown): Promise<boolean>;
|
|
68
|
+
writePhase(dir: string, phase: GjcTeamPhase): Promise<void>;
|
|
69
|
+
readSnapshot(teamName: string, cwd: string, env: NodeJS.ProcessEnv): Promise<GjcTeamSnapshot>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function workerLifecycleContext(
|
|
73
|
+
config: GjcTeamConfig,
|
|
74
|
+
worker: GjcTeamWorker,
|
|
75
|
+
cwd: string,
|
|
76
|
+
env: NodeJS.ProcessEnv,
|
|
77
|
+
): GjcTeamWorkerLifecycleContext {
|
|
78
|
+
return { config, worker, cwd, env };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const lifecyclePath = (runtime: GjcTeamWorkerRuntime, dir: string, worker: string) =>
|
|
82
|
+
path.join(runtime.workerDir(dir, worker), "lifecycle.json");
|
|
83
|
+
const statusPath = (runtime: GjcTeamWorkerRuntime, dir: string, worker: string) =>
|
|
84
|
+
path.join(runtime.workerDir(dir, worker), "status.json");
|
|
85
|
+
const heartbeatPath = (runtime: GjcTeamWorkerRuntime, dir: string, worker: string) =>
|
|
86
|
+
path.join(runtime.workerDir(dir, worker), "heartbeat.json");
|
|
87
|
+
|
|
88
|
+
async function readRuntimeJson<T>(runtime: GjcTeamWorkerRuntime, filePath: string): Promise<T | null> {
|
|
89
|
+
try {
|
|
90
|
+
return await runtime.readJson<T>(filePath);
|
|
91
|
+
} catch {
|
|
92
|
+
// Mutable worker records are operator-visible state. Treat malformed records as absent
|
|
93
|
+
// so continuation fails closed while liveness reconciliation can still recover claims.
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export type GjcShutdownAuthority =
|
|
99
|
+
| { state: "proven_absent" }
|
|
100
|
+
| { state: "valid_present" }
|
|
101
|
+
| { state: "invalid_or_unreadable" };
|
|
102
|
+
|
|
103
|
+
export function isGjcShutdownRequestRecord(value: unknown): boolean {
|
|
104
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
105
|
+
const record = value as Record<string, unknown>;
|
|
106
|
+
return (
|
|
107
|
+
typeof record.worker === "string" &&
|
|
108
|
+
record.worker.length > 0 &&
|
|
109
|
+
typeof record.requested_by === "string" &&
|
|
110
|
+
record.requested_by.length > 0 &&
|
|
111
|
+
typeof record.request_id === "string" &&
|
|
112
|
+
record.request_id.length > 0 &&
|
|
113
|
+
(record.mode === "graceful" || record.mode === "force" || record.mode === "abort") &&
|
|
114
|
+
typeof record.requested_at === "string" &&
|
|
115
|
+
Number.isFinite(Date.parse(record.requested_at))
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function isEnoent(error: unknown): boolean {
|
|
120
|
+
return typeof error === "object" && error !== null && (error as { code?: unknown }).code === "ENOENT";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Distinguishes a missing shutdown record from a present JSON `null` or invalid record. */
|
|
124
|
+
export async function readGjcShutdownAuthority(
|
|
125
|
+
runtime: GjcTeamWorkerRuntime,
|
|
126
|
+
filePath: string,
|
|
127
|
+
): Promise<GjcShutdownAuthority> {
|
|
128
|
+
try {
|
|
129
|
+
await fs.access(filePath);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
if (isEnoent(error)) return { state: "proven_absent" };
|
|
132
|
+
return { state: "invalid_or_unreadable" };
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
const value = await runtime.readJson<unknown>(filePath);
|
|
136
|
+
return isGjcShutdownRequestRecord(value) ? { state: "valid_present" } : { state: "invalid_or_unreadable" };
|
|
137
|
+
} catch {
|
|
138
|
+
return { state: "invalid_or_unreadable" };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function parseGjcWorkerStatusState(value: unknown): GjcWorkerStatusState {
|
|
143
|
+
return typeof value === "string" &&
|
|
144
|
+
["idle", "working", "blocked", "done", "failed", "draining", "unknown"].includes(value)
|
|
145
|
+
? (value as GjcWorkerStatusState)
|
|
146
|
+
: "unknown";
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function lifecycleStateForWorkerStatus(status: GjcWorkerStatusState): GjcTeamWorkerLifecycleState {
|
|
150
|
+
switch (status) {
|
|
151
|
+
case "working":
|
|
152
|
+
return "working";
|
|
153
|
+
case "draining":
|
|
154
|
+
return "draining";
|
|
155
|
+
case "failed":
|
|
156
|
+
return "failed";
|
|
157
|
+
case "unknown":
|
|
158
|
+
return "unknown";
|
|
159
|
+
case "idle":
|
|
160
|
+
case "blocked":
|
|
161
|
+
case "done":
|
|
162
|
+
return "ready";
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function readWorkerStatusFile(
|
|
167
|
+
runtime: GjcTeamWorkerRuntime,
|
|
168
|
+
dir: string,
|
|
169
|
+
worker: string,
|
|
170
|
+
): Promise<WorkerStatusFile> {
|
|
171
|
+
return (
|
|
172
|
+
(await readRuntimeJson<WorkerStatusFile>(runtime, statusPath(runtime, dir, worker))) ?? {
|
|
173
|
+
state: "unknown",
|
|
174
|
+
updated_at: runtime.now(),
|
|
175
|
+
}
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function readWorkerLifecycleRecord(
|
|
180
|
+
runtime: GjcTeamWorkerRuntime,
|
|
181
|
+
dir: string,
|
|
182
|
+
worker: GjcTeamWorker,
|
|
183
|
+
): Promise<GjcTeamWorkerLifecycle> {
|
|
184
|
+
const workerStatus = await readWorkerStatusFile(runtime, dir, worker.id);
|
|
185
|
+
const heartbeat = await readRuntimeJson<WorkerHeartbeatFile>(runtime, heartbeatPath(runtime, dir, worker.id));
|
|
186
|
+
const raw = await readRuntimeJson<Partial<GjcTeamWorkerLifecycle>>(runtime, lifecyclePath(runtime, dir, worker.id));
|
|
187
|
+
const shutdownAck = await readRuntimeJson<Record<string, unknown>>(
|
|
188
|
+
runtime,
|
|
189
|
+
path.join(runtime.workerDir(dir, worker.id), "shutdown-ack.json"),
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const lifecycle: GjcTeamWorkerLifecycle = {
|
|
193
|
+
worker: worker.id,
|
|
194
|
+
lifecycle_state: parseLifecycleState(raw?.lifecycle_state),
|
|
195
|
+
worker_status_state: parseGjcWorkerStatusState(workerStatus.state),
|
|
196
|
+
pane_id: worker.pane_id ?? raw?.pane_id,
|
|
197
|
+
updated_at: raw?.updated_at ?? workerStatus.updated_at ?? runtime.now(),
|
|
198
|
+
};
|
|
199
|
+
if (typeof raw?.pid === "number") lifecycle.pid = raw.pid;
|
|
200
|
+
else if (typeof heartbeat?.pid === "number") lifecycle.pid = heartbeat.pid;
|
|
201
|
+
if (raw?.started_at) lifecycle.started_at = raw.started_at;
|
|
202
|
+
if (raw?.stopped_at) lifecycle.stopped_at = raw.stopped_at;
|
|
203
|
+
if (raw?.stop_reason) lifecycle.stop_reason = raw.stop_reason;
|
|
204
|
+
if (raw?.shutdown_request_id) lifecycle.shutdown_request_id = raw.shutdown_request_id;
|
|
205
|
+
if (raw?.shutdown_requested_at) lifecycle.shutdown_requested_at = raw.shutdown_requested_at;
|
|
206
|
+
if (raw?.shutdown_mode === "graceful" || raw?.shutdown_mode === "force" || raw?.shutdown_mode === "abort")
|
|
207
|
+
lifecycle.shutdown_mode = raw.shutdown_mode;
|
|
208
|
+
if (typeof shutdownAck?.acknowledged_at === "string")
|
|
209
|
+
lifecycle.shutdown_acknowledged_at = shutdownAck.acknowledged_at;
|
|
210
|
+
if (typeof shutdownAck?.status === "string") lifecycle.shutdown_ack_status = shutdownAck.status;
|
|
211
|
+
return lifecycle;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const parseLifecycleState = (value: unknown): GjcTeamWorkerLifecycleState =>
|
|
215
|
+
typeof value === "string" &&
|
|
216
|
+
["starting", "ready", "working", "draining", "stopped", "failed", "unknown"].includes(value)
|
|
217
|
+
? (value as GjcTeamWorkerLifecycleState)
|
|
218
|
+
: "unknown";
|
|
219
|
+
|
|
220
|
+
export async function readWorkerLifecycleById(
|
|
221
|
+
runtime: GjcTeamWorkerRuntime,
|
|
222
|
+
dir: string,
|
|
223
|
+
config: GjcTeamConfig,
|
|
224
|
+
): Promise<Record<string, GjcTeamWorkerLifecycle>> {
|
|
225
|
+
const records = await Promise.all(config.workers.map(worker => readWorkerLifecycleRecord(runtime, dir, worker)));
|
|
226
|
+
return Object.fromEntries(records.map(record => [record.worker, record]));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export async function writeWorkerLifecycleRecord(
|
|
230
|
+
runtime: GjcTeamWorkerRuntime,
|
|
231
|
+
dir: string,
|
|
232
|
+
worker: GjcTeamWorker,
|
|
233
|
+
lifecycleState: GjcTeamWorkerLifecycleState,
|
|
234
|
+
updates: Partial<GjcTeamWorkerLifecycle> = {},
|
|
235
|
+
): Promise<GjcTeamWorkerLifecycle> {
|
|
236
|
+
const current = await readWorkerLifecycleRecord(runtime, dir, worker);
|
|
237
|
+
const next: GjcTeamWorkerLifecycle = {
|
|
238
|
+
...current,
|
|
239
|
+
...updates,
|
|
240
|
+
worker: worker.id,
|
|
241
|
+
lifecycle_state: lifecycleState,
|
|
242
|
+
worker_status_state: current.worker_status_state,
|
|
243
|
+
pane_id: updates.pane_id ?? worker.pane_id ?? current.pane_id,
|
|
244
|
+
updated_at: runtime.now(),
|
|
245
|
+
};
|
|
246
|
+
await runtime.writeJson(lifecyclePath(runtime, dir, worker.id), next);
|
|
247
|
+
return next;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export async function writeWorkerLifecycleForConfig(
|
|
251
|
+
runtime: GjcTeamWorkerRuntime,
|
|
252
|
+
dir: string,
|
|
253
|
+
config: GjcTeamConfig,
|
|
254
|
+
lifecycleState: GjcTeamWorkerLifecycleState,
|
|
255
|
+
updatesFor: (worker: GjcTeamWorker) => Partial<GjcTeamWorkerLifecycle> = () => ({}),
|
|
256
|
+
): Promise<Record<string, GjcTeamWorkerLifecycle>> {
|
|
257
|
+
const records = await Promise.all(
|
|
258
|
+
config.workers.map(worker =>
|
|
259
|
+
writeWorkerLifecycleRecord(runtime, dir, worker, lifecycleState, updatesFor(worker)),
|
|
260
|
+
),
|
|
261
|
+
);
|
|
262
|
+
return Object.fromEntries(records.map(record => [record.worker, record]));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export async function readGjcWorkerStatus(
|
|
266
|
+
runtime: GjcTeamWorkerRuntime,
|
|
267
|
+
teamName: string,
|
|
268
|
+
worker: string,
|
|
269
|
+
cwd = process.cwd(),
|
|
270
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
271
|
+
): Promise<WorkerStatusFile> {
|
|
272
|
+
const dir = await runtime.findTeamDir(teamName, cwd, env);
|
|
273
|
+
runtime.assertKnownWorker(await runtime.readConfig(dir), worker);
|
|
274
|
+
return readWorkerStatusFile(runtime, dir, worker);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export async function updateGjcWorkerStatus(
|
|
278
|
+
runtime: GjcTeamWorkerRuntime,
|
|
279
|
+
teamName: string,
|
|
280
|
+
worker: string,
|
|
281
|
+
status: GjcWorkerStatusState,
|
|
282
|
+
cwd = process.cwd(),
|
|
283
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
284
|
+
currentTaskId?: string,
|
|
285
|
+
reason?: string,
|
|
286
|
+
): Promise<WorkerStatusFile> {
|
|
287
|
+
const dir = await runtime.findTeamDir(teamName, cwd, env);
|
|
288
|
+
const config = await runtime.readConfig(dir);
|
|
289
|
+
const teamWorker = runtime.findKnownWorker(config, worker);
|
|
290
|
+
const value: WorkerStatusFile = {
|
|
291
|
+
state: status,
|
|
292
|
+
...(currentTaskId ? { current_task_id: currentTaskId } : {}),
|
|
293
|
+
...(reason?.trim() ? { reason: reason.trim() } : {}),
|
|
294
|
+
updated_at: runtime.now(),
|
|
295
|
+
};
|
|
296
|
+
await runtime.writeJson(statusPath(runtime, dir, worker), value);
|
|
297
|
+
const current = await readWorkerLifecycleRecord(runtime, dir, teamWorker);
|
|
298
|
+
await writeWorkerLifecycleRecord(
|
|
299
|
+
runtime,
|
|
300
|
+
dir,
|
|
301
|
+
teamWorker,
|
|
302
|
+
current.lifecycle_state === "stopped" ? "stopped" : lifecycleStateForWorkerStatus(status),
|
|
303
|
+
);
|
|
304
|
+
await runtime.appendEvent(dir, {
|
|
305
|
+
type: "worker_status_updated",
|
|
306
|
+
worker,
|
|
307
|
+
message: `Worker ${worker} reported ${status}`,
|
|
308
|
+
data: { status, current_task_id: currentTaskId },
|
|
309
|
+
});
|
|
310
|
+
return value;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export async function readGjcWorkerHeartbeat(
|
|
314
|
+
runtime: GjcTeamWorkerRuntime,
|
|
315
|
+
teamName: string,
|
|
316
|
+
worker: string,
|
|
317
|
+
cwd = process.cwd(),
|
|
318
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
319
|
+
): Promise<WorkerHeartbeatFile | null> {
|
|
320
|
+
const dir = await runtime.findTeamDir(teamName, cwd, env);
|
|
321
|
+
runtime.assertKnownWorker(await runtime.readConfig(dir), worker);
|
|
322
|
+
return readRuntimeJson<WorkerHeartbeatFile>(runtime, heartbeatPath(runtime, dir, worker));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export async function updateGjcWorkerHeartbeat(
|
|
326
|
+
runtime: GjcTeamWorkerRuntime,
|
|
327
|
+
teamName: string,
|
|
328
|
+
worker: string,
|
|
329
|
+
heartbeat: WorkerHeartbeatFile,
|
|
330
|
+
cwd = process.cwd(),
|
|
331
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
332
|
+
): Promise<WorkerHeartbeatFile> {
|
|
333
|
+
const dir = await runtime.findTeamDir(teamName, cwd, env);
|
|
334
|
+
runtime.assertKnownWorker(await runtime.readConfig(dir), worker);
|
|
335
|
+
const value = { ...heartbeat, last_turn_at: heartbeat.last_turn_at || runtime.now() };
|
|
336
|
+
await runtime.writeJson(heartbeatPath(runtime, dir, worker), value);
|
|
337
|
+
return value;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export async function writeGjcWorkerStartupAck(
|
|
341
|
+
runtime: GjcTeamWorkerRuntime,
|
|
342
|
+
teamName: string,
|
|
343
|
+
worker: string,
|
|
344
|
+
cwd: string,
|
|
345
|
+
env: NodeJS.ProcessEnv,
|
|
346
|
+
input: Record<string, unknown>,
|
|
347
|
+
): Promise<Record<string, unknown>> {
|
|
348
|
+
const dir = await runtime.findTeamDir(teamName, cwd, env);
|
|
349
|
+
const teamWorker = runtime.findKnownWorker(await runtime.readConfig(dir), worker);
|
|
350
|
+
const ack = {
|
|
351
|
+
worker,
|
|
352
|
+
pid: typeof input.pid === "number" ? input.pid : undefined,
|
|
353
|
+
session: typeof input.session === "string" ? input.session : undefined,
|
|
354
|
+
protocol_version: String(input.protocol_version ?? "1"),
|
|
355
|
+
ack_at: runtime.now(),
|
|
356
|
+
};
|
|
357
|
+
await runtime.writeJson(path.join(runtime.workerDir(dir, worker), "startup-ack.json"), ack);
|
|
358
|
+
await writeWorkerLifecycleRecord(runtime, dir, teamWorker, "ready", {
|
|
359
|
+
pane_id: teamWorker.pane_id,
|
|
360
|
+
pid: typeof input.pid === "number" ? input.pid : undefined,
|
|
361
|
+
started_at: ack.ack_at,
|
|
362
|
+
});
|
|
363
|
+
await runtime.appendEvent(dir, {
|
|
364
|
+
type: "worker_startup_ack",
|
|
365
|
+
worker,
|
|
366
|
+
message: `Worker ${worker} acknowledged startup`,
|
|
367
|
+
});
|
|
368
|
+
return ack;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export async function writeGjcShutdownRequest(
|
|
372
|
+
runtime: GjcTeamWorkerRuntime,
|
|
373
|
+
teamName: string,
|
|
374
|
+
worker: string,
|
|
375
|
+
requestedBy: string,
|
|
376
|
+
cwd = process.cwd(),
|
|
377
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
378
|
+
requestId: string,
|
|
379
|
+
mode: GjcTeamShutdownMode = "graceful",
|
|
380
|
+
requestedAt = runtime.now(),
|
|
381
|
+
): Promise<Record<string, unknown>> {
|
|
382
|
+
const dir = await runtime.findTeamDir(teamName, cwd, env);
|
|
383
|
+
const config = await runtime.readConfig(dir);
|
|
384
|
+
const teamWorker = runtime.findKnownWorker(config, worker);
|
|
385
|
+
runtime.assertKnownParticipant(config, requestedBy);
|
|
386
|
+
const value = { worker, requested_by: requestedBy, request_id: requestId, mode, requested_at: requestedAt };
|
|
387
|
+
await runtime.writeJson(path.join(runtime.workerDir(dir, worker), "shutdown-request.json"), value);
|
|
388
|
+
await writeWorkerLifecycleRecord(runtime, dir, teamWorker, "draining", {
|
|
389
|
+
shutdown_request_id: requestId,
|
|
390
|
+
shutdown_requested_at: requestedAt,
|
|
391
|
+
shutdown_mode: mode,
|
|
392
|
+
});
|
|
393
|
+
await runtime.appendEvent(dir, {
|
|
394
|
+
type: "worker_shutdown_requested",
|
|
395
|
+
worker,
|
|
396
|
+
message: `Worker ${worker} shutdown requested`,
|
|
397
|
+
data: { requested_by: requestedBy, request_id: requestId, mode },
|
|
398
|
+
});
|
|
399
|
+
return value;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export async function readGjcShutdownAck(
|
|
403
|
+
runtime: GjcTeamWorkerRuntime,
|
|
404
|
+
teamName: string,
|
|
405
|
+
worker: string,
|
|
406
|
+
cwd = process.cwd(),
|
|
407
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
408
|
+
): Promise<Record<string, unknown> | null> {
|
|
409
|
+
const dir = await runtime.findTeamDir(teamName, cwd, env);
|
|
410
|
+
runtime.assertKnownWorker(await runtime.readConfig(dir), worker);
|
|
411
|
+
return runtime.readJson<Record<string, unknown>>(path.join(runtime.workerDir(dir, worker), "shutdown-ack.json"));
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function addRecoveryReason(reasons: GjcTeamLivenessRecoveryReason[], reason: GjcTeamLivenessRecoveryReason): void {
|
|
415
|
+
if (!reasons.includes(reason)) reasons.push(reason);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function claimFromUnknown(value: unknown): GjcTeamTaskClaim | undefined {
|
|
419
|
+
if (typeof value !== "object" || value == null) return undefined;
|
|
420
|
+
const record = value as Record<string, unknown>;
|
|
421
|
+
const owner = typeof record.owner === "string" ? record.owner : "";
|
|
422
|
+
const token = typeof record.token === "string" ? record.token : "";
|
|
423
|
+
const leasedUntil = typeof record.leased_until === "string" ? record.leased_until : "";
|
|
424
|
+
return owner && token && leasedUntil ? { owner, token, leased_until: leasedUntil } : undefined;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async function hasCurrentContinuationClaimAuthority(
|
|
428
|
+
runtime: GjcTeamWorkerOrchestrationRuntime,
|
|
429
|
+
dir: string,
|
|
430
|
+
worker: string,
|
|
431
|
+
task: GjcTeamTask,
|
|
432
|
+
claim: GjcTeamTaskClaim,
|
|
433
|
+
): Promise<boolean> {
|
|
434
|
+
let canonicalClaim: unknown;
|
|
435
|
+
let canonicalTask: unknown;
|
|
436
|
+
try {
|
|
437
|
+
canonicalClaim = await runtime.readJson<unknown>(path.join(dir, "claims", `${task.id}.json`));
|
|
438
|
+
canonicalTask = await runtime.readJson<unknown>(path.join(dir, "tasks", `${task.id}.json`));
|
|
439
|
+
} catch {
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
if (!isCanonicalPersistedGjcTeamTaskClaim(canonicalClaim)) return false;
|
|
443
|
+
if (
|
|
444
|
+
canonicalClaim.owner !== claim.owner ||
|
|
445
|
+
canonicalClaim.token !== claim.token ||
|
|
446
|
+
canonicalClaim.leased_until !== claim.leased_until
|
|
447
|
+
)
|
|
448
|
+
return false;
|
|
449
|
+
if (!isCanonicalPersistedGjcTeamTask(canonicalTask, task.id)) return false;
|
|
450
|
+
return (
|
|
451
|
+
canonicalTask.version === task.version &&
|
|
452
|
+
canonicalTask.status === "in_progress" &&
|
|
453
|
+
canonicalTask.owner === worker &&
|
|
454
|
+
canonicalTask.assignee === worker &&
|
|
455
|
+
canonicalTask.claim?.owner === canonicalClaim.owner &&
|
|
456
|
+
canonicalTask.claim?.token === canonicalClaim.token &&
|
|
457
|
+
canonicalTask.claim?.leased_until === canonicalClaim.leased_until
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function claimIsExpired(value: string | undefined, nowMs: number): boolean {
|
|
462
|
+
if (!value) return false;
|
|
463
|
+
const timestamp = Date.parse(value);
|
|
464
|
+
return Number.isFinite(timestamp) && timestamp <= nowMs;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async function livenessReasons(
|
|
468
|
+
runtime: GjcTeamWorkerOrchestrationRuntime,
|
|
469
|
+
dir: string,
|
|
470
|
+
config: GjcTeamConfig,
|
|
471
|
+
worker: GjcTeamWorker,
|
|
472
|
+
env: NodeJS.ProcessEnv,
|
|
473
|
+
): Promise<GjcTeamLivenessRecoveryReason[]> {
|
|
474
|
+
const reasons: GjcTeamLivenessRecoveryReason[] = [];
|
|
475
|
+
const lifecycle = await readWorkerLifecycleRecord(runtime, dir, worker);
|
|
476
|
+
const heartbeat = await readRuntimeJson<WorkerHeartbeatFile>(runtime, heartbeatPath(runtime, dir, worker.id));
|
|
477
|
+
|
|
478
|
+
if (lifecycle.lifecycle_state === "failed") addRecoveryReason(reasons, "worker_lifecycle_failed");
|
|
479
|
+
if (lifecycle.lifecycle_state === "stopped") addRecoveryReason(reasons, "worker_lifecycle_stopped");
|
|
480
|
+
const staleMs = runtime.parseHeartbeatStaleMs(env);
|
|
481
|
+
const heartbeatAt = Date.parse(heartbeat?.last_turn_at ?? worker.last_heartbeat);
|
|
482
|
+
if (staleMs > 0 && Number.isFinite(heartbeatAt) && runtime.nowMs() - heartbeatAt >= staleMs)
|
|
483
|
+
addRecoveryReason(reasons, "stale_heartbeat");
|
|
484
|
+
if (
|
|
485
|
+
!config.dry_run &&
|
|
486
|
+
(!worker.pane_id?.startsWith("%") || !runtime.paneBelongsToTeamTarget(config, worker.pane_id))
|
|
487
|
+
)
|
|
488
|
+
addRecoveryReason(reasons, "missing_pane");
|
|
489
|
+
return reasons;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function isValidRecordedWorkerStatus(status: WorkerStatusFile): boolean {
|
|
493
|
+
return (
|
|
494
|
+
typeof status === "object" &&
|
|
495
|
+
status !== null &&
|
|
496
|
+
["idle", "working", "blocked", "done", "failed", "draining", "unknown"].includes(status.state) &&
|
|
497
|
+
typeof status.updated_at === "string" &&
|
|
498
|
+
Number.isFinite(Date.parse(status.updated_at))
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function isContinuationLifecycleEligible(lifecycle: GjcTeamWorkerLifecycle, status: WorkerStatusFile): boolean {
|
|
503
|
+
return (
|
|
504
|
+
isValidRecordedWorkerStatus(status) &&
|
|
505
|
+
(lifecycle.lifecycle_state === "ready" || lifecycle.lifecycle_state === "working") &&
|
|
506
|
+
status.state !== "draining" &&
|
|
507
|
+
status.state !== "failed" &&
|
|
508
|
+
status.state !== "unknown"
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
export const GJC_TEAM_CONTINUATION_PROMPT =
|
|
513
|
+
"Continue only your current claimed GJC team task. Re-read current GJC team state; do not replay prior output; report status.";
|
|
514
|
+
|
|
515
|
+
function canonicalJson(value: unknown): string {
|
|
516
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
517
|
+
if (typeof value === "object" && value !== null) {
|
|
518
|
+
const record = value as Record<string, unknown>;
|
|
519
|
+
return `{${Object.keys(record)
|
|
520
|
+
.sort()
|
|
521
|
+
.map(key => `${JSON.stringify(key)}:${canonicalJson(record[key])}`)
|
|
522
|
+
.join(",")}}`;
|
|
523
|
+
}
|
|
524
|
+
return JSON.stringify(value) ?? "null";
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export function gjcContinuationReservationDigest(reservation: Record<string, unknown>): string {
|
|
528
|
+
return createHash("sha256").update(canonicalJson(reservation)).digest("hex");
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const reservationKeys = new Set([
|
|
532
|
+
"schema_version",
|
|
533
|
+
"incident_hash",
|
|
534
|
+
"team_name",
|
|
535
|
+
"worker",
|
|
536
|
+
"task_id",
|
|
537
|
+
"owner",
|
|
538
|
+
"claim_token",
|
|
539
|
+
"task_version",
|
|
540
|
+
"leased_until",
|
|
541
|
+
"heartbeat_at",
|
|
542
|
+
"pane_id",
|
|
543
|
+
"tmux_target",
|
|
544
|
+
"attempt",
|
|
545
|
+
"reserved_at",
|
|
546
|
+
"hold_until",
|
|
547
|
+
"prompt_version",
|
|
548
|
+
"prompt_sha256",
|
|
549
|
+
"dispatch_protocol",
|
|
550
|
+
]);
|
|
551
|
+
|
|
552
|
+
export function isValidGjcContinuationReservation(
|
|
553
|
+
value: unknown,
|
|
554
|
+
incident: string,
|
|
555
|
+
attempt: number,
|
|
556
|
+
config: GjcTeamConfig,
|
|
557
|
+
worker: string,
|
|
558
|
+
task: GjcTeamTask,
|
|
559
|
+
claim: GjcTeamTaskClaim,
|
|
560
|
+
heartbeatAt: string,
|
|
561
|
+
paneId: string,
|
|
562
|
+
): value is Record<string, unknown> {
|
|
563
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
564
|
+
const record = value as Record<string, unknown>;
|
|
565
|
+
if (
|
|
566
|
+
Object.keys(record).length !== reservationKeys.size ||
|
|
567
|
+
Object.keys(record).some(key => !reservationKeys.has(key))
|
|
568
|
+
)
|
|
569
|
+
return false;
|
|
570
|
+
const reservedAt = typeof record.reserved_at === "string" ? Date.parse(record.reserved_at) : Number.NaN;
|
|
571
|
+
const holdUntil = typeof record.hold_until === "string" ? Date.parse(record.hold_until) : Number.NaN;
|
|
572
|
+
const leaseUntil = Date.parse(claim.leased_until);
|
|
573
|
+
return (
|
|
574
|
+
record.schema_version === 1 &&
|
|
575
|
+
record.incident_hash === incident &&
|
|
576
|
+
record.team_name === config.team_name &&
|
|
577
|
+
record.worker === worker &&
|
|
578
|
+
record.task_id === task.id &&
|
|
579
|
+
record.owner === claim.owner &&
|
|
580
|
+
record.claim_token === claim.token &&
|
|
581
|
+
record.task_version === task.version &&
|
|
582
|
+
record.leased_until === claim.leased_until &&
|
|
583
|
+
record.heartbeat_at === heartbeatAt &&
|
|
584
|
+
record.pane_id === paneId &&
|
|
585
|
+
record.tmux_target === config.tmux_target &&
|
|
586
|
+
record.attempt === attempt &&
|
|
587
|
+
record.prompt_version === 1 &&
|
|
588
|
+
record.dispatch_protocol === "tmux_command_sequence_v1" &&
|
|
589
|
+
record.prompt_sha256 === createHash("sha256").update(GJC_TEAM_CONTINUATION_PROMPT).digest("hex") &&
|
|
590
|
+
Number.isFinite(reservedAt) &&
|
|
591
|
+
Number.isFinite(holdUntil) &&
|
|
592
|
+
Number.isFinite(leaseUntil) &&
|
|
593
|
+
holdUntil - reservedAt === (attempt === 1 ? 30_000 : 120_000) &&
|
|
594
|
+
leaseUntil >= holdUntil
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
export function isValidGjcContinuationOutcome(
|
|
599
|
+
value: unknown,
|
|
600
|
+
reservation: Record<string, unknown>,
|
|
601
|
+
incident: string,
|
|
602
|
+
attempt: number,
|
|
603
|
+
): value is Record<string, unknown> {
|
|
604
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
605
|
+
const outcome = value as Record<string, unknown>;
|
|
606
|
+
if (
|
|
607
|
+
outcome.schema_version !== 1 ||
|
|
608
|
+
outcome.incident_hash !== incident ||
|
|
609
|
+
outcome.attempt !== attempt ||
|
|
610
|
+
outcome.reservation_sha256 !== gjcContinuationReservationDigest(reservation) ||
|
|
611
|
+
typeof outcome.recorded_at !== "string" ||
|
|
612
|
+
!Number.isFinite(Date.parse(outcome.recorded_at)) ||
|
|
613
|
+
typeof outcome.reason !== "string" ||
|
|
614
|
+
outcome.reason.length === 0 ||
|
|
615
|
+
outcome.reason.length > 160
|
|
616
|
+
)
|
|
617
|
+
return false;
|
|
618
|
+
const exit = outcome.tmux_exit_code;
|
|
619
|
+
const error = outcome.tmux_error;
|
|
620
|
+
const validError =
|
|
621
|
+
typeof error === "object" &&
|
|
622
|
+
error !== null &&
|
|
623
|
+
Object.keys(error).length >= 2 &&
|
|
624
|
+
Object.keys(error).length <= 3 &&
|
|
625
|
+
Object.keys(error).every(key => key === "name" || key === "code" || key === "message") &&
|
|
626
|
+
typeof (error as Record<string, unknown>).name === "string" &&
|
|
627
|
+
typeof (error as Record<string, unknown>).message === "string" &&
|
|
628
|
+
String((error as Record<string, unknown>).name).length > 0 &&
|
|
629
|
+
String((error as Record<string, unknown>).message).length > 0 &&
|
|
630
|
+
String((error as Record<string, unknown>).name).length <= 120 &&
|
|
631
|
+
String((error as Record<string, unknown>).message).length <= 400 &&
|
|
632
|
+
((error as Record<string, unknown>).code === undefined ||
|
|
633
|
+
(typeof (error as Record<string, unknown>).code === "string" &&
|
|
634
|
+
String((error as Record<string, unknown>).code).length > 0 &&
|
|
635
|
+
String((error as Record<string, unknown>).code).length <= 120));
|
|
636
|
+
const baseKeys = [
|
|
637
|
+
"schema_version",
|
|
638
|
+
"incident_hash",
|
|
639
|
+
"attempt",
|
|
640
|
+
"reservation_sha256",
|
|
641
|
+
"recorded_at",
|
|
642
|
+
"result",
|
|
643
|
+
"reason",
|
|
644
|
+
];
|
|
645
|
+
const hasExactKeys = (keys: readonly string[]) =>
|
|
646
|
+
Object.keys(outcome).length === keys.length && Object.keys(outcome).every(key => keys.includes(key));
|
|
647
|
+
if (outcome.result === "sent") {
|
|
648
|
+
const dispatchedAt = Date.parse(String(outcome.dispatched_at));
|
|
649
|
+
const holdUntil = Date.parse(String(outcome.hold_until));
|
|
650
|
+
const leaseUntil = Date.parse(String(reservation.leased_until));
|
|
651
|
+
return (
|
|
652
|
+
hasExactKeys([...baseKeys, "tmux_exit_code", "dispatched_at", "hold_until"]) &&
|
|
653
|
+
outcome.reason === "tmux_sent" &&
|
|
654
|
+
exit === 0 &&
|
|
655
|
+
Number.isFinite(dispatchedAt) &&
|
|
656
|
+
Number.isFinite(holdUntil) &&
|
|
657
|
+
Number.isFinite(leaseUntil) &&
|
|
658
|
+
holdUntil - dispatchedAt === (attempt === 1 ? 30_000 : 120_000) &&
|
|
659
|
+
leaseUntil >= holdUntil
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
if (outcome.result === "skipped")
|
|
663
|
+
return (
|
|
664
|
+
hasExactKeys(baseKeys) &&
|
|
665
|
+
[
|
|
666
|
+
"invalid_or_absent_running_phase",
|
|
667
|
+
"invalid_pane_authority",
|
|
668
|
+
"shutdown_requested",
|
|
669
|
+
"invalid_shutdown_authority",
|
|
670
|
+
"heartbeat_changed",
|
|
671
|
+
"invalid_worker_lifecycle_or_status",
|
|
672
|
+
"claim_changed",
|
|
673
|
+
"invalid_or_expired_lease",
|
|
674
|
+
"invalid_authority_inventory",
|
|
675
|
+
"lease_does_not_cover_hold",
|
|
676
|
+
"unsupported_send_keys_transport",
|
|
677
|
+
].includes(outcome.reason)
|
|
678
|
+
);
|
|
679
|
+
if (outcome.result !== "unknown") return false;
|
|
680
|
+
return (
|
|
681
|
+
(outcome.reason === "tmux_missing_exit_code" &&
|
|
682
|
+
exit === undefined &&
|
|
683
|
+
error === undefined &&
|
|
684
|
+
hasExactKeys(baseKeys)) ||
|
|
685
|
+
(outcome.reason === "tmux_nonzero_exit" &&
|
|
686
|
+
typeof exit === "number" &&
|
|
687
|
+
Number.isInteger(exit) &&
|
|
688
|
+
exit !== 0 &&
|
|
689
|
+
error === undefined &&
|
|
690
|
+
hasExactKeys([...baseKeys, "tmux_exit_code"])) ||
|
|
691
|
+
(outcome.reason === "tmux_dispatch_threw" &&
|
|
692
|
+
exit === undefined &&
|
|
693
|
+
validError &&
|
|
694
|
+
hasExactKeys([...baseKeys, "tmux_error"]))
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
async function hasActiveContinuationHold(
|
|
699
|
+
runtime: GjcTeamWorkerOrchestrationRuntime,
|
|
700
|
+
dir: string,
|
|
701
|
+
worker: string,
|
|
702
|
+
config: GjcTeamConfig,
|
|
703
|
+
task: GjcTeamTask,
|
|
704
|
+
claim: GjcTeamTaskClaim,
|
|
705
|
+
): Promise<boolean> {
|
|
706
|
+
if (!(await hasCurrentContinuationClaimAuthority(runtime, dir, worker, task, claim))) return false;
|
|
707
|
+
const heartbeat = await readRuntimeJson<WorkerHeartbeatFile>(runtime, heartbeatPath(runtime, dir, worker));
|
|
708
|
+
|
|
709
|
+
const teamWorker = config.workers.find(candidate => candidate.id === worker);
|
|
710
|
+
const paneId = teamWorker?.pane_id;
|
|
711
|
+
const lifecycle = teamWorker ? await readWorkerLifecycleRecord(runtime, dir, teamWorker) : undefined;
|
|
712
|
+
const status = await readWorkerStatusFile(runtime, dir, worker);
|
|
713
|
+
const phase = await readRuntimeJson<Record<string, unknown>>(runtime, path.join(dir, "phase.json"));
|
|
714
|
+
const shutdownAuthority = await readGjcShutdownAuthority(
|
|
715
|
+
runtime,
|
|
716
|
+
path.join(runtime.workerDir(dir, worker), "shutdown-request.json"),
|
|
717
|
+
);
|
|
718
|
+
|
|
719
|
+
if (
|
|
720
|
+
!heartbeat?.last_turn_at ||
|
|
721
|
+
!paneId ||
|
|
722
|
+
!lifecycle ||
|
|
723
|
+
!isContinuationLifecycleEligible(lifecycle, status) ||
|
|
724
|
+
phase?.current_phase !== "running" ||
|
|
725
|
+
shutdownAuthority.state !== "proven_absent"
|
|
726
|
+
)
|
|
727
|
+
return false;
|
|
728
|
+
const incident = runtime.stableHash(
|
|
729
|
+
[
|
|
730
|
+
config.team_name,
|
|
731
|
+
worker,
|
|
732
|
+
task.id,
|
|
733
|
+
claim.owner,
|
|
734
|
+
claim.token,
|
|
735
|
+
task.version,
|
|
736
|
+
heartbeat.last_turn_at,
|
|
737
|
+
paneId,
|
|
738
|
+
config.tmux_target,
|
|
739
|
+
].join(":"),
|
|
740
|
+
);
|
|
741
|
+
const continuations = path.join(runtime.workerDir(dir, worker), "continuations", incident);
|
|
742
|
+
for (const attempt of [1, 2]) {
|
|
743
|
+
const reservation = await readRuntimeJson<Record<string, unknown>>(
|
|
744
|
+
runtime,
|
|
745
|
+
path.join(continuations, `attempt-0${attempt}.reservation.json`),
|
|
746
|
+
);
|
|
747
|
+
|
|
748
|
+
if (
|
|
749
|
+
!isValidGjcContinuationReservation(
|
|
750
|
+
reservation,
|
|
751
|
+
incident,
|
|
752
|
+
attempt,
|
|
753
|
+
config,
|
|
754
|
+
worker,
|
|
755
|
+
task,
|
|
756
|
+
claim,
|
|
757
|
+
heartbeat.last_turn_at,
|
|
758
|
+
paneId,
|
|
759
|
+
)
|
|
760
|
+
)
|
|
761
|
+
return false;
|
|
762
|
+
const reservationHoldUntil = Date.parse(String(reservation.hold_until));
|
|
763
|
+
if (!Number.isFinite(reservationHoldUntil)) return false;
|
|
764
|
+
const outcomePath = path.join(continuations, `attempt-0${attempt}.outcome.json`);
|
|
765
|
+
let outcome: Record<string, unknown> | null;
|
|
766
|
+
try {
|
|
767
|
+
outcome = await runtime.readJson<Record<string, unknown>>(outcomePath);
|
|
768
|
+
} catch (error) {
|
|
769
|
+
if (!isEnoent(error)) return false;
|
|
770
|
+
outcome = null;
|
|
771
|
+
}
|
|
772
|
+
const nowMs = runtime.nowMs();
|
|
773
|
+
// Only proven ENOENT is crash-ambiguous. Present JSON null, malformed, or unreadable
|
|
774
|
+
// outcomes are invalid authority and must not retain a recovery hold.
|
|
775
|
+
if (outcome === null) {
|
|
776
|
+
try {
|
|
777
|
+
await fs.access(outcomePath);
|
|
778
|
+
} catch (error) {
|
|
779
|
+
if (!isEnoent(error)) return false;
|
|
780
|
+
if (reservationHoldUntil <= nowMs) continue;
|
|
781
|
+
if (claimIsExpired(claim.leased_until, nowMs)) return false;
|
|
782
|
+
return true;
|
|
783
|
+
}
|
|
784
|
+
return false;
|
|
785
|
+
}
|
|
786
|
+
if (!isValidGjcContinuationOutcome(outcome, reservation, incident, attempt)) return false;
|
|
787
|
+
if (outcome.result === "skipped") continue;
|
|
788
|
+
const holdUntil = outcome.result === "sent" ? Date.parse(String(outcome.hold_until)) : reservationHoldUntil;
|
|
789
|
+
if (!Number.isFinite(holdUntil) || holdUntil <= nowMs) continue;
|
|
790
|
+
if (claimIsExpired(claim.leased_until, nowMs)) return false;
|
|
791
|
+
return true;
|
|
792
|
+
}
|
|
793
|
+
return false;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
export async function reconcileGjcTeamStaleClaims(
|
|
797
|
+
runtime: GjcTeamWorkerOrchestrationRuntime,
|
|
798
|
+
teamName: string,
|
|
799
|
+
dir: string,
|
|
800
|
+
config: GjcTeamConfig,
|
|
801
|
+
env: NodeJS.ProcessEnv,
|
|
802
|
+
): Promise<GjcTeamLivenessRecoveryResult> {
|
|
803
|
+
return runtime.withTaskMutation(dir, capability =>
|
|
804
|
+
reconcileGjcTeamStaleClaimsUnlocked(runtime, teamName, dir, config, env, capability),
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
export async function reconcileGjcTeamStaleClaimsUnlocked(
|
|
809
|
+
runtime: GjcTeamWorkerOrchestrationRuntime,
|
|
810
|
+
teamName: string,
|
|
811
|
+
dir: string,
|
|
812
|
+
config: GjcTeamConfig,
|
|
813
|
+
env: NodeJS.ProcessEnv,
|
|
814
|
+
capability: GjcTeamTaskMutationCapability,
|
|
815
|
+
): Promise<GjcTeamLivenessRecoveryResult> {
|
|
816
|
+
const staleWorkers: Record<string, GjcTeamLivenessRecoveryReason[]> = {};
|
|
817
|
+
for (const worker of config.workers) {
|
|
818
|
+
const reasons = await livenessReasons(runtime, dir, config, worker, env);
|
|
819
|
+
if (reasons.length === 0) continue;
|
|
820
|
+
staleWorkers[worker.id] = reasons;
|
|
821
|
+
if (reasons.includes("missing_pane") && !reasons.includes("worker_lifecycle_stopped")) {
|
|
822
|
+
try {
|
|
823
|
+
await fs.access(runtime.workerDir(dir, worker.id));
|
|
824
|
+
await writeWorkerLifecycleRecord(runtime, dir, worker, "failed", { stop_reason: "pane_missing" });
|
|
825
|
+
} catch (error) {
|
|
826
|
+
if (!isEnoent(error)) throw error;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
const recoveredClaims: GjcTeamRecoveredClaim[] = [];
|
|
831
|
+
for (const task of await runtime.readTasks(dir)) {
|
|
832
|
+
if (task.status === "completed" || task.status === "failed") continue;
|
|
833
|
+
const claimPath = path.join(dir, "claims", `${task.id}.json`);
|
|
834
|
+
const claim = task.claim ?? claimFromUnknown(await readRuntimeJson<unknown>(runtime, claimPath));
|
|
835
|
+
if (!claim) continue;
|
|
836
|
+
const reasons = [...(staleWorkers[claim.owner] ?? [])];
|
|
837
|
+
if (claimIsExpired(claim.leased_until, runtime.nowMs())) addRecoveryReason(reasons, "claim_expired");
|
|
838
|
+
if (reasons.length === 0) continue;
|
|
839
|
+
if (
|
|
840
|
+
reasons.length === 1 &&
|
|
841
|
+
reasons[0] === "stale_heartbeat" &&
|
|
842
|
+
(await hasActiveContinuationHold(runtime, dir, claim.owner, config, task, claim))
|
|
843
|
+
)
|
|
844
|
+
continue;
|
|
845
|
+
await fs.rm(claimPath, { force: true });
|
|
846
|
+
recoveredClaims.push({ task_id: task.id, worker: claim.owner, reasons });
|
|
847
|
+
if (task.status === "in_progress")
|
|
848
|
+
await capability.writeRecovered(
|
|
849
|
+
normalizeGjcTeamTask({
|
|
850
|
+
...task,
|
|
851
|
+
status: "pending",
|
|
852
|
+
assignee: undefined,
|
|
853
|
+
claim: undefined,
|
|
854
|
+
version: task.version + 1,
|
|
855
|
+
updated_at: runtime.now(),
|
|
856
|
+
}),
|
|
857
|
+
);
|
|
858
|
+
await runtime.appendEvent(dir, {
|
|
859
|
+
type: "task_claim_recovered",
|
|
860
|
+
task_id: task.id,
|
|
861
|
+
worker: claim.owner,
|
|
862
|
+
message:
|
|
863
|
+
task.status === "in_progress" ? "Recovered task from stale worker claim" : "Removed stale task claim file",
|
|
864
|
+
data: { reasons },
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
if (recoveredClaims.length > 0)
|
|
868
|
+
await runtime.appendTelemetry(dir, {
|
|
869
|
+
type: "team_liveness_recovery",
|
|
870
|
+
message: `Recovered ${recoveredClaims.length} stale team task claim(s)`,
|
|
871
|
+
data: { team_name: teamName, recovered_claims: recoveredClaims },
|
|
872
|
+
});
|
|
873
|
+
return { recovered_claims: recoveredClaims, stale_workers: staleWorkers };
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
export async function shutdownGjcTeamWorkers(
|
|
877
|
+
runtime: GjcTeamWorkerOrchestrationRuntime,
|
|
878
|
+
teamName: string,
|
|
879
|
+
cwd: string,
|
|
880
|
+
env: NodeJS.ProcessEnv,
|
|
881
|
+
): Promise<GjcTeamSnapshot> {
|
|
882
|
+
const dir = await runtime.findTeamDir(teamName, cwd, env);
|
|
883
|
+
const config = await runtime.readConfig(dir);
|
|
884
|
+
const tasks = await runtime.readTasks(dir);
|
|
885
|
+
const evidenceFailures = tasks.flatMap(task => {
|
|
886
|
+
const reason = task.status === "completed" ? getGjcTeamTaskCompletionEvidenceFailure(task) : null;
|
|
887
|
+
return reason ? [{ task_id: task.id, reason }] : [];
|
|
888
|
+
});
|
|
889
|
+
const requestId = `shutdown-${runtime.stableHash([config.team_name, runtime.now(), crypto.randomUUID()].join(":"))}`;
|
|
890
|
+
const requestedAt = runtime.now();
|
|
891
|
+
await Promise.all(
|
|
892
|
+
config.workers.map(worker =>
|
|
893
|
+
writeGjcShutdownRequest(
|
|
894
|
+
runtime,
|
|
895
|
+
teamName,
|
|
896
|
+
worker.id,
|
|
897
|
+
"leader-fixed",
|
|
898
|
+
cwd,
|
|
899
|
+
env,
|
|
900
|
+
requestId,
|
|
901
|
+
"graceful",
|
|
902
|
+
requestedAt,
|
|
903
|
+
),
|
|
904
|
+
),
|
|
905
|
+
);
|
|
906
|
+
const completionVerified = tasks.length === 0 || tasks.every(isGjcTeamTaskCompletionVerified);
|
|
907
|
+
const pendingIntegration = completionVerified
|
|
908
|
+
? await runtime.hasPendingIntegration(dir, config, await runtime.readMonitorSnapshot(dir))
|
|
909
|
+
: false;
|
|
910
|
+
runtime.killWorkerPanes(config);
|
|
911
|
+
await runtime.removeCleanCreatedWorktrees(config.workers);
|
|
912
|
+
const stopped: GjcTeamConfig = {
|
|
913
|
+
...config,
|
|
914
|
+
workers: config.workers.map(worker => ({ ...worker, status: "stopped", last_heartbeat: runtime.now() })),
|
|
915
|
+
updated_at: runtime.now(),
|
|
916
|
+
};
|
|
917
|
+
await runtime.writeJson(path.join(dir, "config.json"), stopped);
|
|
918
|
+
await writeWorkerLifecycleForConfig(runtime, dir, stopped, "stopped", worker => ({
|
|
919
|
+
pane_id: worker.pane_id,
|
|
920
|
+
stopped_at: stopped.updated_at,
|
|
921
|
+
stop_reason: "graceful_shutdown",
|
|
922
|
+
shutdown_request_id: requestId,
|
|
923
|
+
shutdown_requested_at: requestedAt,
|
|
924
|
+
shutdown_mode: "graceful",
|
|
925
|
+
}));
|
|
926
|
+
const lifecycle = await readWorkerLifecycleById(runtime, dir, stopped);
|
|
927
|
+
const graceful = stopped.workers.every(
|
|
928
|
+
worker =>
|
|
929
|
+
lifecycle[worker.id]?.lifecycle_state === "stopped" &&
|
|
930
|
+
lifecycle[worker.id]?.shutdown_request_id === requestId &&
|
|
931
|
+
lifecycle[worker.id]?.shutdown_mode === "graceful",
|
|
932
|
+
);
|
|
933
|
+
const phase: GjcTeamPhase =
|
|
934
|
+
completionVerified && graceful
|
|
935
|
+
? pendingIntegration
|
|
936
|
+
? "awaiting_integration"
|
|
937
|
+
: "complete"
|
|
938
|
+
: evidenceFailures.length > 0 || tasks.some(task => task.status === "failed" || task.status === "blocked")
|
|
939
|
+
? "failed"
|
|
940
|
+
: "cancelled";
|
|
941
|
+
await runtime.writePhase(dir, phase);
|
|
942
|
+
const data: Record<string, unknown> = {
|
|
943
|
+
phase,
|
|
944
|
+
shutdown_request_id: requestId,
|
|
945
|
+
graceful_shutdown_complete: graceful,
|
|
946
|
+
};
|
|
947
|
+
if (evidenceFailures.length > 0) data.evidence_failures = evidenceFailures;
|
|
948
|
+
await runtime.appendEvent(dir, {
|
|
949
|
+
type: "team_shutdown",
|
|
950
|
+
message:
|
|
951
|
+
phase === "complete"
|
|
952
|
+
? "Shut down native gjc team runtime after completed tasks"
|
|
953
|
+
: "Shut down native gjc team runtime with incomplete tasks",
|
|
954
|
+
data,
|
|
955
|
+
});
|
|
956
|
+
await runtime.appendTelemetry(dir, {
|
|
957
|
+
type: "team_shutdown",
|
|
958
|
+
message: `Native gjc team runtime stopped with phase ${phase}`,
|
|
959
|
+
data: { shutdown_request_id: requestId, graceful_shutdown_complete: graceful },
|
|
960
|
+
});
|
|
961
|
+
return runtime.readSnapshot(config.team_name, cwd, env);
|
|
962
|
+
}
|