@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
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
<agent id="{{id}}" agent="{{agent}}">
|
|
6
6
|
<status>{{status}}</status>
|
|
7
7
|
{{#if meta}}<meta lines="{{meta.lineCount}}" size="{{meta.charSize}}" />{{/if}}
|
|
8
|
+
{{#if routing}}<routing tier="{{routing.tier}}" model="{{routing.effectiveModel}}" note="{{routing.note}}" />{{/if}}
|
|
8
9
|
<synopsis{{#if outputUri}} ref="{{outputUri}}"{{/if}}>
|
|
9
10
|
{{synopsis}}
|
|
10
11
|
</synopsis>
|
|
@@ -82,3 +82,9 @@ Agent spawning is disabled for this context.
|
|
|
82
82
|
{{/list}}
|
|
83
83
|
{{/if}}
|
|
84
84
|
</agents>
|
|
85
|
+
|
|
86
|
+
{{#if autoroutingActive}}
|
|
87
|
+
<autorouting-guidance>
|
|
88
|
+
Choose a tier by agent role/type, per-call complexity, and cost intent: fast for mechanical/lookup/high-volume work where cheap tokens are the point; balanced (default) for ordinary implementation/review lanes; strong for deep design, hard debugging, or high-stakes review where the cost is justified. Provider availability/auth is enforced by deterministic code and is never an input to tier choice. Omitting tier is fine and routes as balanced.
|
|
89
|
+
</autorouting-guidance>
|
|
90
|
+
{{/if}}
|
package/src/rlm/artifacts.ts
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* RLM session artifact layout under
|
|
2
|
+
* RLM session artifact layout under
|
|
3
|
+
* <cwd>/.gjc/_session-{gjcSessionId}/autoresearch/runs/<rlmSessionId>/.
|
|
3
4
|
*
|
|
4
5
|
* The GJC session id (process boundary) scopes the directory; the RLM session id
|
|
5
6
|
* names the individual research run within it. The two ids are kept distinct.
|
|
7
|
+
* The whole subtree lives under the session autoresearch root.
|
|
6
8
|
*/
|
|
7
9
|
import * as fs from "node:fs/promises";
|
|
8
10
|
import * as path from "node:path";
|
|
9
11
|
import { readNotebookDocument } from "../edit/notebook";
|
|
10
|
-
import {
|
|
12
|
+
import { autoresearchRlmArtifactRoot } from "../gjc-runtime/session-layout";
|
|
11
13
|
import { resolveGjcSessionForWrite } from "../gjc-runtime/session-resolution";
|
|
12
14
|
import type { RlmArtifactPaths } from "./types";
|
|
13
15
|
|
|
14
|
-
export const RLM_DIR_SEGMENT = "
|
|
16
|
+
export const RLM_DIR_SEGMENT = "runs";
|
|
15
17
|
|
|
16
18
|
const SESSION_ID_RE = /^[A-Za-z0-9_-]+$/;
|
|
17
19
|
|
|
@@ -26,13 +28,13 @@ export function generateRlmSessionId(now: Date = new Date()): string {
|
|
|
26
28
|
return `${stamp}-${suffix}`;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
export function resolveRlmArtifactPaths(cwd: string, sessionId: string): RlmArtifactPaths {
|
|
31
|
+
export function resolveRlmArtifactPaths(cwd: string, sessionId: string, gjcSessionId?: string): RlmArtifactPaths {
|
|
30
32
|
if (!isValidRlmSessionId(sessionId)) {
|
|
31
33
|
throw new Error(`Invalid RLM session id: ${JSON.stringify(sessionId)}`);
|
|
32
34
|
}
|
|
33
|
-
const dir =
|
|
35
|
+
const dir = autoresearchRlmArtifactRoot(
|
|
34
36
|
cwd,
|
|
35
|
-
resolveGjcSessionForWrite(cwd, { envSessionId: process.env.GJC_SESSION_ID }).gjcSessionId,
|
|
37
|
+
gjcSessionId ?? resolveGjcSessionForWrite(cwd, { envSessionId: process.env.GJC_SESSION_ID }).gjcSessionId,
|
|
36
38
|
sessionId,
|
|
37
39
|
);
|
|
38
40
|
return {
|
|
@@ -48,8 +50,8 @@ export async function ensureRlmSessionDir(paths: RlmArtifactPaths): Promise<void
|
|
|
48
50
|
await fs.mkdir(paths.dir, { recursive: true });
|
|
49
51
|
}
|
|
50
52
|
|
|
51
|
-
export async function rlmSessionExists(cwd: string, sessionId: string): Promise<boolean> {
|
|
52
|
-
const paths = resolveRlmArtifactPaths(cwd, sessionId);
|
|
53
|
+
export async function rlmSessionExists(cwd: string, sessionId: string, gjcSessionId?: string): Promise<boolean> {
|
|
54
|
+
const paths = resolveRlmArtifactPaths(cwd, sessionId, gjcSessionId);
|
|
53
55
|
try {
|
|
54
56
|
const stat = await fs.stat(paths.dir);
|
|
55
57
|
return stat.isDirectory();
|
|
@@ -58,8 +60,8 @@ export async function rlmSessionExists(cwd: string, sessionId: string): Promise<
|
|
|
58
60
|
}
|
|
59
61
|
}
|
|
60
62
|
|
|
61
|
-
export async function readRlmNotebookIfPresent(cwd: string, sessionId: string) {
|
|
62
|
-
const paths = resolveRlmArtifactPaths(cwd, sessionId);
|
|
63
|
+
export async function readRlmNotebookIfPresent(cwd: string, sessionId: string, gjcSessionId?: string) {
|
|
64
|
+
const paths = resolveRlmArtifactPaths(cwd, sessionId, gjcSessionId);
|
|
63
65
|
try {
|
|
64
66
|
return await readNotebookDocument(paths.notebookPath, paths.notebookPath);
|
|
65
67
|
} catch (error) {
|
|
@@ -20,7 +20,8 @@ export type OptionalRuntimeServicesOverrides = Partial<OptionalRuntimeServices>;
|
|
|
20
20
|
|
|
21
21
|
/** Context needed by services whose identity is scoped to the session cwd. */
|
|
22
22
|
export interface OptionalRuntimeServicesContext {
|
|
23
|
-
cwd
|
|
23
|
+
/** Session cwd. Pass a getter when the session can rescope (`move_session`). */
|
|
24
|
+
cwd?: string | (() => string);
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
/**
|
|
@@ -32,7 +33,7 @@ export function createOptionalRuntimeServices(
|
|
|
32
33
|
overrides: OptionalRuntimeServicesOverrides = {},
|
|
33
34
|
context: OptionalRuntimeServicesContext = {},
|
|
34
35
|
): OptionalRuntimeServices {
|
|
35
|
-
const cwd = context.cwd ?? process.cwd();
|
|
36
|
+
const cwd = context.cwd ?? (() => process.cwd());
|
|
36
37
|
return {
|
|
37
38
|
memoryBackend: overrides.memoryBackend ?? createMemoryBackendService(settings),
|
|
38
39
|
workspaceTree: overrides.workspaceTree ?? createWorkspaceTreeService(settings, cwd),
|
|
@@ -15,8 +15,16 @@ export interface WorkspaceTreeRuntime {
|
|
|
15
15
|
* Build the workspace-tree service without importing the native scanner until
|
|
16
16
|
* the service is activated. The scan itself remains the single authority for
|
|
17
17
|
* both eager startup and the lazy first-turn barrier.
|
|
18
|
+
*
|
|
19
|
+
* `cwd` is resolved per scan rather than captured once: a session that rescopes
|
|
20
|
+
* (`move_session`, `/move`) must have its refreshes re-root at the new cwd,
|
|
21
|
+
* otherwise every later tree describes the abandoned launcher root.
|
|
18
22
|
*/
|
|
19
|
-
export function createWorkspaceTreeService(
|
|
23
|
+
export function createWorkspaceTreeService(
|
|
24
|
+
settings: Settings,
|
|
25
|
+
cwd: string | (() => string),
|
|
26
|
+
): LazyService<WorkspaceTreeRuntime> {
|
|
27
|
+
const resolveCwd = typeof cwd === "function" ? cwd : () => cwd;
|
|
20
28
|
return createLazyService({
|
|
21
29
|
id: "workspaceTree",
|
|
22
30
|
enabled: () => settings.get("workspaceTree.mode") === "eager" || settings.get("workspaceTree.mode") === "lazy",
|
|
@@ -24,7 +32,7 @@ export function createWorkspaceTreeService(settings: Settings, cwd: string): Laz
|
|
|
24
32
|
const scan = async (): Promise<WorkspaceTree> => {
|
|
25
33
|
if (signal.aborted) throw new Error("Workspace-tree scan was aborted before it started.");
|
|
26
34
|
const { buildWorkspaceTree } = await import("../workspace-tree");
|
|
27
|
-
const tree = await buildWorkspaceTree(
|
|
35
|
+
const tree = await buildWorkspaceTree(resolveCwd(), { timeoutMs: WORKSPACE_TREE_SCAN_TIMEOUT_MS });
|
|
28
36
|
if (signal.aborted) throw new Error("Workspace-tree scan was aborted before it completed.");
|
|
29
37
|
return tree;
|
|
30
38
|
};
|
|
@@ -34,7 +42,7 @@ export function createWorkspaceTreeService(settings: Settings, cwd: string): Laz
|
|
|
34
42
|
snapshot,
|
|
35
43
|
refresh: async () => {
|
|
36
44
|
const { buildWorkspaceTree } = await import("../workspace-tree");
|
|
37
|
-
return buildWorkspaceTree(
|
|
45
|
+
return buildWorkspaceTree(resolveCwd(), { timeoutMs: WORKSPACE_TREE_SCAN_TIMEOUT_MS });
|
|
38
46
|
},
|
|
39
47
|
},
|
|
40
48
|
};
|
|
@@ -34,6 +34,12 @@ export interface LoadMCPConfigsOptions {
|
|
|
34
34
|
nativeOnly?: boolean;
|
|
35
35
|
/** Load only this explicit MCP config file. */
|
|
36
36
|
configPath?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Agent directory that holds the user scope (`<agentDir>/mcp.json`).
|
|
39
|
+
* Default: `getAgentDir()`. Sessions created with their own `agentDir` pass it
|
|
40
|
+
* so discovery and `gjc mcp add --scope user` read and write the same file.
|
|
41
|
+
*/
|
|
42
|
+
agentDir?: string;
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
/** Result of loading MCP configs */
|
|
@@ -140,6 +146,7 @@ export async function loadAllMCPConfigs(cwd: string, options?: LoadMCPConfigsOpt
|
|
|
140
146
|
// implicit runtime authorities.
|
|
141
147
|
const result = await loadCapability<MCPServer>(mcpCapability.id, {
|
|
142
148
|
cwd,
|
|
149
|
+
agentDir: options?.agentDir,
|
|
143
150
|
providers: options?.nativeOnly === true ? ["native"] : undefined,
|
|
144
151
|
});
|
|
145
152
|
// Filter out project-level configs if disabled
|
|
@@ -150,7 +157,7 @@ export async function loadAllMCPConfigs(cwd: string, options?: LoadMCPConfigsOpt
|
|
|
150
157
|
// must not abort discovery of valid servers in the other scope (the
|
|
151
158
|
// capability loader itself is already per-file tolerant).
|
|
152
159
|
const [userDisabled, projectDisabled] = await Promise.all([
|
|
153
|
-
readDisabledServers(getMCPConfigPath("user", cwd)).catch(() => []),
|
|
160
|
+
readDisabledServers(getMCPConfigPath("user", cwd, options?.agentDir)).catch(() => []),
|
|
154
161
|
readDisabledServers(getMCPConfigPath("project", cwd)).catch(() => []),
|
|
155
162
|
]);
|
|
156
163
|
disabledServers = new Set([...userDisabled, ...projectDisabled]);
|
|
@@ -134,6 +134,21 @@ export function resolveExactConfigStartupTimeoutMs(configs: MCPServerConfig[]):
|
|
|
134
134
|
return Math.max(...effectiveTimeouts) + STARTUP_TIMEOUT_GRACE_MS;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Whether `config` declared a connection window that is still open `elapsedMs`
|
|
139
|
+
* into startup.
|
|
140
|
+
*
|
|
141
|
+
* The startup wait bounds how long session start blocks; a declared `timeout`
|
|
142
|
+
* bounds how long the server itself may take to come up (`connectToServer`
|
|
143
|
+
* enforces it). Those are different budgets: the wait elapsing says nothing
|
|
144
|
+
* about whether the operator's declared window has been spent.
|
|
145
|
+
*/
|
|
146
|
+
export function withinDeclaredConnectionWindow(config: MCPServerConfig, elapsedMs: number): boolean {
|
|
147
|
+
const timeout = config.timeout;
|
|
148
|
+
if (typeof timeout !== "number" || !Number.isFinite(timeout) || timeout <= 0) return false;
|
|
149
|
+
return elapsedMs < timeout;
|
|
150
|
+
}
|
|
151
|
+
|
|
137
152
|
function trackPromise<T>(promise: Promise<T>): TrackedPromise<T> {
|
|
138
153
|
const tracked: TrackedPromise<T> = { promise, status: "pending" };
|
|
139
154
|
promise.then(
|
|
@@ -1155,6 +1170,7 @@ export class MCPManager {
|
|
|
1155
1170
|
});
|
|
1156
1171
|
}
|
|
1157
1172
|
}
|
|
1173
|
+
const startupStartedAt = Date.now();
|
|
1158
1174
|
const startupOutcome = await Promise.race([
|
|
1159
1175
|
Promise.allSettled(connectionTasks.map(task => task.tracked.promise)).then(() => undefined),
|
|
1160
1176
|
delay(startupTimeoutMs).then(() => undefined),
|
|
@@ -1190,7 +1206,24 @@ export class MCPManager {
|
|
|
1190
1206
|
|
|
1191
1207
|
const pendingWithoutCache = pendingTasks.filter(task => !cachedTools.has(task.name));
|
|
1192
1208
|
if (pendingWithoutCache.length > 0) {
|
|
1209
|
+
// The startup wait elapsing means "stop blocking session start", not
|
|
1210
|
+
// "this server failed". A server whose operator declared a `timeout`
|
|
1211
|
+
// (`gjc mcp add --timeout`) asked to wait that long for it, so while it
|
|
1212
|
+
// is still inside that window it keeps connecting in the background
|
|
1213
|
+
// under `connectToServer`'s own timeout, and the background tool load
|
|
1214
|
+
// adopts it. Only a server that declared no window, or already spent it,
|
|
1215
|
+
// is torn down and reported. Exact-config (`toolsOnly`) startup still
|
|
1216
|
+
// fails fast: it builds a catalog once, so a missing server is an error.
|
|
1217
|
+
const startupElapsedMs = Date.now() - startupStartedAt;
|
|
1193
1218
|
for (const task of pendingWithoutCache) {
|
|
1219
|
+
if (!this.#toolsOnly && withinDeclaredConnectionWindow(task.config, startupElapsedMs)) {
|
|
1220
|
+
logger.warn("MCP server still connecting after the startup wait", {
|
|
1221
|
+
path: `mcp:${task.name}`,
|
|
1222
|
+
startupWaitMs: startupTimeoutMs,
|
|
1223
|
+
declaredTimeoutMs: task.config.timeout,
|
|
1224
|
+
});
|
|
1225
|
+
continue;
|
|
1226
|
+
}
|
|
1194
1227
|
const message = `MCP server connection timed out during startup: ${task.name}`;
|
|
1195
1228
|
errors.set(task.name, this.#serverError(message));
|
|
1196
1229
|
reportedErrors.add(task.name);
|
package/src/sdk/acp/adapter.ts
CHANGED
|
@@ -73,6 +73,8 @@ const ACP_MCP_PRESERVED_LAUNCH_CODES = new Set([
|
|
|
73
73
|
"model_profile_registry_error",
|
|
74
74
|
"startup_admission_refused",
|
|
75
75
|
"startup_admission_timeout",
|
|
76
|
+
"ready_then_exited",
|
|
77
|
+
"endpoint_unreadable",
|
|
76
78
|
]);
|
|
77
79
|
|
|
78
80
|
/**
|
|
@@ -127,6 +129,21 @@ function isLifecycleOperation(operation: string): boolean {
|
|
|
127
129
|
* Pure ACP-to-SDK adapter. It deliberately owns neither an AgentSession nor an
|
|
128
130
|
* ACP bridge: all session work is performed through authenticated v3 frames.
|
|
129
131
|
*/
|
|
132
|
+
/**
|
|
133
|
+
* Provider leases are renewed only through the maintenance capability (#4689).
|
|
134
|
+
* An attachment without it must be rejected at the admission boundary with an
|
|
135
|
+
* explicit migration error: falling back to `send()` would restore the 5s
|
|
136
|
+
* heartbeat-forced locked index rescan, and accepting it silently would leave
|
|
137
|
+
* live leases quietly un-renewed until they expire.
|
|
138
|
+
*/
|
|
139
|
+
function assertMaintenanceCapability(attachment: SessionAttachment): void {
|
|
140
|
+
if (typeof attachment.sendMaintenance === "function") return;
|
|
141
|
+
throw new AcpSdkAdapterError(
|
|
142
|
+
"operation_prohibited",
|
|
143
|
+
"SDK session attachment does not implement sendMaintenance(leaseId); provider leases cannot be renewed. Update the attachment implementation to the current SessionAttachment capability.",
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
130
147
|
export class AcpSdkAdapter {
|
|
131
148
|
readonly #client?: SdkClient;
|
|
132
149
|
readonly #router?: SessionRouter;
|
|
@@ -184,6 +201,11 @@ export class AcpSdkAdapter {
|
|
|
184
201
|
acceptAttachment(attachment: SessionAttachment): void {
|
|
185
202
|
if (!this.#router || attachment.sessionId !== this.#sessionId)
|
|
186
203
|
throw new AcpSdkAdapterError("invalid_input", "ACP attachment does not match this session adapter.");
|
|
204
|
+
// Reject an attachment that cannot renew provider leases at the handoff
|
|
205
|
+
// boundary (#4730 review), not just at start(): acceptAttachment is the
|
|
206
|
+
// single admission point for the replacement/ready paths too, so a
|
|
207
|
+
// capability-less replacement can never silently take over live leases.
|
|
208
|
+
assertMaintenanceCapability(attachment);
|
|
187
209
|
this.#abortActiveReverseRequests();
|
|
188
210
|
this.#attachment = attachment;
|
|
189
211
|
this.#connectionId = undefined;
|
|
@@ -202,6 +224,11 @@ export class AcpSdkAdapter {
|
|
|
202
224
|
}
|
|
203
225
|
|
|
204
226
|
async attachmentReady(attachment: SessionAttachment): Promise<void> {
|
|
227
|
+
// Guard BEFORE the branch (#4730 review): the same-object path below never
|
|
228
|
+
// reaches acceptAttachment, so guarding only inside that one arm let a
|
|
229
|
+
// current attachment without the maintenance capability activate providers
|
|
230
|
+
// and acquire leases it can never renew.
|
|
231
|
+
assertMaintenanceCapability(attachment);
|
|
205
232
|
if (this.#attachment !== attachment) this.acceptAttachment(attachment);
|
|
206
233
|
else {
|
|
207
234
|
this.#abortActiveReverseRequests();
|
|
@@ -251,6 +278,7 @@ export class AcpSdkAdapter {
|
|
|
251
278
|
}
|
|
252
279
|
if (this.#router) {
|
|
253
280
|
if (!this.#attachment?.isCurrent()) return;
|
|
281
|
+
if (this.#attachment) assertMaintenanceCapability(this.#attachment);
|
|
254
282
|
await this.#activateProviders();
|
|
255
283
|
} else {
|
|
256
284
|
await this.#activateProviders();
|
|
@@ -576,13 +604,32 @@ export class AcpSdkAdapter {
|
|
|
576
604
|
if (!attachment?.isCurrent()) throw new SessionRouterError("pre_send", "SDK session attachment is stale.");
|
|
577
605
|
await Promise.resolve(attachment.send(frame));
|
|
578
606
|
}
|
|
607
|
+
/** Lease heartbeats are idempotent maintenance: they skip the authority reconcile (#4689). */
|
|
608
|
+
async #sendLeaseHeartbeat(leaseId: string): Promise<void> {
|
|
609
|
+
if (!this.#router)
|
|
610
|
+
throw new AcpSdkAdapterError(
|
|
611
|
+
"operation_prohibited",
|
|
612
|
+
"Live session sends require the current Router attachment.",
|
|
613
|
+
);
|
|
614
|
+
const attachment = this.#attachment;
|
|
615
|
+
if (!attachment?.isCurrent()) throw new SessionRouterError("pre_send", "SDK session attachment is stale.");
|
|
616
|
+
// Fail closed when the capability is absent (#4730 review). Falling back to
|
|
617
|
+
// send() would put the 5s heartbeat back on the locked authority reconcile,
|
|
618
|
+
// which is the exact idle cost this fix removes.
|
|
619
|
+
if (typeof attachment.sendMaintenance !== "function")
|
|
620
|
+
throw new SessionRouterError(
|
|
621
|
+
"pre_send",
|
|
622
|
+
"SDK session attachment does not support provider-lease maintenance heartbeats.",
|
|
623
|
+
);
|
|
624
|
+
await Promise.resolve(attachment.sendMaintenance(leaseId));
|
|
625
|
+
}
|
|
579
626
|
|
|
580
627
|
async #heartbeatLeases(): Promise<void> {
|
|
581
628
|
if (this.#closed) return;
|
|
582
629
|
try {
|
|
583
630
|
if (!this.#router) return;
|
|
584
631
|
if (!this.#attachment?.isCurrent()) return;
|
|
585
|
-
for (const leaseId of this.#leases.values()) await this.#
|
|
632
|
+
for (const leaseId of this.#leases.values()) await this.#sendLeaseHeartbeat(leaseId);
|
|
586
633
|
} catch (error) {
|
|
587
634
|
if (error instanceof SessionRouterError && error.phase === "pre_send") return;
|
|
588
635
|
this.#reportReconnectFailure(error);
|
package/src/sdk/broker/broker.ts
CHANGED
|
@@ -6,6 +6,8 @@ import path from "node:path";
|
|
|
6
6
|
import type { NativeDirectoryTreeSnapshot } from "@gajae-code/natives";
|
|
7
7
|
import { logger } from "@gajae-code/utils";
|
|
8
8
|
import type { ModelProfileErrorDetails } from "../../config/model-profile-contract";
|
|
9
|
+
import { planLaunchWorktree } from "../../gjc-runtime/launch-worktree";
|
|
10
|
+
import { createDefaultSdkHostModelResolver, type SdkHostModelResolver } from "../host/model-pin";
|
|
9
11
|
import {
|
|
10
12
|
type DirectoryMigrationPolicy,
|
|
11
13
|
listManagedSessionCandidates,
|
|
@@ -27,9 +29,7 @@ import {
|
|
|
27
29
|
redactBrokerDiscovery,
|
|
28
30
|
} from "./discovery";
|
|
29
31
|
import { deriveIdempotencyIdentity } from "./identity";
|
|
30
|
-
|
|
31
32
|
import { canonicalDeleteLocatorPath, executeLifecycle, isCanonicalSessionId } from "./lifecycle";
|
|
32
|
-
|
|
33
33
|
import {
|
|
34
34
|
type LifecycleDurableEffectsReceipt,
|
|
35
35
|
LifecycleLedger,
|
|
@@ -56,6 +56,22 @@ type ResolvedBrokerSettings = {
|
|
|
56
56
|
resolveDirectoryMigration: (_cwd: string) => Promise<DirectoryMigrationPolicy>;
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
+
function modelResolutionCwd(input: Record<string, unknown>): string | undefined {
|
|
60
|
+
const cwd = typeof input.cwd === "string" ? input.cwd : undefined;
|
|
61
|
+
if (!cwd) return undefined;
|
|
62
|
+
const target = input.target;
|
|
63
|
+
if (!target || typeof target !== "object" || Array.isArray(target)) return cwd;
|
|
64
|
+
const worktree = (target as Record<string, unknown>).worktree;
|
|
65
|
+
if (!worktree || typeof worktree !== "object" || Array.isArray(worktree)) return cwd;
|
|
66
|
+
if ((worktree as Record<string, unknown>).enabled !== true) return cwd;
|
|
67
|
+
const name = (worktree as Record<string, unknown>).name;
|
|
68
|
+
const planned =
|
|
69
|
+
typeof name === "string" && name.length > 0
|
|
70
|
+
? planLaunchWorktree(cwd, { enabled: true, detached: false, name })
|
|
71
|
+
: planLaunchWorktree(cwd, { enabled: true, detached: true, name: null });
|
|
72
|
+
return planned.enabled ? path.resolve(planned.worktreePath) : cwd;
|
|
73
|
+
}
|
|
74
|
+
|
|
59
75
|
export type BrokerErrorCode =
|
|
60
76
|
| "idempotency_conflict"
|
|
61
77
|
| "terminal_uncertain"
|
|
@@ -65,6 +81,8 @@ export type BrokerErrorCode =
|
|
|
65
81
|
| "resource_gone"
|
|
66
82
|
| "invalid_input"
|
|
67
83
|
| "spawn_failed"
|
|
84
|
+
| "ready_then_exited"
|
|
85
|
+
| "endpoint_unreadable"
|
|
68
86
|
| "startup_admission_timeout"
|
|
69
87
|
| "startup_admission_refused"
|
|
70
88
|
| "readiness_timeout"
|
|
@@ -796,6 +814,7 @@ export class Broker {
|
|
|
796
814
|
#completion!: Promise<void>;
|
|
797
815
|
#resolveCompletion!: () => void;
|
|
798
816
|
#rejectCompletion!: (error: unknown) => void;
|
|
817
|
+
readonly #resolveModelPin: SdkHostModelResolver;
|
|
799
818
|
constructor(settings: BrokerSettings) {
|
|
800
819
|
this.settings = {
|
|
801
820
|
agentDir: settings.agentDir,
|
|
@@ -806,6 +825,7 @@ export class Broker {
|
|
|
806
825
|
};
|
|
807
826
|
this.index = new SessionIndex(settings.agentDir);
|
|
808
827
|
this.ledger = new LifecycleLedger(settings.agentDir);
|
|
828
|
+
this.#resolveModelPin = createDefaultSdkHostModelResolver(this.settings.agentDir);
|
|
809
829
|
this.#lock = path.join(settings.agentDir, "sdk", "broker.lock");
|
|
810
830
|
const completion = Promise.withResolvers<void>();
|
|
811
831
|
this.#completion = completion.promise;
|
|
@@ -1496,6 +1516,14 @@ export class Broker {
|
|
|
1496
1516
|
return page;
|
|
1497
1517
|
}
|
|
1498
1518
|
if (operation === "session.get_endpoint") return this.#endpoint(input);
|
|
1519
|
+
if (operation === "model.resolve") {
|
|
1520
|
+
try {
|
|
1521
|
+
const cwd = modelResolutionCwd(input);
|
|
1522
|
+
return { ok: true, result: await this.#resolveModelPin(input.model, { cwd }) };
|
|
1523
|
+
} catch {
|
|
1524
|
+
return error("unavailable", "SDK host model resolution is unavailable.");
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1499
1527
|
if (operation === "broker.lookup_lifecycle") {
|
|
1500
1528
|
const requestedOperation = typeof input.operation === "string" ? input.operation : undefined;
|
|
1501
1529
|
const requestedFingerprint = typeof input.fingerprint === "string" ? input.fingerprint : undefined;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
+
import * as fsSync from "node:fs";
|
|
2
3
|
import * as fs from "node:fs/promises";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
|
|
@@ -24,12 +25,221 @@ export interface RetainedBrokerDiscovery {
|
|
|
24
25
|
close(): void;
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Objects retained broker publication authority opens, in the order and with
|
|
30
|
+
* the access mode the native layer opens them. Every one is opened no-follow,
|
|
31
|
+
* so a symlink or a missing entry withholds authority; the lock record is
|
|
32
|
+
* opened read-only and only the published record read/write, so a
|
|
33
|
+
* readable-but-unwritable lock record is not an obstruction.
|
|
34
|
+
*
|
|
35
|
+
* A wrong file kind is only ever reported through the open the native itself
|
|
36
|
+
* would have failed: `O_DIRECTORY` refuses a non-directory, and the read/write
|
|
37
|
+
* open of the published record refuses a directory with `EISDIR`. The kind is
|
|
38
|
+
* never re-judged after a successful open, because the native layer does not --
|
|
39
|
+
* it accepts, for example, a directory or a character device as the lock
|
|
40
|
+
* record, and refuses a character-device published record only later, for its
|
|
41
|
+
* missing heartbeat. Judging kind here would blame an object the native never
|
|
42
|
+
* rejected and hide the real obstruction.
|
|
43
|
+
*/
|
|
44
|
+
const BROKER_PUBLICATION_OBJECTS = [
|
|
45
|
+
{ name: "sdk", segments: ["sdk"], directory: true, write: false },
|
|
46
|
+
{ name: "sdk/broker.lock", segments: ["sdk", "broker.lock"], directory: true, write: false },
|
|
47
|
+
{
|
|
48
|
+
name: "sdk/broker.lock/owner.json",
|
|
49
|
+
segments: ["sdk", "broker.lock", "owner.json"],
|
|
50
|
+
directory: false,
|
|
51
|
+
write: false,
|
|
52
|
+
},
|
|
53
|
+
{ name: "sdk/broker.json", segments: ["sdk", "broker.json"], directory: false, write: true },
|
|
54
|
+
] as const;
|
|
55
|
+
|
|
56
|
+
/** Width the native layer requires of the published `heartbeatAt` field. */
|
|
57
|
+
const FIXED_WIDTH_HEARTBEAT_DIGITS = 13;
|
|
58
|
+
|
|
59
|
+
/** Bound on the published record the diagnostic will read through its descriptor. */
|
|
60
|
+
const MAX_DIAGNOSTIC_PUBLISHED_BYTES = 1 << 20;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Bound on the agent directory in the failure message. Broker startup persists
|
|
64
|
+
* a 512-character reason, and an unbounded directory would truncate away the
|
|
65
|
+
* obstruction this diagnostic exists to report.
|
|
66
|
+
*/
|
|
67
|
+
const MAX_AGENT_DIR_IN_MESSAGE = 120;
|
|
68
|
+
|
|
69
|
+
function boundedAgentDir(agentDir: string): string {
|
|
70
|
+
return agentDir.length <= MAX_AGENT_DIR_IN_MESSAGE
|
|
71
|
+
? agentDir
|
|
72
|
+
: `...${agentDir.slice(agentDir.length - (MAX_AGENT_DIR_IN_MESSAGE - 3))}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function sanitizedAgentDir(agentDir: string): string {
|
|
76
|
+
const sanitized = agentDir.replace(/[\p{Cc}\p{Cf}\u2028\u2029]/gu, character => {
|
|
77
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
78
|
+
return `\\u${codePoint.toString(16).padStart(4, "0")}`;
|
|
79
|
+
});
|
|
80
|
+
return boundedAgentDir(sanitized);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const NATIVE_RETAINED_FAILURE = /\[retained-publication object=([^;\]]+); reason=([^\]]+)\]$/;
|
|
84
|
+
|
|
85
|
+
function nativeRetainedObstruction(error: unknown): string | undefined {
|
|
86
|
+
if (!(error instanceof Error)) return undefined;
|
|
87
|
+
const match = NATIVE_RETAINED_FAILURE.exec(error.message);
|
|
88
|
+
if (!match) return undefined;
|
|
89
|
+
const [, object, reason] = match;
|
|
90
|
+
if (reason === "heartbeat-missing") return `${object} has no heartbeatAt field`;
|
|
91
|
+
if (reason === "heartbeat-width")
|
|
92
|
+
return `${object} heartbeatAt is not a fixed-width ${FIXED_WIDTH_HEARTBEAT_DIGITS}-digit timestamp`;
|
|
93
|
+
if (reason === "non-regular") return `${object} is not a regular file`;
|
|
94
|
+
if (reason === "errno-ENOENT") return `${object} is missing`;
|
|
95
|
+
if (reason === "errno-EISDIR") return `${object} is not a regular file`;
|
|
96
|
+
if (reason === "unsupported-platform") return "retained publication authority is not implemented on this platform";
|
|
97
|
+
// The native reason names the stage that refused, so the prose must not
|
|
98
|
+
// claim an open failed when the descriptor was already open: `errno-`/`io-`
|
|
99
|
+
// come from the open itself, `read-` from reading the published record, and
|
|
100
|
+
// `clone-`/`metadata` from inspecting an object already opened.
|
|
101
|
+
if (reason.startsWith("errno-")) return `${object} could not be opened (${reason.slice("errno-".length)})`;
|
|
102
|
+
if (reason.startsWith("io-")) return `${object} could not be opened (${reason.slice("io-".length)})`;
|
|
103
|
+
if (reason.startsWith("read-")) return `${object} could not be read (${reason.slice("read-".length)})`;
|
|
104
|
+
if (reason.startsWith("clone-")) return `${object} could not be inspected (${reason.slice("clone-".length)})`;
|
|
105
|
+
if (reason === "metadata") return `${object} could not be inspected`;
|
|
106
|
+
if (reason.startsWith("metadata-")) return `${object} could not be inspected (${reason.slice("metadata-".length)})`;
|
|
107
|
+
return `${object} withheld publication authority (${reason})`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Name the first condition the published record itself violates, reading only
|
|
112
|
+
* through the descriptor the no-follow open already verified. Reopening by
|
|
113
|
+
* pathname would let a replacement in that window redirect the diagnostic to a
|
|
114
|
+
* different object, or block on a name that is no longer a regular file.
|
|
115
|
+
*/
|
|
116
|
+
function describeWithheldPublishedRecord(descriptor: number, size: number): string | undefined {
|
|
117
|
+
if (size > MAX_DIAGNOSTIC_PUBLISHED_BYTES) return undefined;
|
|
118
|
+
const bytes = Buffer.alloc(Math.min(MAX_DIAGNOSTIC_PUBLISHED_BYTES, Math.max(size, 4096)));
|
|
119
|
+
let read = 0;
|
|
120
|
+
try {
|
|
121
|
+
while (read < bytes.length) {
|
|
122
|
+
const count = fsSync.readSync(descriptor, bytes, read, bytes.length - read, read);
|
|
123
|
+
if (count === 0) break;
|
|
124
|
+
read += count;
|
|
125
|
+
}
|
|
126
|
+
} catch (error) {
|
|
127
|
+
return `sdk/broker.json could not be read (${(error as NodeJS.ErrnoException).code ?? "unknown error"})`;
|
|
128
|
+
}
|
|
129
|
+
let hasMore = false;
|
|
130
|
+
if (read === bytes.length) {
|
|
131
|
+
try {
|
|
132
|
+
hasMore = fsSync.readSync(descriptor, Buffer.alloc(1), 0, 1, read) > 0;
|
|
133
|
+
} catch {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// latin1 keeps one character per byte, so offsets match the native's byte scan.
|
|
138
|
+
const published = bytes.subarray(0, read).toString("latin1");
|
|
139
|
+
const needle = '"heartbeatAt":';
|
|
140
|
+
const at = published.indexOf(needle);
|
|
141
|
+
// A record larger than this bounded read is inconclusive, never an absence.
|
|
142
|
+
if (at < 0) return hasMore ? undefined : "sdk/broker.json has no heartbeatAt field";
|
|
143
|
+
const digits = published.slice(at + needle.length, at + needle.length + FIXED_WIDTH_HEARTBEAT_DIGITS);
|
|
144
|
+
const next = published.charAt(at + needle.length + FIXED_WIDTH_HEARTBEAT_DIGITS);
|
|
145
|
+
if (!/^[0-9]{13}$/.test(digits) || /^[0-9]$/.test(next))
|
|
146
|
+
return `sdk/broker.json heartbeatAt is not a fixed-width ${FIXED_WIDTH_HEARTBEAT_DIGITS}-digit timestamp`;
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function isSymbolicLink(pathname: string): boolean {
|
|
151
|
+
try {
|
|
152
|
+
return fsSync.lstatSync(pathname).isSymbolicLink();
|
|
153
|
+
} catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Name the first condition that withholds retained publication authority.
|
|
160
|
+
*
|
|
161
|
+
* The native layer reports withheld authority as one opaque failure, so a
|
|
162
|
+
* broker that cannot retain its own publication dies without naming the object
|
|
163
|
+
* responsible. Operators then cannot distinguish a missing lock record from a
|
|
164
|
+
* redirected `sdk` directory (a symlinked agent-directory entry is refused by
|
|
165
|
+
* the no-follow open, which is how shared multi-account layouts fail), and the
|
|
166
|
+
* only way to learn the precondition is to read the native source.
|
|
167
|
+
*
|
|
168
|
+
* This inspects the same objects in the same order and returns a description of
|
|
169
|
+
* the first obstruction, or `undefined` when every precondition holds and the
|
|
170
|
+
* refusal came from a race the caller must still fail closed on.
|
|
171
|
+
*/
|
|
172
|
+
function describeWithheldPublicationAuthority(agentDir: string): string | undefined {
|
|
173
|
+
for (const object of BROKER_PUBLICATION_OBJECTS) {
|
|
174
|
+
const pathname = path.join(agentDir, ...object.segments);
|
|
175
|
+
// O_NONBLOCK keeps the diagnostic itself from parking on an object that is
|
|
176
|
+
// no longer a regular file; the following fstat still names the wrong kind.
|
|
177
|
+
const flags = object.directory
|
|
178
|
+
? fsSync.constants.O_RDONLY | fsSync.constants.O_DIRECTORY | fsSync.constants.O_NOFOLLOW
|
|
179
|
+
: (object.write ? fsSync.constants.O_RDWR : fsSync.constants.O_RDONLY) |
|
|
180
|
+
fsSync.constants.O_NOFOLLOW |
|
|
181
|
+
fsSync.constants.O_NONBLOCK;
|
|
182
|
+
let descriptor: number;
|
|
183
|
+
try {
|
|
184
|
+
descriptor = fsSync.openSync(pathname, flags);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
187
|
+
if (code === "ENOENT") return `${object.name} is missing`;
|
|
188
|
+
// A refused open is already authoritative; this only labels it. Linux
|
|
189
|
+
// answers a no-follow directory open on a symlink with ENOTDIR rather
|
|
190
|
+
// than ELOOP, so the entry kind decides between the two labels.
|
|
191
|
+
if (code === "ELOOP" || code === "ENOTDIR") {
|
|
192
|
+
if (isSymbolicLink(pathname))
|
|
193
|
+
return `${object.name} is a symlink; every publication object is opened no-follow`;
|
|
194
|
+
if (code === "ENOTDIR" && object.directory) return `${object.name} is not a directory`;
|
|
195
|
+
}
|
|
196
|
+
if (code === "EISDIR") return `${object.name} is not a regular file`;
|
|
197
|
+
return `${object.name} could not be opened (${code ?? "unknown error"})`;
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
// The published record is the last precondition: the native layer edits
|
|
201
|
+
// the heartbeat in place and therefore requires a fixed-width field. It is
|
|
202
|
+
// read through this verified descriptor, never reopened by name.
|
|
203
|
+
if (object.write) return describeWithheldPublishedRecord(descriptor, fsSync.fstatSync(descriptor).size);
|
|
204
|
+
} catch (error) {
|
|
205
|
+
return `${object.name} could not be inspected (${(error as NodeJS.ErrnoException).code ?? "unknown error"})`;
|
|
206
|
+
} finally {
|
|
207
|
+
try {
|
|
208
|
+
fsSync.closeSync(descriptor);
|
|
209
|
+
} catch {
|
|
210
|
+
// Closing a diagnostic-only descriptor is best effort; the native refusal
|
|
211
|
+
// remains the authoritative error when this probe itself is inconclusive.
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
|
|
27
218
|
function requireRetainedBrokerPublication(agentDir: string): NativeRetainedBrokerPublication {
|
|
28
219
|
const retainBrokerPublication = nativeBrokerDiscovery().retainBrokerPublication;
|
|
29
220
|
if (typeof retainBrokerPublication !== "function") {
|
|
30
221
|
throw new Error("Loaded native bindings do not expose retained broker publication authority.");
|
|
31
222
|
}
|
|
32
|
-
|
|
223
|
+
try {
|
|
224
|
+
return retainBrokerPublication(agentDir);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
// Fail closed exactly as before; only name the condition on the way out.
|
|
227
|
+
const obstruction = nativeRetainedObstruction(error);
|
|
228
|
+
if (obstruction) {
|
|
229
|
+
throw new Error(
|
|
230
|
+
`${error instanceof Error ? error.message : String(error)} (${obstruction}; agent directory ${sanitizedAgentDir(agentDir)})`,
|
|
231
|
+
{ cause: error },
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
const observed = describeWithheldPublicationAuthority(agentDir);
|
|
235
|
+
if (!observed) throw error;
|
|
236
|
+
// The obstruction precedes the agent directory, and the directory is
|
|
237
|
+
// bounded, so a long valid path cannot truncate the named object away.
|
|
238
|
+
throw new Error(
|
|
239
|
+
`${error instanceof Error ? error.message : String(error)} (current observed state: ${observed}; agent directory ${sanitizedAgentDir(agentDir)})`,
|
|
240
|
+
{ cause: error },
|
|
241
|
+
);
|
|
242
|
+
}
|
|
33
243
|
}
|
|
34
244
|
|
|
35
245
|
async function rollbackPublishedBrokerDiscovery(agentDir: string, discovery: BrokerDiscovery): Promise<void> {
|
|
@@ -216,9 +426,14 @@ export async function publishBrokerDiscovery(
|
|
|
216
426
|
try {
|
|
217
427
|
await rollbackPublishedBrokerDiscovery(agentDir, published);
|
|
218
428
|
} catch (rollbackError) {
|
|
429
|
+
// The aggregate message is the only thing the durable startup-failure
|
|
430
|
+
// marker persists (`AggregateError.errors` is not serialized), so the named
|
|
431
|
+
// obstruction has to survive into it rather than only into `errors[0]`.
|
|
219
432
|
throw new AggregateError(
|
|
220
433
|
[error, rollbackError],
|
|
221
|
-
|
|
434
|
+
`Broker publication authority acquisition and exact rollback both failed. Acquisition: ${
|
|
435
|
+
error instanceof Error ? error.message : String(error)
|
|
436
|
+
}`,
|
|
222
437
|
);
|
|
223
438
|
}
|
|
224
439
|
throw error;
|