@gajae-code/coding-agent 0.17.2 → 0.17.4
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 +194 -0
- package/dist/types/cli/daemon-cli.d.ts +3 -1
- package/dist/types/cli/github-release.d.ts +17 -0
- package/dist/types/cli/public-command-entry.d.ts +56 -0
- package/dist/types/cli/public-command-errors.d.ts +126 -0
- package/dist/types/cli/public-command-evidence.d.ts +99 -0
- package/dist/types/cli/public-command-help.d.ts +65 -0
- package/dist/types/cli/public-command-registry.d.ts +69 -0
- package/dist/types/cli/session-picker.d.ts +1 -1
- package/dist/types/cli/setup-cli.d.ts +1 -0
- package/dist/types/cli/skills-cli.d.ts +5 -0
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/commands/daemon.d.ts +4 -50
- package/dist/types/commands/sdk.d.ts +20 -37
- package/dist/types/commands/setup.d.ts +3 -0
- package/dist/types/commands/skills.d.ts +9 -0
- package/dist/types/config/autorouting-tier-map.d.ts +18 -0
- package/dist/types/config/file-lock.d.ts +9 -1
- package/dist/types/config/model-profile-activation.d.ts +45 -1
- package/dist/types/config/model-registry.d.ts +11 -2
- package/dist/types/config/settings-schema.d.ts +11 -0
- package/dist/types/coordinator-mcp/question-state.d.ts +6 -1
- package/dist/types/daemon/operator-contract.d.ts +5 -1
- package/dist/types/eval/js/worker-protocol.d.ts +2 -0
- package/dist/types/extensibility/extensions/types.d.ts +12 -0
- package/dist/types/extensibility/gjc-plugins/registry.d.ts +4 -0
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +18 -0
- package/dist/types/extensibility/shared-events.d.ts +3 -1
- package/dist/types/gjc-runtime/session-state-lock.d.ts +61 -0
- package/dist/types/internal-urls/embedded-protocol.d.ts +9 -0
- package/dist/types/internal-urls/index.d.ts +1 -0
- package/dist/types/internal-urls/parse.d.ts +9 -1
- package/dist/types/main.d.ts +22 -0
- package/dist/types/modes/acp/acp-agent.d.ts +55 -1
- package/dist/types/modes/acp/startup-provenance.d.ts +16 -0
- package/dist/types/modes/components/custom-provider-wizard.d.ts +19 -1
- package/dist/types/modes/components/session-selector.d.ts +5 -2
- package/dist/types/modes/components/user-message-selector.d.ts +5 -2
- package/dist/types/modes/controllers/command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +11 -1
- package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +14 -1
- package/dist/types/modes/types.d.ts +2 -2
- package/dist/types/runtime/process-lifecycle.d.ts +2 -0
- package/dist/types/runtime-mcp/manager.d.ts +56 -0
- package/dist/types/runtime-mcp/pool.d.ts +17 -0
- package/dist/types/runtime-mcp/redaction.d.ts +20 -0
- package/dist/types/runtime-mcp/startup-policy.d.ts +6 -0
- package/dist/types/runtime-mcp/tool-bridge.d.ts +9 -0
- package/dist/types/runtime-mcp/transports/http.d.ts +11 -0
- package/dist/types/sdk/broker/broker.d.ts +13 -2
- package/dist/types/sdk/broker/identity.d.ts +3 -1
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +23 -3
- package/dist/types/sdk/broker/lifecycle.d.ts +15 -8
- package/dist/types/sdk/broker/runtime.d.ts +1 -1
- package/dist/types/sdk/broker/session-index.d.ts +22 -3
- package/dist/types/sdk/broker/spawn-authority.d.ts +4 -0
- package/dist/types/sdk/broker/stderr-drainer.d.ts +13 -0
- package/dist/types/sdk/bus/index.d.ts +19 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +5 -0
- package/dist/types/sdk/cli/master-cli.d.ts +6 -10
- package/dist/types/sdk/cli/rows.d.ts +19 -1
- package/dist/types/sdk/cli/session-cli.d.ts +22 -5
- package/dist/types/sdk/client/client.d.ts +2 -0
- package/dist/types/sdk/guides/cli.d.ts +3 -4
- package/dist/types/sdk/guides/verify.d.ts +2 -0
- package/dist/types/sdk/host/host.d.ts +9 -0
- package/dist/types/sdk/host/session-runtime.d.ts +31 -0
- package/dist/types/sdk/host/types.d.ts +10 -0
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/prompt-deadline-flush.d.ts +83 -0
- package/dist/types/sdk/prompt-deadline-manager.d.ts +37 -0
- package/dist/types/sdk/prompt-tool-boundary.d.ts +73 -0
- package/dist/types/sdk/router/session-router.d.ts +2 -0
- package/dist/types/sdk/startup-capability.d.ts +2 -0
- package/dist/types/sdk/transport/relay.d.ts +1 -0
- package/dist/types/sdk/transport/serve-cli.d.ts +48 -4
- package/dist/types/sdk/transport/stdio.d.ts +5 -1
- package/dist/types/session/agent-session.d.ts +168 -21
- package/dist/types/session/fallback-chain-controller.d.ts +6 -6
- package/dist/types/session/fold-coordinator.d.ts +17 -3
- package/dist/types/session/internal/managed-session-storage.d.ts +7 -1
- package/dist/types/session/session-manager.d.ts +64 -1
- package/dist/types/session/session-work-lease.d.ts +14 -0
- package/dist/types/setup/model-onboarding-guidance.d.ts +1 -1
- package/dist/types/setup/provider-onboarding.d.ts +81 -1
- package/dist/types/tools/bash.d.ts +37 -11
- package/dist/types/tools/job.d.ts +1 -1
- package/dist/types/tools/steer-fold.d.ts +31 -0
- package/dist/types/utils/git.d.ts +28 -2
- package/dist/types/utils/mupdf-embedded.d.ts +2 -0
- package/dist/types/utils/mupdf.d.ts +4 -0
- package/dist/types/utils/session-title-generation.d.ts +6 -0
- package/dist/types/utils/title-generator.d.ts +11 -2
- package/dist/types/web/scrapers/types.d.ts +3 -0
- package/package.json +18 -10
- package/scripts/build-binary.ts +9 -3
- package/scripts/compile-args.ts +0 -9
- package/scripts/embed-mupdf.ts +52 -0
- package/scripts/generate-sdk-operation-inventory.ts +31 -1
- package/scripts/vendor-markit.ts +196 -0
- package/src/async/job-manager.ts +22 -1
- package/src/cli/daemon-cli.ts +53 -18
- package/src/cli/github-release.ts +87 -5
- package/src/cli/mcp-cli.ts +38 -4
- package/src/cli/public-command-entry.ts +450 -0
- package/src/cli/public-command-errors.ts +581 -0
- package/src/cli/public-command-evidence.ts +717 -0
- package/src/cli/public-command-help.ts +360 -0
- package/src/cli/public-command-registry.ts +535 -0
- package/src/cli/session-picker.ts +2 -1
- package/src/cli/setup-cli.ts +9 -1
- package/src/cli/skills-cli.ts +46 -2
- package/src/cli.ts +61 -1
- package/src/commands/daemon.ts +65 -69
- package/src/commands/sdk.ts +175 -117
- package/src/commands/setup.ts +2 -0
- package/src/commands/skills.ts +11 -0
- package/src/config/autorouting-tier-map.ts +14 -0
- package/src/config/file-lock.ts +147 -51
- package/src/config/model-profile-activation.ts +334 -43
- package/src/config/model-registry.ts +183 -25
- package/src/config/settings-schema.ts +26 -2
- package/src/coordinator-mcp/policy.ts +10 -5
- package/src/coordinator-mcp/question-gate-codec.ts +17 -0
- package/src/coordinator-mcp/question-state.ts +175 -8
- package/src/coordinator-mcp/server.ts +507 -81
- package/src/coordinator-mcp/session-reaper.ts +6 -4
- package/src/crash/record-loader.ts +6 -2
- package/src/daemon/operator-contract.ts +26 -12
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +1 -1
- package/src/eval/js/context-manager.ts +8 -8
- package/src/eval/js/worker-core.ts +9 -4
- package/src/eval/js/worker-protocol.ts +2 -0
- package/src/extensibility/extensions/runner.ts +52 -4
- package/src/extensibility/extensions/types.ts +10 -0
- package/src/extensibility/gjc-plugins/registry.ts +196 -37
- package/src/extensibility/runtime-skill-discovery.ts +61 -3
- package/src/extensibility/shared-events.ts +3 -1
- package/src/gjc-runtime/session-state-lock.ts +165 -24
- package/src/gjc-runtime/ultragoal-succession.ts +31 -3
- package/src/internal-urls/docs-index.generated.ts +20 -19
- package/src/internal-urls/embedded-protocol.ts +83 -0
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/parse.ts +21 -1
- package/src/internal-urls/router.ts +4 -2
- package/src/main.ts +123 -55
- package/src/modes/acp/acp-agent.ts +408 -35
- package/src/modes/acp/startup-provenance.ts +59 -0
- package/src/modes/components/custom-provider-wizard.ts +308 -7
- package/src/modes/components/oauth-selector.ts +9 -0
- package/src/modes/components/session-selector.ts +47 -4
- package/src/modes/components/user-message-selector.ts +57 -15
- package/src/modes/controllers/command-controller.ts +58 -32
- package/src/modes/controllers/extension-ui-controller.ts +33 -7
- package/src/modes/controllers/input-controller.ts +4 -0
- package/src/modes/controllers/selector-controller.ts +161 -15
- package/src/modes/interactive-mode.ts +21 -7
- package/src/modes/runtime-init.ts +1 -0
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +124 -7
- package/src/modes/shared/agent-wire/workflow-gate-types.ts +16 -2
- package/src/modes/types.ts +2 -2
- package/src/prompts/agent-fragments/browser-aside.md +1 -0
- package/src/prompts/system/title-system.md +1 -1
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/job.md +1 -1
- package/src/prompts/tools/read.md +2 -0
- package/src/runtime/network-prewarm-service.test.ts +36 -1
- package/src/runtime/network-prewarm-service.ts +28 -3
- package/src/runtime/process-lifecycle.ts +22 -11
- package/src/runtime-mcp/config.ts +16 -1
- package/src/runtime-mcp/manager.ts +689 -55
- package/src/runtime-mcp/pool.test.ts +31 -0
- package/src/runtime-mcp/pool.ts +304 -28
- package/src/runtime-mcp/redaction.ts +42 -0
- package/src/runtime-mcp/startup-policy.ts +7 -0
- package/src/runtime-mcp/tool-bridge.ts +71 -0
- package/src/runtime-mcp/transports/http.ts +108 -25
- package/src/sdk/acp/adapter.ts +27 -8
- package/src/sdk/broker/broker.ts +378 -52
- package/src/sdk/broker/identity.ts +9 -1
- package/src/sdk/broker/lifecycle-ledger.ts +95 -31
- package/src/sdk/broker/lifecycle.ts +690 -141
- package/src/sdk/broker/runtime.ts +1 -1
- package/src/sdk/broker/session-index.ts +97 -4
- package/src/sdk/broker/spawn-authority.ts +35 -7
- package/src/sdk/broker/stderr-drainer.ts +59 -0
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +1 -0
- package/src/sdk/bus/index.ts +690 -80
- package/src/sdk/bus/telegram-daemon-cli.ts +31 -2
- package/src/sdk/bus/telegram-daemon-contract.ts +3 -1
- package/src/sdk/bus/telegram-daemon.ts +10 -3
- package/src/sdk/cli/master-cli.ts +47 -36
- package/src/sdk/cli/rows.ts +30 -7
- package/src/sdk/cli/session-cli.ts +687 -184
- package/src/sdk/client/client.ts +18 -0
- package/src/sdk/guides/catalog.ts +2 -2
- package/src/sdk/guides/cli.ts +20 -62
- package/src/sdk/guides/verify.ts +14 -0
- package/src/sdk/host/host.ts +49 -5
- package/src/sdk/host/session-runtime.test.ts +138 -2
- package/src/sdk/host/session-runtime.ts +311 -97
- package/src/sdk/host/types.ts +12 -1
- package/src/sdk/host/websocket-transport.lifecycle.test.ts +84 -0
- package/src/sdk/host/websocket-transport.ts +40 -6
- package/src/sdk/index.ts +11 -0
- package/src/sdk/prompt-deadline-flush.ts +334 -0
- package/src/sdk/prompt-deadline-manager.ts +133 -1
- package/src/sdk/prompt-tool-boundary.ts +121 -0
- package/src/sdk/protocol/operation-inventory.generated.json +106 -0
- package/src/sdk/router/session-router.ts +9 -1
- package/src/sdk/session.ts +182 -7
- package/src/sdk/startup-capability.ts +1 -1
- package/src/sdk/transport/relay.ts +71 -6
- package/src/sdk/transport/serve-cli.ts +397 -42
- package/src/sdk/transport/socket.ts +12 -11
- package/src/sdk/transport/stdio.ts +7 -3
- package/src/session/agent-session.ts +2437 -400
- package/src/session/fallback-chain-controller.ts +6 -7
- package/src/session/fold-coordinator.ts +24 -5
- package/src/session/internal/managed-session-scope.ts +58 -18
- package/src/session/internal/managed-session-storage.ts +304 -11
- package/src/session/session-manager.ts +382 -44
- package/src/session/session-work-lease.ts +34 -0
- package/src/setup/model-onboarding-guidance.ts +1 -1
- package/src/setup/provider-onboarding.ts +397 -24
- package/src/slash-commands/builtin-registry.ts +258 -14
- package/src/task/index.ts +14 -2
- package/src/tools/bash.ts +98 -78
- package/src/tools/fetch.ts +119 -20
- package/src/tools/job.ts +151 -25
- package/src/tools/output-meta.ts +15 -2
- package/src/tools/path-utils.ts +27 -3
- package/src/tools/read.ts +3 -3
- package/src/tools/steer-fold.ts +107 -0
- package/src/tools/tool-catalog.generated.ts +29 -2
- package/src/tools/tool-errors.ts +4 -0
- package/src/utils/git.ts +53 -3
- package/src/utils/markit.ts +55 -41
- package/src/utils/mupdf-assets.d.ts +4 -0
- package/src/utils/mupdf-embedded.ts +4 -0
- package/src/utils/mupdf.ts +129 -0
- package/src/utils/session-title-generation.ts +29 -0
- package/src/utils/title-generator.ts +46 -2
- package/src/web/scrapers/types.ts +24 -3
- package/vendor/markit-ai/LICENSE +21 -0
- package/vendor/markit-ai/MANIFEST.json +412 -0
- package/vendor/markit-ai/dist/commands/config.d.ts +4 -0
- package/vendor/markit-ai/dist/commands/config.js +139 -0
- package/vendor/markit-ai/dist/commands/convert.d.ts +6 -0
- package/vendor/markit-ai/dist/commands/convert.js +113 -0
- package/vendor/markit-ai/dist/commands/formats.d.ts +2 -0
- package/vendor/markit-ai/dist/commands/formats.js +92 -0
- package/vendor/markit-ai/dist/commands/init.d.ts +2 -0
- package/vendor/markit-ai/dist/commands/init.js +29 -0
- package/vendor/markit-ai/dist/commands/onboard.d.ts +2 -0
- package/vendor/markit-ai/dist/commands/onboard.js +61 -0
- package/vendor/markit-ai/dist/commands/plugin.d.ts +4 -0
- package/vendor/markit-ai/dist/commands/plugin.js +58 -0
- package/vendor/markit-ai/dist/config.d.ts +26 -0
- package/vendor/markit-ai/dist/config.js +42 -0
- package/vendor/markit-ai/dist/converters/audio.d.ts +7 -0
- package/vendor/markit-ai/dist/converters/audio.js +104 -0
- package/vendor/markit-ai/dist/converters/csv.d.ts +7 -0
- package/vendor/markit-ai/dist/converters/csv.js +83 -0
- package/vendor/markit-ai/dist/converters/docx.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/docx.js +57 -0
- package/vendor/markit-ai/dist/converters/epub.d.ts +8 -0
- package/vendor/markit-ai/dist/converters/epub.js +128 -0
- package/vendor/markit-ai/dist/converters/github.d.ts +18 -0
- package/vendor/markit-ai/dist/converters/github.js +148 -0
- package/vendor/markit-ai/dist/converters/html.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/html.js +30 -0
- package/vendor/markit-ai/dist/converters/image.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/image.js +124 -0
- package/vendor/markit-ai/dist/converters/ipynb.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/ipynb.js +75 -0
- package/vendor/markit-ai/dist/converters/iwork.d.ts +20 -0
- package/vendor/markit-ai/dist/converters/iwork.js +391 -0
- package/vendor/markit-ai/dist/converters/json.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/json.js +21 -0
- package/vendor/markit-ai/dist/converters/pdf/columns.d.ts +35 -0
- package/vendor/markit-ai/dist/converters/pdf/columns.js +93 -0
- package/vendor/markit-ai/dist/converters/pdf/extract.d.ts +19 -0
- package/vendor/markit-ai/dist/converters/pdf/extract.js +514 -0
- package/vendor/markit-ai/dist/converters/pdf/grid.d.ts +25 -0
- package/vendor/markit-ai/dist/converters/pdf/grid.js +795 -0
- package/vendor/markit-ai/dist/converters/pdf/headers.d.ts +24 -0
- package/vendor/markit-ai/dist/converters/pdf/headers.js +108 -0
- package/vendor/markit-ai/dist/converters/pdf/index.d.ts +19 -0
- package/vendor/markit-ai/dist/converters/pdf/index.js +136 -0
- package/vendor/markit-ai/dist/converters/pdf/mupdf-loader.js +9 -0
- package/vendor/markit-ai/dist/converters/pdf/render.d.ts +24 -0
- package/vendor/markit-ai/dist/converters/pdf/render.js +513 -0
- package/vendor/markit-ai/dist/converters/pdf/types.d.ts +75 -0
- package/vendor/markit-ai/dist/converters/pdf/types.js +1 -0
- package/vendor/markit-ai/dist/converters/plain-text.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/plain-text.js +87 -0
- package/vendor/markit-ai/dist/converters/pptx.d.ts +8 -0
- package/vendor/markit-ai/dist/converters/pptx.js +289 -0
- package/vendor/markit-ai/dist/converters/rss.d.ts +11 -0
- package/vendor/markit-ai/dist/converters/rss.js +138 -0
- package/vendor/markit-ai/dist/converters/wikipedia.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/wikipedia.js +33 -0
- package/vendor/markit-ai/dist/converters/xlsx.d.ts +8 -0
- package/vendor/markit-ai/dist/converters/xlsx.js +147 -0
- package/vendor/markit-ai/dist/converters/xml.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/xml.js +18 -0
- package/vendor/markit-ai/dist/converters/yaml.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/yaml.js +17 -0
- package/vendor/markit-ai/dist/converters/zip.d.ts +8 -0
- package/vendor/markit-ai/dist/converters/zip.js +57 -0
- package/vendor/markit-ai/dist/index.d.ts +30 -0
- package/vendor/markit-ai/dist/index.js +26 -0
- package/vendor/markit-ai/dist/main.d.ts +2 -0
- package/vendor/markit-ai/dist/main.js +191 -0
- package/vendor/markit-ai/dist/markit.d.ts +45 -0
- package/vendor/markit-ai/dist/markit.js +267 -0
- package/vendor/markit-ai/dist/plugins/api.d.ts +7 -0
- package/vendor/markit-ai/dist/plugins/api.js +42 -0
- package/vendor/markit-ai/dist/plugins/index.d.ts +4 -0
- package/vendor/markit-ai/dist/plugins/index.js +3 -0
- package/vendor/markit-ai/dist/plugins/installer.d.ts +25 -0
- package/vendor/markit-ai/dist/plugins/installer.js +176 -0
- package/vendor/markit-ai/dist/plugins/loader.d.ts +6 -0
- package/vendor/markit-ai/dist/plugins/loader.js +61 -0
- package/vendor/markit-ai/dist/plugins/types.d.ts +25 -0
- package/vendor/markit-ai/dist/plugins/types.js +1 -0
- package/vendor/markit-ai/dist/providers/anthropic.d.ts +2 -0
- package/vendor/markit-ai/dist/providers/anthropic.js +47 -0
- package/vendor/markit-ai/dist/providers/index.d.ts +21 -0
- package/vendor/markit-ai/dist/providers/index.js +56 -0
- package/vendor/markit-ai/dist/providers/openai.d.ts +2 -0
- package/vendor/markit-ai/dist/providers/openai.js +76 -0
- package/vendor/markit-ai/dist/providers/types.d.ts +26 -0
- package/vendor/markit-ai/dist/providers/types.js +1 -0
- package/vendor/markit-ai/dist/types.d.ts +36 -0
- package/vendor/markit-ai/dist/types.js +1 -0
- package/vendor/markit-ai/dist/utils/exit-codes.d.ts +4 -0
- package/vendor/markit-ai/dist/utils/exit-codes.js +4 -0
- package/vendor/markit-ai/dist/utils/output.d.ts +22 -0
- package/vendor/markit-ai/dist/utils/output.js +31 -0
- package/vendor/markit-ai/dist/utils/turndown.d.ts +8 -0
- package/vendor/markit-ai/dist/utils/turndown.js +70 -0
- package/vendor/markit-ai/package.json +74 -0
- package/vendor/markit-ai.patch +49 -0
|
@@ -54,6 +54,16 @@ export interface RuntimeSkillDiscoveryResult {
|
|
|
54
54
|
candidates: RuntimeSkillDiscoveryCandidate[];
|
|
55
55
|
/** Deduped skill metadata the query filter ran against, including bundled entries; candidates.length <= scanned. */
|
|
56
56
|
scanned: number;
|
|
57
|
+
/**
|
|
58
|
+
* Skills left after the query filter, before the page slice: the denominator
|
|
59
|
+
* a page is a page *of*. `scanned` counts everything the filter ran against,
|
|
60
|
+
* so it cannot serve this role.
|
|
61
|
+
*/
|
|
62
|
+
matching: number;
|
|
63
|
+
/** Normalized zero-based offset the returned page starts at. */
|
|
64
|
+
offset: number;
|
|
65
|
+
/** Offset of the next page, present only while one exists; omitted on the final page. */
|
|
66
|
+
nextOffset?: number;
|
|
57
67
|
diagnostics: RuntimeSkillDiscoveryDiagnostics;
|
|
58
68
|
}
|
|
59
69
|
|
|
@@ -65,6 +75,8 @@ export interface DiscoverRuntimeSkillsOptions {
|
|
|
65
75
|
profileAuthority?: "default" | "custom";
|
|
66
76
|
query?: string;
|
|
67
77
|
limit?: number;
|
|
78
|
+
/** Zero-based index into the matching set the returned page starts at; defaults to 0. */
|
|
79
|
+
offset?: number;
|
|
68
80
|
source?: RuntimeSkillDiscoveryScope | "all";
|
|
69
81
|
policy?: SkillsSettings;
|
|
70
82
|
}
|
|
@@ -74,11 +86,26 @@ function getRuntimeHome(): string {
|
|
|
74
86
|
}
|
|
75
87
|
|
|
76
88
|
const DEFAULT_LIMIT = 20;
|
|
77
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Upper bound every caller's `limit` clamps to. Exported so `gjc skills discover`
|
|
91
|
+
* can default a human to the widest page the library will serve instead of the
|
|
92
|
+
* model-context-sized `DEFAULT_LIMIT`.
|
|
93
|
+
*/
|
|
94
|
+
export const SKILL_DISCOVERY_MAX_LIMIT = 50;
|
|
78
95
|
const MAX_DIAGNOSTICS = 10;
|
|
79
96
|
function normalizeLimit(limit: number | undefined): number {
|
|
80
97
|
if (limit === undefined || !Number.isFinite(limit)) return DEFAULT_LIMIT;
|
|
81
|
-
return Math.max(1, Math.min(
|
|
98
|
+
return Math.max(1, Math.min(SKILL_DISCOVERY_MAX_LIMIT, Math.trunc(limit)));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Offsets are clamped from below only. An offset past the end is a legitimate
|
|
103
|
+
* empty final page (the catalog shrank between pages), not an error, so it is
|
|
104
|
+
* reported rather than silently snapped back onto the last populated page.
|
|
105
|
+
*/
|
|
106
|
+
function normalizeOffset(offset: number | undefined): number {
|
|
107
|
+
if (offset === undefined || !Number.isFinite(offset) || offset < 0) return 0;
|
|
108
|
+
return Math.trunc(offset);
|
|
82
109
|
}
|
|
83
110
|
|
|
84
111
|
interface ProjectScanDir {
|
|
@@ -558,9 +585,40 @@ export async function discoverRuntimeSkills(
|
|
|
558
585
|
seenNames,
|
|
559
586
|
diagnostics,
|
|
560
587
|
);
|
|
588
|
+
const limit = normalizeLimit(options.limit);
|
|
589
|
+
const offset = normalizeOffset(options.offset);
|
|
590
|
+
const matching = candidates.length;
|
|
591
|
+
const page = candidates.slice(offset, offset + limit);
|
|
592
|
+
// Continuation is omitted on the final page, so "is there more" is the
|
|
593
|
+
// presence of the field rather than a value the caller has to compare.
|
|
594
|
+
const nextOffset = offset + page.length < matching ? offset + page.length : undefined;
|
|
595
|
+
// Appended directly instead of through pushDiagnostic: these are the only
|
|
596
|
+
// messages that explain why a skill the caller can see on disk is absent
|
|
597
|
+
// from the result, and the reported case (several stale
|
|
598
|
+
// skills.customDirectories entries, one "does not exist" message each)
|
|
599
|
+
// fills the MAX_DIAGNOSTICS budget before the slice is ever reached. A
|
|
600
|
+
// dropped paging notice makes the truncation invisible again.
|
|
601
|
+
if (page.length === 0) {
|
|
602
|
+
if (offset > 0 && matching > 0) {
|
|
603
|
+
diagnostics.push(
|
|
604
|
+
`no skills at offset ${offset}; ${matching} matching skill${matching === 1 ? "" : "s"} total, pass --offset 0 to start over`,
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
} else if (nextOffset !== undefined) {
|
|
608
|
+
diagnostics.push(
|
|
609
|
+
`showing ${offset + 1}-${offset + page.length} of ${matching} matching skills; pass --offset ${nextOffset} for the next page, or narrow the query`,
|
|
610
|
+
);
|
|
611
|
+
} else if (offset > 0) {
|
|
612
|
+
// Final page of a multi-page walk: no continuation to offer, but the
|
|
613
|
+
// caller still needs to know this page is not the whole matching set.
|
|
614
|
+
diagnostics.push(`showing ${offset + 1}-${offset + page.length} of ${matching} matching skills`);
|
|
615
|
+
}
|
|
561
616
|
return {
|
|
562
|
-
candidates:
|
|
617
|
+
candidates: page,
|
|
563
618
|
scanned,
|
|
619
|
+
matching,
|
|
620
|
+
offset,
|
|
621
|
+
...(nextOffset === undefined ? {} : { nextOffset }),
|
|
564
622
|
diagnostics: { messages: diagnostics },
|
|
565
623
|
};
|
|
566
624
|
}
|
|
@@ -93,9 +93,11 @@ export interface SessionCompactEvent {
|
|
|
93
93
|
fromExtension: boolean;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
/** Fired
|
|
96
|
+
/** Fired during session teardown, including process exit and SDK host self-reap. */
|
|
97
97
|
export interface SessionShutdownEvent {
|
|
98
98
|
type: "session_shutdown";
|
|
99
|
+
/** Set only when the SDK host reaped itself after detached-idle grace. */
|
|
100
|
+
reason?: "detached_idle";
|
|
99
101
|
}
|
|
100
102
|
|
|
101
103
|
/** Preparation data for tree navigation (used by session_before_tree event) */
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
readFileLockObservationForGc,
|
|
15
15
|
} from "../config/file-lock";
|
|
16
16
|
import { loadInstallationHostId, loadLegacyInstallationHostId } from "../config/machine-identity";
|
|
17
|
-
import { readLinuxProcStartTimeSync } from "./linux-proc";
|
|
17
|
+
import { type LinuxProcPidProbeResult, probeLinuxProcPidSync, readLinuxProcStartTimeSync } from "./linux-proc";
|
|
18
18
|
|
|
19
19
|
/** SHA-256 of the empty payload; the constant identity of every verified-empty receipt. */
|
|
20
20
|
const EMPTY_FILE_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
|
@@ -122,6 +122,10 @@ function lockRetryExhausted(budget: LockRetryBudget): boolean {
|
|
|
122
122
|
return lockRetryRemainingMs(budget) <= 0;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
function lockRetryTotalWaitExhausted(budget: LockRetryBudget): boolean {
|
|
126
|
+
return Math.max(0, performance.now() - budget.firstAttemptAt) >= LOCK_ACQUIRE_MAX_WAIT_MS;
|
|
127
|
+
}
|
|
128
|
+
|
|
125
129
|
async function waitForLockRetry(budget: LockRetryBudget): Promise<boolean> {
|
|
126
130
|
budget.attempts++;
|
|
127
131
|
const remainingMs = lockRetryRemainingMs(budget);
|
|
@@ -501,6 +505,13 @@ export const SessionStateLockTestHooks: {
|
|
|
501
505
|
* configuration.
|
|
502
506
|
*/
|
|
503
507
|
probeProcessSignal?: (pid: number) => void;
|
|
508
|
+
/**
|
|
509
|
+
* @internal How an owner pid's `/proc` entry is read. Whether a killed pid is still an
|
|
510
|
+
* unreaped zombie depends on when its parent happens to reap it, so it cannot be
|
|
511
|
+
* arranged deterministically from a test. Production code never sets it, and it is
|
|
512
|
+
* never serialized or exposed as runtime configuration.
|
|
513
|
+
*/
|
|
514
|
+
probeLinuxProcPid?: (pid: number) => LinuxProcPidProbeResult;
|
|
504
515
|
/**
|
|
505
516
|
* @internal Runs inside a NEW owner record's write, after the exclusive create has
|
|
506
517
|
* taken the pathname and before the record's bytes land. Throwing from it fails that
|
|
@@ -560,6 +571,33 @@ export type SessionStateLockUnavailableReason =
|
|
|
560
571
|
| "transition_claim_timeout"
|
|
561
572
|
| "unsafe_lock_path_type";
|
|
562
573
|
|
|
574
|
+
/**
|
|
575
|
+
* Why a stale-transition reclaim refused, as a bounded classifier.
|
|
576
|
+
*
|
|
577
|
+
* `transition_claim_timeout` collapsed several structurally different refusals into
|
|
578
|
+
* one message, so an operator could not tell a foreign-host tombstone from a
|
|
579
|
+
* live owner from a directory an external process was stamping — the gap #5606
|
|
580
|
+
* asked for. Every member is a fixed enum: never a path, token, host id, pid or
|
|
581
|
+
* raw errno, because this rides on an error message users paste into reports.
|
|
582
|
+
*/
|
|
583
|
+
export type TransitionReclaimRefusal =
|
|
584
|
+
/** The claim vanished, or its owner sidecar was absent/unparseable/invalid. */
|
|
585
|
+
| "claim_or_owner_unreadable"
|
|
586
|
+
/** A released tombstone carrying no `owner_host_id` at all. */
|
|
587
|
+
| "released_owner_unprovenanced"
|
|
588
|
+
/** A released tombstone belonging to a different installation. */
|
|
589
|
+
| "released_owner_foreign_host"
|
|
590
|
+
/** A released tombstone still inside `RELEASED_TRANSITION_GRACE_MS`. */
|
|
591
|
+
| "released_owner_within_grace"
|
|
592
|
+
/** The owner is alive, or its liveness could not be disproven. */
|
|
593
|
+
| "owner_live_or_unverifiable"
|
|
594
|
+
/** The claim changed between inspection and capture — an external writer. */
|
|
595
|
+
| "claim_generation_changed"
|
|
596
|
+
/** The claim was not empty when captured, so it is not ours to remove. */
|
|
597
|
+
| "claim_not_empty"
|
|
598
|
+
/** The claim's canonical path or directory snapshot could not be taken. */
|
|
599
|
+
| "claim_capture_failed";
|
|
600
|
+
|
|
563
601
|
interface SessionStateLockUnavailableDetails {
|
|
564
602
|
lockPath: string;
|
|
565
603
|
reason: SessionStateLockUnavailableReason;
|
|
@@ -750,6 +788,28 @@ function lockUnavailable(
|
|
|
750
788
|
});
|
|
751
789
|
}
|
|
752
790
|
|
|
791
|
+
/**
|
|
792
|
+
* `transition_claim_timeout` with the reclaim refusal that kept repeating.
|
|
793
|
+
*
|
|
794
|
+
* The bare classifier could not distinguish several structurally different
|
|
795
|
+
* refusals, so an operator had no way to tell which action would help (#5606).
|
|
796
|
+
* The suffix is a fixed enum member and nothing else — this string reaches user
|
|
797
|
+
* reports, so it must never carry a path, token, host id, pid or errno.
|
|
798
|
+
*/
|
|
799
|
+
function transitionClaimTimeout(
|
|
800
|
+
transitionDir: string,
|
|
801
|
+
budget: LockRetryBudget,
|
|
802
|
+
cause: unknown,
|
|
803
|
+
refusal: TransitionReclaimRefusal | undefined,
|
|
804
|
+
): SessionStateLockUnavailableError {
|
|
805
|
+
const error = lockUnavailable(transitionDir, "transition_claim_timeout", budget, cause);
|
|
806
|
+
if (refusal === undefined) return error;
|
|
807
|
+
return Object.assign(error, {
|
|
808
|
+
reclaimRefusal: refusal,
|
|
809
|
+
message: `${error.message.replace(/\.$/, "")} (reclaim refused: ${refusal}).`,
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
|
|
753
813
|
function lockDiagnostic(
|
|
754
814
|
error: unknown,
|
|
755
815
|
lockPath: string,
|
|
@@ -941,7 +1001,17 @@ function probeOwnerProcess(pid: number): OwnerProcessLiveness {
|
|
|
941
1001
|
try {
|
|
942
1002
|
const probe = SessionStateLockTestHooks.probeProcessSignal;
|
|
943
1003
|
if (probe) probe(pid);
|
|
944
|
-
else
|
|
1004
|
+
else {
|
|
1005
|
+
// Linux keeps a killed child as a zombie until its parent reaps it. `kill(pid, 0)`
|
|
1006
|
+
// still succeeds for that zombie even though it can no longer hold the lock, so
|
|
1007
|
+
// inspect `/proc` first and treat the terminal `Z` state as proof of death.
|
|
1008
|
+
if (process.platform === "linux") {
|
|
1009
|
+
const proc = (SessionStateLockTestHooks.probeLinuxProcPid ?? probeLinuxProcPidSync)(pid);
|
|
1010
|
+
if (proc.kind === "absent") return "dead";
|
|
1011
|
+
if (proc.kind === "live" && proc.state === "Z") return "dead";
|
|
1012
|
+
}
|
|
1013
|
+
process.kill(pid, 0);
|
|
1014
|
+
}
|
|
945
1015
|
return "alive";
|
|
946
1016
|
} catch (error) {
|
|
947
1017
|
const code = (error as NodeJS.ErrnoException).code;
|
|
@@ -1846,12 +1916,27 @@ async function releaseTransitionClaim(
|
|
|
1846
1916
|
}
|
|
1847
1917
|
}
|
|
1848
1918
|
|
|
1849
|
-
|
|
1919
|
+
/**
|
|
1920
|
+
* Outcome of one stale-transition reclaim attempt. `reclaimed` drives the
|
|
1921
|
+
* caller's retry accounting exactly as the previous boolean did; `refusal`
|
|
1922
|
+
* exists only so the eventual `transition_claim_timeout` can name which
|
|
1923
|
+
* precondition said no (#5606).
|
|
1924
|
+
*/
|
|
1925
|
+
interface TransitionReclaimOutcome {
|
|
1926
|
+
reclaimed: boolean;
|
|
1927
|
+
reclaimReason?: "released_handoff" | "dead_owner";
|
|
1928
|
+
refusal?: TransitionReclaimRefusal;
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
async function reclaimStaleTransitionClaim(
|
|
1932
|
+
transitionDir: string,
|
|
1933
|
+
quarantineName: string,
|
|
1934
|
+
): Promise<TransitionReclaimOutcome> {
|
|
1850
1935
|
let stat: fsSync.BigIntStats;
|
|
1851
1936
|
try {
|
|
1852
1937
|
stat = await fs.lstat(transitionDir, { bigint: true });
|
|
1853
1938
|
} catch {
|
|
1854
|
-
return false;
|
|
1939
|
+
return { reclaimed: false, refusal: "claim_or_owner_unreadable" };
|
|
1855
1940
|
}
|
|
1856
1941
|
// Regular-file claims belong to the superseded protocol. They retain the old
|
|
1857
1942
|
// exact-identity stale path; released PID-1 tombstones deliberately require
|
|
@@ -1865,41 +1950,46 @@ async function reclaimStaleTransitionClaim(transitionDir: string, quarantineName
|
|
|
1865
1950
|
},
|
|
1866
1951
|
quarantineName,
|
|
1867
1952
|
);
|
|
1868
|
-
return
|
|
1953
|
+
return {
|
|
1954
|
+
reclaimed: reclaimed === "owner_removed",
|
|
1955
|
+
...(reclaimed === "owner_removed" ? { reclaimReason: "dead_owner" as const } : {}),
|
|
1956
|
+
};
|
|
1869
1957
|
}
|
|
1870
1958
|
if (!stat.isDirectory()) throw new SessionStateLockUnavailableError();
|
|
1871
1959
|
const ownerSnapshot = await captureRegularLockOwner(`${transitionDir}.owner`);
|
|
1872
|
-
if (!ownerSnapshot) return false;
|
|
1960
|
+
if (!ownerSnapshot) return { reclaimed: false, refusal: "claim_or_owner_unreadable" };
|
|
1873
1961
|
let owner: unknown;
|
|
1874
1962
|
try {
|
|
1875
1963
|
owner = JSON.parse(ownerSnapshot.bytes);
|
|
1876
1964
|
} catch {
|
|
1877
|
-
return false;
|
|
1965
|
+
return { reclaimed: false, refusal: "claim_or_owner_unreadable" };
|
|
1878
1966
|
}
|
|
1879
|
-
if (!validLockOwner(owner)) return false;
|
|
1967
|
+
if (!validLockOwner(owner)) return { reclaimed: false, refusal: "claim_or_owner_unreadable" };
|
|
1880
1968
|
await SessionStateLockTestHooks.afterTransitionStaleInspection?.(transitionDir);
|
|
1881
1969
|
if (owner.released === true) {
|
|
1882
1970
|
// A released record is safe only when it is qualified by this installation.
|
|
1883
1971
|
// Never let a shared-volume tombstone, an absent identity, or a legacy foreign
|
|
1884
1972
|
// identity authorize removal of a claim this process cannot own.
|
|
1885
|
-
if (owner.owner_host_id === undefined) return false;
|
|
1973
|
+
if (owner.owner_host_id === undefined) return { reclaimed: false, refusal: "released_owner_unprovenanced" };
|
|
1886
1974
|
const currentHost = await currentOwnerHostId();
|
|
1887
1975
|
const legacyHost = await currentLegacyOwnerHostId();
|
|
1888
|
-
if (owner.owner_host_id !== currentHost && owner.owner_host_id !== legacyHost)
|
|
1889
|
-
|
|
1976
|
+
if (owner.owner_host_id !== currentHost && owner.owner_host_id !== legacyHost)
|
|
1977
|
+
return { reclaimed: false, refusal: "released_owner_foreign_host" };
|
|
1978
|
+
if (Date.now() - Number(ownerSnapshot.mtimeNs / 1_000_000n) < RELEASED_TRANSITION_GRACE_MS)
|
|
1979
|
+
return { reclaimed: false, refusal: "released_owner_within_grace" };
|
|
1890
1980
|
} else if (await lockOwnerIsAlive(owner)) {
|
|
1891
1981
|
// Only a host-qualified owner whose pid is PROVEN dead (ESRCH, or a live pid
|
|
1892
1982
|
// with a provably different incarnation) authorizes reclaim. The generation
|
|
1893
1983
|
// + exact-tree checks below then bind the removal to the very directory
|
|
1894
1984
|
// inspected here, on every platform; a claim stranded by a force-quit
|
|
1895
1985
|
// (`postmortem.quit`) would otherwise wall the session directory forever.
|
|
1896
|
-
return false;
|
|
1986
|
+
return { reclaimed: false, refusal: "owner_live_or_unverifiable" };
|
|
1897
1987
|
}
|
|
1898
1988
|
const generation = transitionGenerationFromStat(stat);
|
|
1899
1989
|
const nativePath = await canonicalOwnedTransitionPath(transitionDir).catch(() => null);
|
|
1900
|
-
if (!nativePath) return false;
|
|
1990
|
+
if (!nativePath) return { reclaimed: false, refusal: "claim_capture_failed" };
|
|
1901
1991
|
const captured = nativeSessionStateLock().snapshotDirectoryTree(nativePath);
|
|
1902
|
-
if (!captured.ok || !captured.snapshot) return false;
|
|
1992
|
+
if (!captured.ok || !captured.snapshot) return { reclaimed: false, refusal: "claim_capture_failed" };
|
|
1903
1993
|
const root = captured.snapshot.entries.find(entry => entry.relativePath === "");
|
|
1904
1994
|
if (
|
|
1905
1995
|
root?.kind !== "directory" ||
|
|
@@ -1909,12 +1999,15 @@ async function reclaimStaleTransitionClaim(transitionDir: string, quarantineName
|
|
|
1909
1999
|
root.mtimeNs !== String(generation.mtimeNs) ||
|
|
1910
2000
|
root.ctimeNs !== String(generation.ctimeNs)
|
|
1911
2001
|
)
|
|
1912
|
-
return false;
|
|
1913
|
-
if (captured.snapshot.entries.length !== 1) return false;
|
|
2002
|
+
return { reclaimed: false, refusal: "claim_generation_changed" };
|
|
2003
|
+
if (captured.snapshot.entries.length !== 1) return { reclaimed: false, refusal: "claim_not_empty" };
|
|
1914
2004
|
const removed = nativeSessionStateLock().exactRemoveDirectoryTree(nativePath, captured.snapshot);
|
|
1915
2005
|
if (removed.ok || removed.code === "not_found") {
|
|
1916
2006
|
exactUnlinkOwnerRecord(`${transitionDir}.owner`, ownerSnapshot, quarantineName);
|
|
1917
|
-
return
|
|
2007
|
+
return {
|
|
2008
|
+
reclaimed: removed.ok,
|
|
2009
|
+
...(removed.ok ? { reclaimReason: owner.released === true ? "released_handoff" : "dead_owner" } : {}),
|
|
2010
|
+
};
|
|
1918
2011
|
}
|
|
1919
2012
|
if (
|
|
1920
2013
|
removed.code === "cleanup_pending" &&
|
|
@@ -1931,7 +2024,7 @@ async function reclaimStaleTransitionClaim(transitionDir: string, quarantineName
|
|
|
1931
2024
|
// successor populated the detached path.
|
|
1932
2025
|
await removeTransitionDir(removed.detachedPath);
|
|
1933
2026
|
exactUnlinkOwnerRecord(`${transitionDir}.owner`, ownerSnapshot, quarantineName);
|
|
1934
|
-
return true;
|
|
2027
|
+
return { reclaimed: true, reclaimReason: owner.released === true ? "released_handoff" : "dead_owner" };
|
|
1935
2028
|
}
|
|
1936
2029
|
throw new SessionStateLockUnavailableError(
|
|
1937
2030
|
new Error(`Stale transition claim could not be reclaimed (${removed.code ?? "unknown"}).`),
|
|
@@ -2310,6 +2403,7 @@ async function runLockPathTransition<T>(
|
|
|
2310
2403
|
throw new SessionStateLockUnavailableError(new Error("Safe transition ownership is unsupported."));
|
|
2311
2404
|
const transitionDir = `${lockFile}${LOCK_TRANSITION_RESOURCE_SUFFIX}`;
|
|
2312
2405
|
const ownerFile = `${transitionDir}.owner`;
|
|
2406
|
+
let lastReclaimRefusal: TransitionReclaimRefusal | undefined;
|
|
2313
2407
|
|
|
2314
2408
|
for (;;) {
|
|
2315
2409
|
if (ownerAccessStrategy() === "unsupported" && fsSync.existsSync(transitionDir))
|
|
@@ -2332,18 +2426,37 @@ async function runLockPathTransition<T>(
|
|
|
2332
2426
|
// Claims are held only for pathname bookkeeping, so a changed claim identity is
|
|
2333
2427
|
// proof that contenders are cycling through rather than that one is wedged.
|
|
2334
2428
|
const observedClaim = await observedLockHolderIdentity(transitionDir);
|
|
2335
|
-
const
|
|
2336
|
-
creditLockHandoffProgress(budget, observedClaim, reclaimed);
|
|
2429
|
+
const outcome = await reclaimStaleTransitionClaim(transitionDir, quarantineName);
|
|
2430
|
+
creditLockHandoffProgress(budget, observedClaim, outcome.reclaimed);
|
|
2431
|
+
// Remember the LAST refusal: on a wedged claim every retry refuses for the
|
|
2432
|
+
// same reason, and that reason is the actionable one. Carrying it onto the
|
|
2433
|
+
// timeout is the whole point — `transition_claim_timeout` alone cannot tell
|
|
2434
|
+
// a foreign-host tombstone from a live owner from an external writer
|
|
2435
|
+
// restamping the claim directory (#5606).
|
|
2436
|
+
if (outcome.refusal !== undefined) lastReclaimRefusal = outcome.refusal;
|
|
2337
2437
|
// One safe reclaim may make progress, but a try-acquire never follows a
|
|
2338
2438
|
// succession of contenders. A later maintenance pass can take the freed claim.
|
|
2339
2439
|
if (budget.nonBlocking) throw SESSION_STATE_LOCK_BUSY;
|
|
2340
|
-
if (reclaimed) {
|
|
2341
|
-
if (lockRetryExhausted(budget))
|
|
2342
|
-
|
|
2440
|
+
if (outcome.reclaimed) {
|
|
2441
|
+
if (lockRetryExhausted(budget)) {
|
|
2442
|
+
// Only a released tombstone is the previous holder's explicit handoff.
|
|
2443
|
+
// Ordinary dead-owner reclaims remain bounded by the original deadline,
|
|
2444
|
+
// even though both reclaim paths unlink their owner sidecar.
|
|
2445
|
+
const ownerStillPresent = await fs.lstat(`${transitionDir}.owner`).then(
|
|
2446
|
+
() => true,
|
|
2447
|
+
error => (error as NodeJS.ErrnoException).code !== "ENOENT",
|
|
2448
|
+
);
|
|
2449
|
+
if (
|
|
2450
|
+
outcome.reclaimReason !== "released_handoff" ||
|
|
2451
|
+
ownerStillPresent ||
|
|
2452
|
+
lockRetryTotalWaitExhausted(budget)
|
|
2453
|
+
)
|
|
2454
|
+
throw transitionClaimTimeout(transitionDir, budget, error, lastReclaimRefusal);
|
|
2455
|
+
}
|
|
2343
2456
|
continue;
|
|
2344
2457
|
}
|
|
2345
2458
|
if (!(await waitForLockRetry(budget)))
|
|
2346
|
-
throw
|
|
2459
|
+
throw transitionClaimTimeout(transitionDir, budget, error, lastReclaimRefusal);
|
|
2347
2460
|
continue;
|
|
2348
2461
|
}
|
|
2349
2462
|
const pendingSetup: PendingTransitionRelease = {
|
|
@@ -2753,6 +2866,34 @@ async function observedLockHolderIdentity(lockPath: string): Promise<string | un
|
|
|
2753
2866
|
return `d:${stat.dev}:${stat.ino}:${stat.ctimeNs}`;
|
|
2754
2867
|
}
|
|
2755
2868
|
|
|
2869
|
+
/**
|
|
2870
|
+
* Classify one stale-transition reclaim attempt.
|
|
2871
|
+
*
|
|
2872
|
+
* @internal exported as the seam the refusal classifier is tested through. Driving
|
|
2873
|
+
* a real `transition_claim_timeout` would mean waiting out `LOCK_ACQUIRE_MAX_WAIT_MS`
|
|
2874
|
+
* (60s) per case, and `tryWithSessionStateFileLock` throws BUSY before the timeout
|
|
2875
|
+
* is ever built, so the per-branch reasons have no other reachable surface (#5606).
|
|
2876
|
+
*/
|
|
2877
|
+
export async function classifyTransitionReclaimForTests(
|
|
2878
|
+
transitionDir: string,
|
|
2879
|
+
quarantineName: string,
|
|
2880
|
+
): Promise<TransitionReclaimOutcome> {
|
|
2881
|
+
return reclaimStaleTransitionClaim(transitionDir, quarantineName);
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2884
|
+
/**
|
|
2885
|
+
* Build the timeout error exactly as the transition loop does.
|
|
2886
|
+
*
|
|
2887
|
+
* @internal exported so the message contract — a bounded enum suffix and nothing
|
|
2888
|
+
* else — is pinned without a 60s wait (#5606).
|
|
2889
|
+
*/
|
|
2890
|
+
export function transitionClaimTimeoutForTests(
|
|
2891
|
+
transitionDir: string,
|
|
2892
|
+
refusal: TransitionReclaimRefusal | undefined,
|
|
2893
|
+
): SessionStateLockUnavailableError {
|
|
2894
|
+
return transitionClaimTimeout(transitionDir, lockRetryBudget(), new Error("EEXIST"), refusal);
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2756
2897
|
/**
|
|
2757
2898
|
* Decide what actually occupies the lock path, without following it.
|
|
2758
2899
|
*
|
|
@@ -382,6 +382,26 @@ function repositoryIdentity(binding: RepositoryBinding): Record<string, unknown>
|
|
|
382
382
|
};
|
|
383
383
|
}
|
|
384
384
|
|
|
385
|
+
/**
|
|
386
|
+
* Self-transfer guards are narrower than repository identity guards: linked
|
|
387
|
+
* worktrees share a common dir but carry independent `.gjc` state, so they are
|
|
388
|
+
* valid succession peers. `captureRepositoryBinding` records a realpath for
|
|
389
|
+
* active roots, but persisted plan bindings are operator-controlled and may
|
|
390
|
+
* contain a symlink alias.
|
|
391
|
+
* Resolve both roots through the filesystem before comparing them; a failed
|
|
392
|
+
* resolution is a refusal, never a fallback to lexical path comparison.
|
|
393
|
+
*/
|
|
394
|
+
async function canonicalWorktreeRoot(binding: RepositoryBinding, side: "source" | "target"): Promise<string> {
|
|
395
|
+
try {
|
|
396
|
+
return await fs.realpath(binding.worktreeRoot);
|
|
397
|
+
} catch {
|
|
398
|
+
throw new UltragoalSuccessionError(
|
|
399
|
+
"unsafe_target",
|
|
400
|
+
`Cannot resolve the ${side} worktree root physically (${binding.worktreeRoot}); refusing succession.`,
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
385
405
|
function successionOperationId(input: {
|
|
386
406
|
sourceRepository: RepositoryBinding;
|
|
387
407
|
sourceSessionId: string;
|
|
@@ -781,11 +801,15 @@ async function resolveTargetRepositoryBinding(
|
|
|
781
801
|
throw new UltragoalSuccessionError("unsafe_target", `--target-repo is not a directory: ${raw}`);
|
|
782
802
|
}
|
|
783
803
|
const target = publicRepositoryBinding(await captureRepositoryBinding(resolved, { displayPath: resolved }));
|
|
784
|
-
|
|
804
|
+
const [targetRoot, sourceRoot] = await Promise.all([
|
|
805
|
+
canonicalWorktreeRoot(target, "target"),
|
|
806
|
+
canonicalWorktreeRoot(sourceBinding, "source"),
|
|
807
|
+
]);
|
|
808
|
+
if (targetRoot === sourceRoot) {
|
|
785
809
|
throw new UltragoalSuccessionError(
|
|
786
810
|
"unsafe_target",
|
|
787
811
|
`--target-repo resolves to the source repository itself (${target.worktreeRoot}). ` +
|
|
788
|
-
"Succession
|
|
812
|
+
"Succession requires a distinct worktree or repository; it is not a rebind of the original run.",
|
|
789
813
|
);
|
|
790
814
|
}
|
|
791
815
|
return target;
|
|
@@ -1277,7 +1301,11 @@ export async function adoptUltragoalSuccession(
|
|
|
1277
1301
|
"adoption is pinned to the exact worktree the offer named.",
|
|
1278
1302
|
);
|
|
1279
1303
|
}
|
|
1280
|
-
|
|
1304
|
+
const [targetRoot, sourceRoot] = await Promise.all([
|
|
1305
|
+
canonicalWorktreeRoot(targetActual, "target"),
|
|
1306
|
+
canonicalWorktreeRoot(sourceRepository, "source"),
|
|
1307
|
+
]);
|
|
1308
|
+
if (targetRoot === sourceRoot) {
|
|
1281
1309
|
throw new UltragoalSuccessionError(
|
|
1282
1310
|
"unsafe_target",
|
|
1283
1311
|
"Refusing to adopt a succession offer into its own source repository.",
|