@gajae-code/coding-agent 0.14.2 → 0.15.0
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 +120 -1
- package/dist/types/async/job-manager.d.ts +4 -1
- package/dist/types/autoresearch/dashboard.d.ts +29 -4
- package/dist/types/autoresearch/data-context.d.ts +25 -0
- package/dist/types/autoresearch/git.d.ts +65 -7
- package/dist/types/autoresearch/harness.d.ts +58 -0
- package/dist/types/autoresearch/prompts.d.ts +51 -0
- package/dist/types/autoresearch/runs.d.ts +173 -0
- package/dist/types/autoresearch/types.d.ts +9 -139
- package/dist/types/capability/types.d.ts +20 -0
- package/dist/types/cli/accounts-cli.d.ts +1 -0
- package/dist/types/commands/{rlm.d.ts → autoresearch.d.ts} +2 -4
- package/dist/types/config/autorouting-contract.d.ts +146 -0
- package/dist/types/config/autorouting-generator.d.ts +40 -0
- package/dist/types/config/autorouting-tier-map.d.ts +16844 -0
- package/dist/types/config/autorouting.d.ts +35 -0
- package/dist/types/config/model-discovery-manager.d.ts +8 -1
- package/dist/types/config/model-registry.d.ts +30 -1
- package/dist/types/config/provider-selection-policy.d.ts +23 -0
- package/dist/types/config/settings-schema.d.ts +61 -3
- package/dist/types/config/settings.d.ts +3 -0
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/coordinator-mcp/durability.d.ts +29 -0
- package/dist/types/coordinator-mcp/event-webhook.d.ts +50 -0
- package/dist/types/coordinator-mcp/question-state.d.ts +1 -1
- package/dist/types/coordinator-mcp/server.d.ts +14 -0
- package/dist/types/crash/index-store.d.ts +3 -0
- package/dist/types/crash/record-loader.d.ts +2 -0
- package/dist/types/crash/upstream/relay.d.ts +5 -3
- package/dist/types/cursor.d.ts +12 -0
- package/dist/types/defaults/gjc-defaults.d.ts +35 -1
- package/dist/types/defaults/gjc-skills.generated.d.ts +1 -1
- package/dist/types/discovery/helpers.d.ts +1 -1
- package/dist/types/edit/index.d.ts +2 -1
- package/dist/types/eval/py/executor.d.ts +2 -0
- package/dist/types/extensibility/extensions/loader.d.ts +32 -0
- package/dist/types/extensibility/gjc-plugins/types.d.ts +1 -1
- package/dist/types/extensibility/skills.d.ts +2 -0
- package/dist/types/gjc-runtime/autoresearch-runtime.d.ts +194 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
- package/dist/types/gjc-runtime/psmux-detect.d.ts +1 -1
- package/dist/types/gjc-runtime/python-transcript.d.ts +19 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +2 -8
- package/dist/types/gjc-runtime/session-layout.d.ts +6 -3
- package/dist/types/gjc-runtime/state-renderer.d.ts +0 -22
- package/dist/types/gjc-runtime/state-schema.d.ts +7 -7
- package/dist/types/gjc-runtime/tmux-common.d.ts +2 -2
- package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +38 -1
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +2 -24
- package/dist/types/gjc-runtime/ultragoal-validation-policy.d.ts +73 -0
- package/dist/types/gjc-runtime/workflow-recovery-projection.d.ts +100 -0
- package/dist/types/hooks/skill-keywords.d.ts +1 -1
- package/dist/types/main.d.ts +0 -10
- package/dist/types/modes/acp/acp-agent.d.ts +13 -6
- package/dist/types/modes/components/model-selector.d.ts +14 -0
- package/dist/types/modes/components/skill-hud/render.d.ts +6 -1
- package/dist/types/modes/components/smart-routing-panel.d.ts +82 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +14 -0
- package/dist/types/modes/interactive-mode.d.ts +3 -0
- package/dist/types/modes/types.d.ts +9 -0
- package/dist/types/rlm/artifacts.d.ts +4 -4
- package/dist/types/runtime/optional-runtime-services.d.ts +2 -1
- package/dist/types/runtime/workspace-tree-service.d.ts +5 -1
- package/dist/types/runtime-mcp/config.d.ts +6 -0
- package/dist/types/runtime-mcp/manager.d.ts +10 -0
- package/dist/types/sdk/acp/adapter.d.ts +0 -4
- package/dist/types/sdk/broker/broker.d.ts +1 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +57 -2
- package/dist/types/sdk/broker/session-index.d.ts +15 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +6 -0
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +4 -0
- package/dist/types/sdk/bus/index.d.ts +8 -0
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +12 -2
- package/dist/types/sdk/client/client.d.ts +39 -4
- package/dist/types/sdk/host/host.d.ts +1 -0
- package/dist/types/sdk/host/internal-autorouting-state.d.ts +3 -0
- package/dist/types/sdk/host/model-pin.d.ts +29 -0
- package/dist/types/sdk/host/session-runtime.d.ts +2 -1
- package/dist/types/sdk/lifecycle-session.d.ts +13 -0
- package/dist/types/sdk/router/session-router.d.ts +27 -1
- package/dist/types/sdk/session.d.ts +11 -1
- package/dist/types/session/agent-session.d.ts +36 -6
- package/dist/types/session/artifacts.d.ts +16 -1
- package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
- package/dist/types/session/session-manager.d.ts +84 -2
- package/dist/types/session/session-staging-paths.d.ts +3 -0
- package/dist/types/skill-state/canonical-skills.d.ts +1 -1
- package/dist/types/skill-state/workflow-hud.d.ts +12 -21
- package/dist/types/skill-state/workflow-mutation-guard.d.ts +1 -0
- package/dist/types/slash-commands/helpers/autorouting-status.d.ts +8 -0
- package/dist/types/system-prompt.d.ts +23 -0
- package/dist/types/task/executor.d.ts +35 -4
- package/dist/types/task/index.d.ts +17 -3
- package/dist/types/task/receipt.d.ts +3 -1
- package/dist/types/task/types.d.ts +144 -0
- package/dist/types/tools/atomic-file-write.d.ts +72 -0
- package/dist/types/tools/descriptors.d.ts +1 -0
- package/dist/types/tools/index.d.ts +36 -1
- package/dist/types/tools/move-session.d.ts +40 -0
- package/dist/types/tools/python.d.ts +12 -0
- package/dist/types/tools/resource-gc.d.ts +0 -3
- package/dist/types/tools/search.d.ts +8 -1
- package/dist/types/tools/skill.d.ts +9 -4
- package/dist/types/tools/subagent.d.ts +33 -3
- package/dist/types/utils/xml-escape.d.ts +2 -0
- package/dist/types/workflow/workflow-intent-diff.d.ts +2 -2
- package/dist/types/workflow/workflow-intent-report-consumer.d.ts +1 -1
- package/package.json +11 -12
- package/scripts/build-sdk-package-smoke.ts +7 -5
- package/scripts/check-autorouting-tier-map.ts +183 -0
- package/scripts/generate-sdk-operation-inventory.ts +12 -0
- package/scripts/generate-tool-catalog.ts +40 -5
- package/src/async/job-manager.ts +5 -2
- package/src/autoresearch/dashboard.ts +134 -314
- package/src/autoresearch/data-context.ts +35 -0
- package/src/autoresearch/git.ts +186 -26
- package/src/autoresearch/{helpers.ts → harness.ts} +65 -40
- package/src/autoresearch/{prompt-setup.md → prompts/prompt-setup.md} +7 -7
- package/src/autoresearch/prompts/prompt.md +103 -0
- package/src/autoresearch/prompts.ts +126 -0
- package/src/autoresearch/runs.ts +708 -0
- package/src/autoresearch/types.ts +9 -150
- package/src/capability/index.ts +4 -4
- package/src/capability/types.ts +20 -0
- package/src/cli/accounts-cli.ts +21 -5
- package/src/cli/config-cli.ts +25 -1
- package/src/cli/fast-help.ts +1 -2
- package/src/cli/list-models.ts +5 -8
- package/src/cli/setup-cli.ts +8 -0
- package/src/cli.ts +2 -21
- package/src/commands/autoresearch.ts +33 -0
- package/src/commands/coordinator.ts +15 -0
- package/src/commands/launch.ts +2 -4
- package/src/commands/quick-lane.ts +1 -1
- package/src/commands/sdk.ts +24 -0
- package/src/commands/state.ts +1 -1
- package/src/config/autorouting-contract.ts +474 -0
- package/src/config/autorouting-generator.ts +238 -0
- package/src/config/autorouting-tier-map.ts +6108 -0
- package/src/config/autorouting.ts +120 -0
- package/src/config/model-discovery-manager.ts +42 -11
- package/src/config/model-registry.ts +348 -68
- package/src/config/provider-selection-policy.ts +60 -9
- package/src/config/settings-schema.ts +195 -6
- package/src/config/settings.ts +62 -1
- package/src/config.ts +2 -3
- package/src/coordinator/contract.ts +0 -1
- package/src/coordinator-mcp/codex-handoff.ts +68 -42
- package/src/coordinator-mcp/durability.ts +200 -0
- package/src/coordinator-mcp/event-webhook.ts +278 -0
- package/src/coordinator-mcp/question-state.ts +109 -46
- package/src/coordinator-mcp/server.ts +932 -504
- package/src/crash/index-store.ts +126 -22
- package/src/crash/record-loader.ts +7 -0
- package/src/crash/upstream/relay.ts +82 -34
- package/src/cursor.ts +84 -1
- package/src/debug/index.ts +26 -0
- package/src/defaults/gjc/skills/autoresearch/SKILL.md +116 -0
- package/src/defaults/gjc/skills/autoresearch/auto-critic.md +46 -0
- package/src/defaults/gjc/skills/autoresearch/auto-iterate.md +42 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +17 -37
- package/src/defaults/gjc/skills/ralplan/SKILL.md +21 -18
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +6 -17
- package/src/defaults/gjc/skills/ultragoal/ai-slop-cleaner.md +2 -2
- package/src/defaults/gjc-defaults.ts +89 -2
- package/src/defaults/gjc-skills.generated.ts +24 -17
- package/src/discovery/builtin.ts +18 -5
- package/src/discovery/helpers.ts +6 -6
- package/src/edit/index.ts +5 -2
- package/src/eval/py/executor.ts +8 -0
- package/src/extensibility/extensions/loader.ts +135 -13
- package/src/extensibility/extensions/wrapper.ts +1 -2
- package/src/extensibility/gjc-plugins/schema.ts +1 -1
- package/src/extensibility/runtime-skill-discovery.ts +2 -2
- package/src/extensibility/skill-management.ts +2 -3
- package/src/extensibility/skills.ts +16 -8
- package/src/gjc-runtime/autoresearch-runtime.ts +1509 -0
- package/src/gjc-runtime/gc-render.ts +0 -1
- package/src/gjc-runtime/gc-runtime.ts +1 -11
- package/src/gjc-runtime/launch-tmux.ts +3 -3
- package/src/gjc-runtime/psmux-detect.ts +2 -2
- package/src/gjc-runtime/python-transcript.ts +58 -0
- package/src/gjc-runtime/ralplan-runtime.ts +6 -18
- package/src/gjc-runtime/session-layout.ts +18 -7
- package/src/gjc-runtime/session-state-sidecar.ts +58 -9
- package/src/gjc-runtime/state-renderer.ts +0 -38
- package/src/gjc-runtime/state-runtime.ts +43 -22
- package/src/gjc-runtime/state-schema.ts +1 -1
- package/src/gjc-runtime/tmux-common.ts +7 -7
- package/src/gjc-runtime/ultragoal-change-set.ts +232 -11
- package/src/gjc-runtime/ultragoal-runtime.ts +309 -114
- package/src/gjc-runtime/ultragoal-validation-policy.ts +247 -0
- package/src/gjc-runtime/workflow-command-ref.ts +15 -11
- package/src/gjc-runtime/workflow-manifest.generated.json +413 -532
- package/src/gjc-runtime/workflow-manifest.ts +43 -83
- package/src/gjc-runtime/workflow-recovery-projection.ts +616 -0
- package/src/hooks/skill-keywords.ts +3 -3
- package/src/hooks/skill-state.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +26 -24
- package/src/lsp/index.ts +25 -5
- package/src/main.ts +2 -27
- package/src/modes/acp/acp-agent.ts +200 -75
- package/src/modes/components/model-selector.ts +216 -13
- package/src/modes/components/oauth-selector.ts +5 -5
- package/src/modes/components/skill-hud/render.ts +173 -35
- package/src/modes/components/smart-routing-panel.ts +531 -0
- package/src/modes/components/tool-status-header.ts +6 -3
- package/src/modes/controllers/command-controller.ts +17 -7
- package/src/modes/controllers/input-controller.ts +44 -9
- package/src/modes/controllers/runtime-mcp-command-controller.ts +4 -0
- package/src/modes/controllers/selector-controller.ts +298 -65
- package/src/modes/interactive-mode.ts +8 -1
- package/src/modes/shared/agent-wire/workflow-gate-types.ts +1 -1
- package/src/modes/types.ts +9 -1
- package/src/prompts/agents/planner.md +1 -1
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/prompts/system/volatile-project-context.md +2 -1
- package/src/prompts/tools/move-session.md +15 -0
- package/src/prompts/tools/python.md +12 -0
- package/src/prompts/tools/skill-discovery.md +1 -1
- package/src/prompts/tools/skill.md +2 -2
- package/src/prompts/tools/task-summary.md +1 -0
- package/src/prompts/tools/task.md +6 -0
- package/src/rlm/artifacts.ts +12 -10
- package/src/runtime/optional-runtime-services.ts +3 -2
- package/src/runtime/workspace-tree-service.ts +11 -3
- package/src/runtime-mcp/config.ts +8 -1
- package/src/runtime-mcp/manager.ts +33 -0
- package/src/sdk/acp/adapter.ts +48 -1
- package/src/sdk/broker/broker.ts +30 -2
- package/src/sdk/broker/discovery.ts +217 -2
- package/src/sdk/broker/lifecycle.ts +385 -59
- package/src/sdk/broker/session-index.ts +243 -48
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/bus/chat-daemon-control.ts +8 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +5 -0
- package/src/sdk/bus/index.ts +208 -11
- package/src/sdk/bus/kind-aware-reconciliation.ts +16 -0
- package/src/sdk/bus/reconciliation-store.ts +27 -5
- package/src/sdk/bus/telegram-daemon-contract.ts +12 -2
- package/src/sdk/client/client.ts +204 -22
- package/src/sdk/host/host.ts +16 -0
- package/src/sdk/host/internal-autorouting-state.ts +13 -0
- package/src/sdk/host/model-pin.ts +101 -0
- package/src/sdk/host/reverse-leases.ts +2 -1
- package/src/sdk/host/session-runtime.test.ts +224 -1
- package/src/sdk/host/session-runtime.ts +125 -37
- package/src/sdk/lifecycle/service.ts +3 -1
- package/src/sdk/lifecycle-session.ts +42 -1
- package/src/sdk/protocol/operation-inventory.generated.json +66 -0
- package/src/sdk/router/session-router.ts +375 -36
- package/src/sdk/session.ts +547 -47
- package/src/session/account-inventory.ts +1 -1
- package/src/session/agent-session.ts +765 -189
- package/src/session/artifacts.ts +234 -4
- package/src/session/internal/managed-session-storage.ts +91 -4
- package/src/session/provider-safety-stop-hint.ts +3 -2
- package/src/session/session-manager.ts +775 -16
- package/src/session/session-staging-paths.ts +8 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +0 -1
- package/src/setup/host-plugin-setup.ts +2 -2
- package/src/skill-state/active-state.ts +2 -2
- package/src/skill-state/canonical-skills.ts +1 -1
- package/src/skill-state/initial-phase.ts +1 -1
- package/src/skill-state/workflow-hud.ts +41 -41
- package/src/skill-state/workflow-mutation-guard.ts +26 -7
- package/src/slash-commands/builtin-registry.ts +77 -0
- package/src/slash-commands/helpers/autorouting-status.ts +55 -0
- package/src/system-prompt.ts +102 -5
- package/src/task/discovery.ts +2 -3
- package/src/task/executor.ts +617 -43
- package/src/task/index.ts +262 -15
- package/src/task/receipt.ts +30 -0
- package/src/task/render.ts +11 -1
- package/src/task/types.ts +316 -0
- package/src/tools/atomic-file-write.ts +594 -0
- package/src/tools/bisect.ts +3 -2
- package/src/tools/descriptors.test.ts +3 -0
- package/src/tools/descriptors.ts +24 -0
- package/src/tools/image-gen.ts +5 -1
- package/src/tools/index.ts +62 -5
- package/src/tools/move-session.ts +107 -0
- package/src/tools/python.ts +205 -0
- package/src/tools/read.ts +88 -15
- package/src/tools/renderers.ts +2 -0
- package/src/tools/resource-gc.ts +8 -177
- package/src/tools/search.ts +30 -6
- package/src/tools/skill-discovery.ts +1 -0
- package/src/tools/skill.ts +37 -7
- package/src/tools/subagent-render.ts +119 -13
- package/src/tools/subagent.ts +91 -74
- package/src/tools/tool-catalog.generated.ts +64 -2
- package/src/tools/write.ts +16 -12
- package/src/utils/xml-escape.ts +14 -0
- package/src/web/search/providers/codex.ts +2 -2
- package/src/workflow/workflow-intent-diff.ts +13 -12
- package/src/workflow/workflow-intent-report-consumer.ts +6 -6
- package/dist/types/autoresearch/helpers.d.ts +0 -24
- package/dist/types/autoresearch/index.d.ts +0 -2
- package/dist/types/autoresearch/state.d.ts +0 -17
- package/dist/types/autoresearch/storage.d.ts +0 -142
- package/dist/types/autoresearch/tools/init-experiment.d.ts +0 -31
- package/dist/types/autoresearch/tools/log-experiment.d.ts +0 -23
- package/dist/types/autoresearch/tools/run-experiment.d.ts +0 -8
- package/dist/types/autoresearch/tools/update-notes.d.ts +0 -12
- package/dist/types/commands/team.d.ts +0 -24
- package/dist/types/gjc-runtime/team-gc.d.ts +0 -7
- package/dist/types/gjc-runtime/team-launch.d.ts +0 -61
- package/dist/types/gjc-runtime/team-notify.d.ts +0 -45
- package/dist/types/gjc-runtime/team-runtime.d.ts +0 -336
- package/dist/types/gjc-runtime/team-store.d.ts +0 -168
- package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +0 -43
- package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +0 -92
- package/dist/types/gjc-runtime/team-workers.d.ts +0 -83
- package/dist/types/rlm/index.d.ts +0 -47
- package/dist/types/rlm/preset.d.ts +0 -23
- package/dist/types/rlm/python-tool.d.ts +0 -16
- package/src/autoresearch/command-resume.md +0 -14
- package/src/autoresearch/index.ts +0 -536
- package/src/autoresearch/prompt.md +0 -103
- package/src/autoresearch/resume-message.md +0 -10
- package/src/autoresearch/state.ts +0 -273
- package/src/autoresearch/storage.ts +0 -699
- package/src/autoresearch/tools/init-experiment.ts +0 -272
- package/src/autoresearch/tools/log-experiment.ts +0 -524
- package/src/autoresearch/tools/run-experiment.ts +0 -475
- package/src/autoresearch/tools/update-notes.ts +0 -109
- package/src/commands/rlm.ts +0 -19
- package/src/commands/team.ts +0 -238
- package/src/defaults/gjc/skills/deep-interview/auto-research-greenfield.md +0 -42
- package/src/defaults/gjc/skills/team/SKILL.md +0 -467
- package/src/gjc-runtime/team-gc.ts +0 -49
- package/src/gjc-runtime/team-launch.ts +0 -404
- package/src/gjc-runtime/team-notify.ts +0 -326
- package/src/gjc-runtime/team-runtime.ts +0 -5923
- package/src/gjc-runtime/team-store.ts +0 -878
- package/src/gjc-runtime/team-worker-heartbeat.ts +0 -149
- package/src/gjc-runtime/team-worker-memory-guard.ts +0 -363
- package/src/gjc-runtime/team-workers.ts +0 -1123
- package/src/prompts/system/rlm-report-command.md +0 -1
- package/src/prompts/system/rlm-research.md +0 -23
- package/src/rlm/index.ts +0 -365
- package/src/rlm/preset.ts +0 -76
- package/src/rlm/python-tool.ts +0 -68
package/src/sdk/client/client.ts
CHANGED
|
@@ -74,9 +74,47 @@ export interface SdkRequestOptions {
|
|
|
74
74
|
timeoutMs?: number;
|
|
75
75
|
idempotencyKey?: string;
|
|
76
76
|
confirm?: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Synchronous pre-send observer for one request. Called after the
|
|
79
|
+
* connection is live and validated, immediately before the frame is written
|
|
80
|
+
* to the socket. Throwing aborts the dispatch: nothing is written, no sent
|
|
81
|
+
* record is retained, and the request rejects with the thrown error, so the
|
|
82
|
+
* caller may safely retry (pre-send semantics).
|
|
83
|
+
*/
|
|
84
|
+
beforeDispatch?: SdkBeforeDispatchHandler;
|
|
85
|
+
/**
|
|
86
|
+
* Synchronous dispatch-boundary observer for one request. Called immediately
|
|
87
|
+
* after the frame was handed to the socket — never before — and before any
|
|
88
|
+
* other client work. From this point a transport close before the response
|
|
89
|
+
* settles the request as `uncertain_after_send`; observer exceptions cannot
|
|
90
|
+
* alter that settlement.
|
|
91
|
+
*/
|
|
92
|
+
onDispatch?: SdkDispatchHandler;
|
|
77
93
|
}
|
|
78
|
-
|
|
79
94
|
export type SdkFrame = Record<string, unknown>;
|
|
95
|
+
/**
|
|
96
|
+
* Synchronous by contract. Returning a thenable (e.g. an `async` function) is a
|
|
97
|
+
* contract violation: pre-send the dispatch aborts retryably and the eventual
|
|
98
|
+
* rejection is sunk, never escaping to the process unhandled-rejection channel.
|
|
99
|
+
*/
|
|
100
|
+
export type SdkBeforeDispatchHandler = (request: SdkDispatchContext) => void;
|
|
101
|
+
/**
|
|
102
|
+
* Synchronous by contract. A returned thenable's rejection is sunk; it can
|
|
103
|
+
* neither displace request settlement nor escape to the process
|
|
104
|
+
* unhandled-rejection channel.
|
|
105
|
+
*/
|
|
106
|
+
export type SdkDispatchHandler = (request: SdkDispatchContext) => void;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Facts about one request at its dispatch boundary. `frame.id` is the exact
|
|
110
|
+
* correlated identity a response frame must carry to settle this request.
|
|
111
|
+
*/
|
|
112
|
+
export interface SdkDispatchContext {
|
|
113
|
+
readonly frame: SdkFrame;
|
|
114
|
+
/** Transport generation this request was written to. */
|
|
115
|
+
readonly connectionId: string | undefined;
|
|
116
|
+
readonly generation: number;
|
|
117
|
+
}
|
|
80
118
|
|
|
81
119
|
/** Request identity retained after an uncertain send for lifecycle reconciliation. */
|
|
82
120
|
|
|
@@ -158,6 +196,52 @@ function parseFrame(value: unknown): Frame {
|
|
|
158
196
|
}
|
|
159
197
|
throw new SdkClientError("protocol_error", "SDK server sent a malformed frame.");
|
|
160
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* Recursively freezes the parsed request frame handed to dispatch-boundary
|
|
201
|
+
* observers. The frame a consumer sees in `beforeDispatch`/`onDispatch` must be
|
|
202
|
+
* the exact immutable bytes-derived identity that went on the wire, so a
|
|
203
|
+
* mutating callback can never desynchronize the advertised identity or the
|
|
204
|
+
* reconciliation fingerprint from the request that was actually sent.
|
|
205
|
+
*/
|
|
206
|
+
function deepFreeze<T>(value: T): T {
|
|
207
|
+
if (value && typeof value === "object") {
|
|
208
|
+
for (const nested of Object.values(value as Record<string, unknown>)) deepFreeze(nested);
|
|
209
|
+
Object.freeze(value);
|
|
210
|
+
}
|
|
211
|
+
return value;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
type Thenable = { then?: unknown };
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Observers are synchronous by contract, but TypeScript happily accepts an
|
|
218
|
+
* `async` function where `() => void` is expected. A rejected promise returned
|
|
219
|
+
* by an observer would otherwise reach the process-level unhandled-rejection
|
|
220
|
+
* channel — terminating `--unhandled-rejections=strict` consumers — and a
|
|
221
|
+
* rejected `beforeDispatch` would silently not abort the dispatch. Observers
|
|
222
|
+
* that return a thenable are treated as a contract violation: detected
|
|
223
|
+
* explicitly, sunk here, and (pre-send) failed before the wire.
|
|
224
|
+
*/
|
|
225
|
+
function isThenable(value: unknown): value is Thenable {
|
|
226
|
+
return (
|
|
227
|
+
(typeof value === "object" || typeof value === "function") &&
|
|
228
|
+
value !== null &&
|
|
229
|
+
typeof (value as Thenable).then === "function"
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function sinkThenable(value: unknown): void {
|
|
234
|
+
const thenable = value as { then: (onFulfilled: unknown, onRejected: (error: unknown) => void) => unknown };
|
|
235
|
+
try {
|
|
236
|
+
thenable.then(undefined, () => {
|
|
237
|
+
// Observer rejections never displace settlement; swallow them here
|
|
238
|
+
// so they cannot escape to the process unhandled-rejection channel.
|
|
239
|
+
});
|
|
240
|
+
} catch {
|
|
241
|
+
// A throwing then-accessor already surfaced synchronously to the caller
|
|
242
|
+
// of the observer; nothing further to sink.
|
|
243
|
+
}
|
|
244
|
+
}
|
|
161
245
|
|
|
162
246
|
function lifecycleFingerprint(operation: string, input: unknown): string {
|
|
163
247
|
return createHash("sha256").update(JSON.stringify({ operation, input })).digest("hex");
|
|
@@ -255,9 +339,9 @@ export class SdkClient {
|
|
|
255
339
|
}
|
|
256
340
|
}
|
|
257
341
|
|
|
258
|
-
request(frame: SdkFrame,
|
|
259
|
-
const
|
|
260
|
-
return this.#request(frame,
|
|
342
|
+
request(frame: SdkFrame, options?: number | SdkRequestOptions): Promise<SdkFrame> {
|
|
343
|
+
const resolved = typeof options === "number" ? { timeoutMs: options } : (options ?? {});
|
|
344
|
+
return this.#request(frame, resolved) as Promise<SdkFrame>;
|
|
261
345
|
}
|
|
262
346
|
|
|
263
347
|
close(): Promise<void> {
|
|
@@ -370,7 +454,18 @@ export class SdkClient {
|
|
|
370
454
|
...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
|
|
371
455
|
};
|
|
372
456
|
const serializedRequest = JSON.stringify(requestFrame);
|
|
373
|
-
const serializedFrame = JSON.parse(serializedRequest) as Frame;
|
|
457
|
+
const serializedFrame: Frame = deepFreeze(JSON.parse(serializedRequest) as Frame);
|
|
458
|
+
// Snapshot reconciliation identity from the exact serialized bytes BEFORE any
|
|
459
|
+
// observer runs. Callers own `options`, so rereading `options.idempotencyKey`
|
|
460
|
+
// or recomputing the fingerprint after a callback would let a mutating or
|
|
461
|
+
// getter-swapping observer diverge the retained record from the wire request.
|
|
462
|
+
const sentOperation = typeof serializedFrame.operation === "string" ? serializedFrame.operation : undefined;
|
|
463
|
+
const sentIdempotencyKey =
|
|
464
|
+
typeof serializedFrame.idempotencyKey === "string" ? serializedFrame.idempotencyKey : undefined;
|
|
465
|
+
const sentFingerprint =
|
|
466
|
+
typeof serializedFrame.operation === "string"
|
|
467
|
+
? lifecycleFingerprint(serializedFrame.operation, serializedFrame.input ?? {})
|
|
468
|
+
: inputFingerprint(serializedFrame.input ?? {});
|
|
374
469
|
const deferred = Promise.withResolvers<unknown>();
|
|
375
470
|
const pending: Pending = {
|
|
376
471
|
incarnation,
|
|
@@ -397,26 +492,113 @@ export class SdkClient {
|
|
|
397
492
|
this.#settlePending(id, pending, new SdkClientError("unavailable", "SDK WebSocket is not connected"));
|
|
398
493
|
return await deferred.promise;
|
|
399
494
|
}
|
|
495
|
+
if (options.beforeDispatch) {
|
|
496
|
+
try {
|
|
497
|
+
const observerResult = options.beforeDispatch({
|
|
498
|
+
frame: serializedFrame,
|
|
499
|
+
connectionId: this.connectionId,
|
|
500
|
+
generation: incarnation.generation,
|
|
501
|
+
});
|
|
502
|
+
if (isThenable(observerResult)) {
|
|
503
|
+
// An async observer cannot honor the synchronous pre-send
|
|
504
|
+
// contract: its abort decision is unavailable before the
|
|
505
|
+
// write. Sink the eventual rejection so it never escapes,
|
|
506
|
+
// and abort the dispatch pre-send (retryable, nothing on
|
|
507
|
+
// the wire).
|
|
508
|
+
sinkThenable(observerResult);
|
|
509
|
+
this.#settlePending(
|
|
510
|
+
id,
|
|
511
|
+
pending,
|
|
512
|
+
new SdkClientError(
|
|
513
|
+
"invalid_input",
|
|
514
|
+
"beforeDispatch must be synchronous; an async observer cannot gate the dispatch boundary.",
|
|
515
|
+
),
|
|
516
|
+
false,
|
|
517
|
+
);
|
|
518
|
+
return await deferred.promise;
|
|
519
|
+
}
|
|
520
|
+
} catch (error) {
|
|
521
|
+
// Pre-send abort: the wire never saw this request, so retirement
|
|
522
|
+
// must not retain a sent record or classify it as uncertain. The
|
|
523
|
+
// caller's own rejection comes back unchanged so it stays
|
|
524
|
+
// retryable and distinguishable from transport failure.
|
|
525
|
+
this.#settlePending(
|
|
526
|
+
id,
|
|
527
|
+
pending,
|
|
528
|
+
error instanceof Error ? error : new SdkClientError("invalid_input", String(error), error),
|
|
529
|
+
false,
|
|
530
|
+
);
|
|
531
|
+
return await deferred.promise;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
// `beforeDispatch` is arbitrary synchronous caller code: it may have closed
|
|
535
|
+
// the client, retired this incarnation, settled this pending entry, or run
|
|
536
|
+
// the deadline past its end. Nothing about the pre-call validation still
|
|
537
|
+
// holds, so revalidate all of it before the wire write and bookkeeping.
|
|
538
|
+
if (this.#closed || this.#pending.get(id) !== pending || !this.#isActive(incarnation)) {
|
|
539
|
+
if (this.#pending.get(id) === pending)
|
|
540
|
+
this.#settlePending(
|
|
541
|
+
id,
|
|
542
|
+
pending,
|
|
543
|
+
new SdkClientError("connection_closed", "SDK client closed during dispatch"),
|
|
544
|
+
);
|
|
545
|
+
return await deferred.promise;
|
|
546
|
+
}
|
|
547
|
+
if (this.#deadline !== undefined && Date.now() >= this.#deadline) {
|
|
548
|
+
this.#settlePending(id, pending, this.#deadlineError());
|
|
549
|
+
return await deferred.promise;
|
|
550
|
+
}
|
|
551
|
+
if (incarnation.socket.readyState !== WebSocket.OPEN) {
|
|
552
|
+
this.#settlePending(id, pending, new SdkClientError("unavailable", "SDK WebSocket is not connected"));
|
|
553
|
+
return await deferred.promise;
|
|
554
|
+
}
|
|
555
|
+
// Handoff bookkeeping is reentrancy-safe: `sent` flips BEFORE the wire
|
|
556
|
+
// write so a send that synchronously triggers a close event or response
|
|
557
|
+
// retires this request as already-sent (uncertain_after_send on close,
|
|
558
|
+
// never pre-send), and the sent record is retained up-front for the same
|
|
559
|
+
// reason. Only a synchronous send THROW rolls the pre-write state back:
|
|
560
|
+
// nothing reached the transport, so the request stays retryable and
|
|
561
|
+
// non-uncertain with no record retained.
|
|
562
|
+
pending.sent = true;
|
|
563
|
+
this.#rememberSentRecord({
|
|
564
|
+
id,
|
|
565
|
+
operation: sentOperation,
|
|
566
|
+
idempotencyKey: sentIdempotencyKey,
|
|
567
|
+
fingerprint: sentFingerprint,
|
|
568
|
+
});
|
|
400
569
|
try {
|
|
401
570
|
incarnation.socket.send(serializedRequest);
|
|
402
|
-
pending.sent = true;
|
|
403
|
-
this.#rememberSentRecord({
|
|
404
|
-
id,
|
|
405
|
-
operation: typeof serializedFrame.operation === "string" ? serializedFrame.operation : undefined,
|
|
406
|
-
idempotencyKey: options.idempotencyKey,
|
|
407
|
-
fingerprint:
|
|
408
|
-
typeof serializedFrame.operation === "string"
|
|
409
|
-
? lifecycleFingerprint(serializedFrame.operation, serializedFrame.input ?? {})
|
|
410
|
-
: inputFingerprint(serializedFrame.input ?? {}),
|
|
411
|
-
});
|
|
412
571
|
} catch (error) {
|
|
413
|
-
this.#
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
572
|
+
if (this.#pending.get(id) === pending) {
|
|
573
|
+
// Nothing was accepted by the transport (the write threw before
|
|
574
|
+
// handoff), so this stays a retryable pre-send failure — unless a
|
|
575
|
+
// reentrant event already settled the request during the throw,
|
|
576
|
+
// in which case that settlement stands and must not be displaced.
|
|
577
|
+
pending.sent = false;
|
|
578
|
+
this.#sentRecords.delete(id);
|
|
579
|
+
this.#settlePending(
|
|
580
|
+
id,
|
|
581
|
+
pending,
|
|
582
|
+
error instanceof SdkClientError
|
|
583
|
+
? error
|
|
584
|
+
: new SdkClientError("unavailable", "SDK WebSocket send failed", error),
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
return await deferred.promise;
|
|
588
|
+
}
|
|
589
|
+
// Reconciliation identity was retained above the send so reentrant
|
|
590
|
+
// close/response handling finds it; nothing further to record here.
|
|
591
|
+
try {
|
|
592
|
+
const observerResult = options.onDispatch?.({
|
|
593
|
+
frame: serializedFrame,
|
|
594
|
+
connectionId: this.connectionId,
|
|
595
|
+
generation: incarnation.generation,
|
|
596
|
+
});
|
|
597
|
+
if (isThenable(observerResult)) sinkThenable(observerResult);
|
|
598
|
+
} catch {
|
|
599
|
+
// The frame was already handed to the socket. An observer failure can
|
|
600
|
+
// neither un-send it nor displace settlement, so the request stays
|
|
601
|
+
// pending for its response, deadline, or transport-close retirement.
|
|
420
602
|
}
|
|
421
603
|
return await deferred.promise;
|
|
422
604
|
}
|
package/src/sdk/host/host.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { logger } from "@gajae-code/utils";
|
|
2
|
+
import { AUTOROUTING_INACTIVE_WARNING } from "../../config/autorouting-contract";
|
|
2
3
|
import { redactBrokerRuntimeCloseCapability } from "./control/runtime-gate";
|
|
3
4
|
import { type EventFrame, SessionEventStream } from "./events";
|
|
5
|
+
import { isAutoroutingInactive } from "./internal-autorouting-state";
|
|
4
6
|
import { type ProviderLease, ReverseLeaseError, ReverseLeaseRuntime } from "./reverse-leases";
|
|
5
7
|
import type { BrokerIndexWriter, HostEndpointAdapters, SdkFrame } from "./types";
|
|
6
8
|
|
|
@@ -77,6 +79,7 @@ export interface SessionSdkHostOptions extends HostEndpointAdapters {
|
|
|
77
79
|
connectionCapabilities?: (connectionId: string) => ReadonlySet<string> | undefined;
|
|
78
80
|
/** Readiness publication mode; defaults to the stock immediate contract. */
|
|
79
81
|
readiness?: SessionReadinessMode;
|
|
82
|
+
|
|
80
83
|
/**
|
|
81
84
|
* Authorization for a deferred activation. It is consulted on every attempt
|
|
82
85
|
* that would publish readiness and never on an idempotent replay, and a gate
|
|
@@ -247,9 +250,22 @@ export class SessionSdkHost {
|
|
|
247
250
|
return this.events.emit(frame);
|
|
248
251
|
}
|
|
249
252
|
|
|
253
|
+
emitAutoroutingInactiveNotice(): EventFrame {
|
|
254
|
+
return this.emitEvent({
|
|
255
|
+
kind: "notice",
|
|
256
|
+
payload: {
|
|
257
|
+
type: "notice",
|
|
258
|
+
level: "warning",
|
|
259
|
+
message: AUTOROUTING_INACTIVE_WARNING,
|
|
260
|
+
source: "autorouting",
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
250
265
|
async start(): Promise<"started" | "already"> {
|
|
251
266
|
if (this.#started) return "already";
|
|
252
267
|
this.events.restart();
|
|
268
|
+
if (isAutoroutingInactive(this)) this.emitAutoroutingInactiveNotice();
|
|
253
269
|
if (this.#options.readiness !== "deferred") this.#publishReadiness();
|
|
254
270
|
else
|
|
255
271
|
this.emitEvent({
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const inactiveAutoroutingState = new WeakMap<object, boolean>();
|
|
2
|
+
|
|
3
|
+
export function markAutoroutingInactive(target: object): void {
|
|
4
|
+
inactiveAutoroutingState.set(target, true);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function clearAutoroutingInactive(target: object): void {
|
|
8
|
+
inactiveAutoroutingState.delete(target);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function isAutoroutingInactive(target: unknown): boolean {
|
|
12
|
+
return typeof target === "object" && target !== null && inactiveAutoroutingState.get(target) === true;
|
|
13
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import type { AuthStorage } from "@gajae-code/ai/core";
|
|
3
|
+
import { ModelRegistry } from "../../config/model-registry";
|
|
4
|
+
import {
|
|
5
|
+
formatModelSelectorValue,
|
|
6
|
+
formatModelString,
|
|
7
|
+
type ResolveCliModelResult,
|
|
8
|
+
resolveCliModel,
|
|
9
|
+
} from "../../config/model-resolver";
|
|
10
|
+
import { Settings } from "../../config/settings";
|
|
11
|
+
import { discoverAuthStorage } from "../session";
|
|
12
|
+
|
|
13
|
+
const MAX_ECHOED_MODEL_LENGTH = 256;
|
|
14
|
+
|
|
15
|
+
export interface SdkHostModelResolveContext {
|
|
16
|
+
cwd?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Registry loader owned by the SDK host, not by machine-facing adapters. */
|
|
20
|
+
export type SdkHostModelRegistryLoader = (
|
|
21
|
+
context?: SdkHostModelResolveContext,
|
|
22
|
+
) => ModelRegistry | Promise<ModelRegistry>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Builds an offline model resolver for a single SDK host process.
|
|
26
|
+
*
|
|
27
|
+
* The registry is reused, but refreshed for every validation so additions and
|
|
28
|
+
* removals made while the broker is alive are observed without network I/O.
|
|
29
|
+
*/
|
|
30
|
+
export function createSdkHostModelRegistryLoader(
|
|
31
|
+
discoverStorage: () => Promise<AuthStorage>,
|
|
32
|
+
modelsPath?: string,
|
|
33
|
+
loadSettings?: (context?: SdkHostModelResolveContext) => Promise<Pick<Settings, "get" | "getGlobal">>,
|
|
34
|
+
): SdkHostModelRegistryLoader {
|
|
35
|
+
const cachedRegistries = new Map<string, Promise<ModelRegistry>>();
|
|
36
|
+
return async (context?: SdkHostModelResolveContext) => {
|
|
37
|
+
const scopeKey = context?.cwd ?? "";
|
|
38
|
+
let cachedRegistry = cachedRegistries.get(scopeKey);
|
|
39
|
+
if (cachedRegistry === undefined) {
|
|
40
|
+
const initializing = Promise.all([discoverStorage(), loadSettings?.(context)]).then(
|
|
41
|
+
([storage, registrySettings]) => new ModelRegistry(storage, modelsPath, registrySettings),
|
|
42
|
+
);
|
|
43
|
+
cachedRegistries.set(scopeKey, initializing);
|
|
44
|
+
cachedRegistry = initializing;
|
|
45
|
+
try {
|
|
46
|
+
await initializing;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (cachedRegistries.get(scopeKey) === initializing) cachedRegistries.delete(scopeKey);
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const registry = await cachedRegistry;
|
|
53
|
+
if (loadSettings) registry.setScopedSettings(await loadSettings(context));
|
|
54
|
+
await registry.refresh("offline");
|
|
55
|
+
return registry;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type SdkHostModelResolution =
|
|
60
|
+
| { ok: true; model: string | null }
|
|
61
|
+
| { ok: false; reason: "unknown_model"; model: string; error: string };
|
|
62
|
+
|
|
63
|
+
export type SdkHostModelResolver = (
|
|
64
|
+
raw: unknown,
|
|
65
|
+
context?: SdkHostModelResolveContext,
|
|
66
|
+
) => Promise<SdkHostModelResolution>;
|
|
67
|
+
|
|
68
|
+
/** Resolve the explicit model pin at the SDK host boundary. */
|
|
69
|
+
export async function resolveSdkHostModel(
|
|
70
|
+
raw: unknown,
|
|
71
|
+
loadRegistry: SdkHostModelRegistryLoader,
|
|
72
|
+
context?: SdkHostModelResolveContext,
|
|
73
|
+
): Promise<SdkHostModelResolution> {
|
|
74
|
+
if (raw === undefined || raw === null) return { ok: true, model: null };
|
|
75
|
+
const requested = typeof raw === "string" ? raw : "";
|
|
76
|
+
const echoed = requested.trim().slice(0, MAX_ECHOED_MODEL_LENGTH);
|
|
77
|
+
const registry = await loadRegistry(context);
|
|
78
|
+
const resolved: ResolveCliModelResult = resolveCliModel({ cliModel: requested, modelRegistry: registry });
|
|
79
|
+
if (!resolved.model)
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
reason: "unknown_model",
|
|
83
|
+
model: echoed,
|
|
84
|
+
error: resolved.error ?? "No models available. Check your installation or add models to models.json.",
|
|
85
|
+
};
|
|
86
|
+
return {
|
|
87
|
+
ok: true,
|
|
88
|
+
model: formatModelSelectorValue(formatModelString(resolved.model), resolved.thinkingLevel),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Default resolver used by the SDK broker host. */
|
|
93
|
+
export function createDefaultSdkHostModelResolver(agentDir: string): SdkHostModelResolver {
|
|
94
|
+
const loadRegistry = createSdkHostModelRegistryLoader(
|
|
95
|
+
() => discoverAuthStorage(agentDir),
|
|
96
|
+
path.join(agentDir, "models.yml"),
|
|
97
|
+
context => Settings.loadReadonly({ agentDir, cwd: context?.cwd }),
|
|
98
|
+
);
|
|
99
|
+
return (raw: unknown, context?: SdkHostModelResolveContext): Promise<SdkHostModelResolution> =>
|
|
100
|
+
resolveSdkHostModel(raw, loadRegistry, context);
|
|
101
|
+
}
|
|
@@ -281,8 +281,9 @@ export class ReverseLeaseRuntime {
|
|
|
281
281
|
throw new ReverseLeaseError("not_lease_owner");
|
|
282
282
|
this.#takeOutstanding(id);
|
|
283
283
|
if (error) {
|
|
284
|
-
const rejection = new Error(error.message);
|
|
284
|
+
const rejection = new Error(error.message) as Error & { code: string };
|
|
285
285
|
rejection.name = error.code;
|
|
286
|
+
rejection.code = error.code;
|
|
286
287
|
request.reject(rejection);
|
|
287
288
|
} else request.resolve(result);
|
|
288
289
|
}
|
|
@@ -13,9 +13,11 @@ import {
|
|
|
13
13
|
} from "../../session/terminal-abort";
|
|
14
14
|
import { Broker } from "../broker/broker";
|
|
15
15
|
import { createReconciliationStore } from "../bus/reconciliation-store";
|
|
16
|
+
import { CursorRegistry, QueryHandlers, RevisionStore } from "./query";
|
|
16
17
|
import {
|
|
17
18
|
createInvocationReconciliation,
|
|
18
19
|
createSdkSessionRuntimeExtension,
|
|
20
|
+
createSdkSurfaceFactory,
|
|
19
21
|
SessionSdkSessionRuntime,
|
|
20
22
|
type SessionSdkTransport,
|
|
21
23
|
} from "./session-runtime";
|
|
@@ -76,7 +78,15 @@ function admissionBarrier(target: number) {
|
|
|
76
78
|
};
|
|
77
79
|
}
|
|
78
80
|
|
|
79
|
-
function extensionContext(
|
|
81
|
+
function extensionContext(
|
|
82
|
+
sessionId: string,
|
|
83
|
+
cwd: string,
|
|
84
|
+
options: {
|
|
85
|
+
goalState?: unknown;
|
|
86
|
+
branch?: unknown[];
|
|
87
|
+
liveState?: { isStreaming: boolean; steeringQueueDepth: number; followupQueueDepth: number };
|
|
88
|
+
} = {},
|
|
89
|
+
): ExtensionContext {
|
|
80
90
|
return {
|
|
81
91
|
cwd,
|
|
82
92
|
workflowGate: undefined,
|
|
@@ -85,10 +95,133 @@ function extensionContext(sessionId: string, cwd: string): ExtensionContext {
|
|
|
85
95
|
getSessionId: () => sessionId,
|
|
86
96
|
getSessionFile: () => path.join(cwd, `${sessionId}.json`),
|
|
87
97
|
getSessionName: () => undefined,
|
|
98
|
+
getBranch: () => options.branch ?? [],
|
|
88
99
|
},
|
|
100
|
+
getGoalState: () => options.goalState,
|
|
89
101
|
} as unknown as ExtensionContext;
|
|
90
102
|
}
|
|
91
103
|
|
|
104
|
+
function goalModeEntry(sessionId: string, tokensUsed: number): Record<string, unknown> {
|
|
105
|
+
return {
|
|
106
|
+
type: "mode_change",
|
|
107
|
+
id: `goal-${sessionId}`,
|
|
108
|
+
parentId: null,
|
|
109
|
+
timestamp: new Date(1_000).toISOString(),
|
|
110
|
+
mode: "goal",
|
|
111
|
+
data: {
|
|
112
|
+
goal: {
|
|
113
|
+
id: `goal-${sessionId}`,
|
|
114
|
+
objective: "Preserve the active SDK goal",
|
|
115
|
+
status: "active",
|
|
116
|
+
tokensUsed,
|
|
117
|
+
timeUsedSeconds: 2,
|
|
118
|
+
createdAt: 1_000,
|
|
119
|
+
updatedAt: 2_000,
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function queryGoalState(
|
|
126
|
+
ctx: ExtensionContext,
|
|
127
|
+
sessionId: string,
|
|
128
|
+
liveState?: { isStreaming: boolean; steeringQueueDepth: number; followupQueueDepth: number },
|
|
129
|
+
): Promise<unknown> {
|
|
130
|
+
const surface = createSdkSurfaceFactory({
|
|
131
|
+
ctx,
|
|
132
|
+
id: sessionId,
|
|
133
|
+
api: {} as ExtensionAPI,
|
|
134
|
+
getLiveState: () => liveState ?? { isStreaming: false, steeringQueueDepth: 0, followupQueueDepth: 0 },
|
|
135
|
+
}).query;
|
|
136
|
+
const store = new RevisionStore(sessionId);
|
|
137
|
+
const cursors = new CursorRegistry("goal-test-token", store);
|
|
138
|
+
const response = await new QueryHandlers(surface, sessionId, store, cursors).dispatch({
|
|
139
|
+
query: "goal.list/get",
|
|
140
|
+
connectionId: "goal-test-connection",
|
|
141
|
+
});
|
|
142
|
+
if (!response.ok) throw new Error(`goal query failed: ${JSON.stringify(response)}`);
|
|
143
|
+
return response.page?.items[0];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
describe("SDK goal snapshot lifecycle", () => {
|
|
147
|
+
test("recovers an active nonzero-activity goal from the durable session projection", async () => {
|
|
148
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-goal-recovery-"));
|
|
149
|
+
try {
|
|
150
|
+
const sessionId = "runtime-recreated";
|
|
151
|
+
const branch = [goalModeEntry(sessionId, 17)];
|
|
152
|
+
const liveGoal = {
|
|
153
|
+
enabled: true,
|
|
154
|
+
mode: "active",
|
|
155
|
+
goal: { ...(branch[0] as { data: { goal: Record<string, unknown> } }).data.goal },
|
|
156
|
+
};
|
|
157
|
+
const live = await queryGoalState(
|
|
158
|
+
extensionContext(sessionId, cwd, { goalState: liveGoal, branch }),
|
|
159
|
+
sessionId,
|
|
160
|
+
{ isStreaming: true, steeringQueueDepth: 1, followupQueueDepth: 0 },
|
|
161
|
+
);
|
|
162
|
+
const recreated = await queryGoalState(extensionContext(sessionId, cwd, { branch }), sessionId);
|
|
163
|
+
|
|
164
|
+
expect(live).toMatchObject({ enabled: true, goal: { status: "active", tokensUsed: 17 } });
|
|
165
|
+
expect(recreated).toEqual(live);
|
|
166
|
+
} finally {
|
|
167
|
+
await rm(cwd, { recursive: true, force: true });
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("keeps fresh session/worktree projections isolated while an active turn has activity", async () => {
|
|
172
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-goal-isolation-"));
|
|
173
|
+
try {
|
|
174
|
+
const first = await queryGoalState(
|
|
175
|
+
extensionContext("fresh-session-a", path.join(root, "a"), {
|
|
176
|
+
branch: [goalModeEntry("fresh-session-a", 11)],
|
|
177
|
+
}),
|
|
178
|
+
"fresh-session-a",
|
|
179
|
+
);
|
|
180
|
+
const second = await queryGoalState(
|
|
181
|
+
extensionContext("fresh-session-b", path.join(root, "b"), {
|
|
182
|
+
branch: [goalModeEntry("fresh-session-b", 23)],
|
|
183
|
+
}),
|
|
184
|
+
"fresh-session-b",
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
expect(first).toMatchObject({ goal: { id: "goal-fresh-session-a", tokensUsed: 11 } });
|
|
188
|
+
expect(second).toMatchObject({ goal: { id: "goal-fresh-session-b", tokensUsed: 23 } });
|
|
189
|
+
expect(first).not.toEqual(second);
|
|
190
|
+
} finally {
|
|
191
|
+
await rm(root, { recursive: true, force: true });
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("returns an explicit no-active-goal diagnostic for the zero-activity control", async () => {
|
|
196
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-goal-empty-"));
|
|
197
|
+
try {
|
|
198
|
+
const result = await queryGoalState(extensionContext("zero-activity", cwd), "zero-activity");
|
|
199
|
+
expect(result).toMatchObject({ enabled: false, goal: null, reason: "no_active_goal" });
|
|
200
|
+
} finally {
|
|
201
|
+
await rm(cwd, { recursive: true, force: true });
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("returns a recoverable diagnostic when durable active-goal state is malformed", async () => {
|
|
206
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-goal-corrupt-"));
|
|
207
|
+
try {
|
|
208
|
+
const result = await queryGoalState(
|
|
209
|
+
extensionContext("corrupt-goal", cwd, {
|
|
210
|
+
branch: [{ ...goalModeEntry("corrupt-goal", 19), data: { goal: { status: "active" } } }],
|
|
211
|
+
}),
|
|
212
|
+
"corrupt-goal",
|
|
213
|
+
);
|
|
214
|
+
expect(result).toMatchObject({
|
|
215
|
+
reason: "goal_state_unavailable",
|
|
216
|
+
recoverable: true,
|
|
217
|
+
goal: null,
|
|
218
|
+
});
|
|
219
|
+
} finally {
|
|
220
|
+
await rm(cwd, { recursive: true, force: true });
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
92
225
|
test("preserves an agent failure code in host prompt reconciliation", async () => {
|
|
93
226
|
const reconciliation = createInvocationReconciliation();
|
|
94
227
|
const correlation = { commandId: "failed-command", turnId: "failed-turn" };
|
|
@@ -2534,6 +2667,96 @@ describe("post-acceptance invocation terminalization", () => {
|
|
|
2534
2667
|
await rm(cwd, { recursive: true, force: true });
|
|
2535
2668
|
}
|
|
2536
2669
|
});
|
|
2670
|
+
test("immediate prompt after abort ack is not terminalized by the aborted turn's delayed agent_end", async () => {
|
|
2671
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-abort-immediate-prompt-"));
|
|
2672
|
+
try {
|
|
2673
|
+
const firstInflight = Promise.withResolvers<void>();
|
|
2674
|
+
const successorStarted = Promise.withResolvers<void>();
|
|
2675
|
+
let prompts = 0;
|
|
2676
|
+
const harness = await invocationHarness("abort-immediate-prompt", cwd, {
|
|
2677
|
+
sendUserMessage: async (_content, options) => {
|
|
2678
|
+
prompts += 1;
|
|
2679
|
+
await options?.onPreflightAcceptCommit?.();
|
|
2680
|
+
if (prompts === 1) {
|
|
2681
|
+
await firstInflight.promise;
|
|
2682
|
+
return;
|
|
2683
|
+
}
|
|
2684
|
+
successorStarted.resolve();
|
|
2685
|
+
await Promise.withResolvers<void>().promise;
|
|
2686
|
+
},
|
|
2687
|
+
});
|
|
2688
|
+
const first = await harness.control("turn.prompt", { text: "first" });
|
|
2689
|
+
expect(first.ok).toBe(true);
|
|
2690
|
+
const firstIds = { commandId: first.result?.commandId, turnId: first.result?.turnId };
|
|
2691
|
+
await harness.emit("agent_start");
|
|
2692
|
+
expect(await harness.control("turn.abort", {})).toMatchObject({ ok: true });
|
|
2693
|
+
firstInflight.reject(Object.assign(new Error("turn aborted"), { code: "aborted" }));
|
|
2694
|
+
const second = await harness.control("turn.prompt", { text: "successor" });
|
|
2695
|
+
expect(second.ok).toBe(true);
|
|
2696
|
+
const secondIds = { commandId: second.result?.commandId, turnId: second.result?.turnId };
|
|
2697
|
+
expect(secondIds.commandId).not.toBe(firstIds.commandId);
|
|
2698
|
+
expect(secondIds.turnId).not.toBe(firstIds.turnId);
|
|
2699
|
+
await harness.emit("agent_start");
|
|
2700
|
+
await successorStarted.promise;
|
|
2701
|
+
await harness.emit("agent_end");
|
|
2702
|
+
expect(await harness.query("turn.prompt_status", secondIds)).toMatchObject({
|
|
2703
|
+
result: { status: expect.stringMatching(/accepted|in_flight/) },
|
|
2704
|
+
});
|
|
2705
|
+
expect(await settledStatus(harness, "turn.prompt_status", firstIds)).toMatchObject({
|
|
2706
|
+
status: "failed",
|
|
2707
|
+
error: { code: "aborted" },
|
|
2708
|
+
});
|
|
2709
|
+
await harness.emit("agent_end");
|
|
2710
|
+
expect(await settledStatus(harness, "turn.prompt_status", secondIds)).toMatchObject({
|
|
2711
|
+
status: "terminal_ok",
|
|
2712
|
+
});
|
|
2713
|
+
expect(prompts).toBe(2);
|
|
2714
|
+
await harness.stop();
|
|
2715
|
+
} finally {
|
|
2716
|
+
await Bun.sleep(50);
|
|
2717
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2718
|
+
}
|
|
2719
|
+
});
|
|
2720
|
+
|
|
2721
|
+
test("abort_and_prompt starts exactly one successor and is not duplicated by delayed abort teardown", async () => {
|
|
2722
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-abort-and-prompt-once-"));
|
|
2723
|
+
try {
|
|
2724
|
+
const firstInflight = Promise.withResolvers<void>();
|
|
2725
|
+
const abortReleased = Promise.withResolvers<void>();
|
|
2726
|
+
let prompts = 0;
|
|
2727
|
+
const harness = await invocationHarness("abort-and-prompt-once", cwd, {
|
|
2728
|
+
sendUserMessage: async (_content, options) => {
|
|
2729
|
+
prompts += 1;
|
|
2730
|
+
await options?.onPreflightAcceptCommit?.();
|
|
2731
|
+
if (prompts === 1) {
|
|
2732
|
+
await firstInflight.promise;
|
|
2733
|
+
return;
|
|
2734
|
+
}
|
|
2735
|
+
},
|
|
2736
|
+
abort: () => abortReleased.promise,
|
|
2737
|
+
});
|
|
2738
|
+
const first = await harness.control("turn.prompt", { text: "first" });
|
|
2739
|
+
expect(first.ok).toBe(true);
|
|
2740
|
+
await harness.emit("agent_start");
|
|
2741
|
+
const replacement = harness.control("turn.abort_and_prompt", { text: "replacement" });
|
|
2742
|
+
firstInflight.reject(Object.assign(new Error("turn aborted"), { code: "aborted" }));
|
|
2743
|
+
await harness.emit("agent_end");
|
|
2744
|
+
abortReleased.resolve();
|
|
2745
|
+
const accepted = await replacement;
|
|
2746
|
+
expect(accepted.ok).toBe(true);
|
|
2747
|
+
const successorIds = { commandId: accepted.result?.commandId, turnId: accepted.result?.turnId };
|
|
2748
|
+
await harness.emit("agent_start");
|
|
2749
|
+
await harness.emit("agent_end");
|
|
2750
|
+
expect(await settledStatus(harness, "turn.prompt_status", successorIds)).toMatchObject({
|
|
2751
|
+
status: "terminal_ok",
|
|
2752
|
+
});
|
|
2753
|
+
expect(prompts).toBe(2);
|
|
2754
|
+
await harness.stop();
|
|
2755
|
+
} finally {
|
|
2756
|
+
await Bun.sleep(50);
|
|
2757
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2758
|
+
}
|
|
2759
|
+
});
|
|
2537
2760
|
|
|
2538
2761
|
test("a failed skill invocation still reports a terminal failed status", async () => {
|
|
2539
2762
|
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-terminalize-skill-"));
|