@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
|
@@ -57,10 +57,12 @@ export function selectReapableSessions(
|
|
|
57
57
|
/**
|
|
58
58
|
* Only a stale/absent endpoint makes a session permanently unreapable, so it is
|
|
59
59
|
* the sole failure that advances the force-eviction counter. Every other reap
|
|
60
|
-
* failure (close_failed, broker unavailability,
|
|
61
|
-
* and must keep retrying without ever escalating
|
|
62
|
-
* wires `reapSession` to throw `new Error(reason)`,
|
|
63
|
-
*
|
|
60
|
+
* failure (a close_failed without a stale endpoint, broker unavailability,
|
|
61
|
+
* filesystem errors) is transient and must keep retrying without ever escalating
|
|
62
|
+
* to force eviction. The server wires `reapSession` to throw `new Error(reason)`,
|
|
63
|
+
* except that a close the broker answered with endpoint_stale (close_failed with
|
|
64
|
+
* that detail) is thrown as endpoint_stale, so the reason code is the error
|
|
65
|
+
* message here.
|
|
64
66
|
*/
|
|
65
67
|
function isEndpointStaleReapError(err: unknown): boolean {
|
|
66
68
|
return (err instanceof Error ? err.message : String(err)) === "endpoint_stale";
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type CrashFingerprint,
|
|
18
18
|
type CrashProvenance,
|
|
19
19
|
computeCrashFingerprint,
|
|
20
|
+
computeHandledErrorFingerprint,
|
|
20
21
|
parseCrashRecordMarker,
|
|
21
22
|
} from "@gajae-code/utils";
|
|
22
23
|
|
|
@@ -60,8 +61,11 @@ function findBoundFingerprint(
|
|
|
60
61
|
// suffix omission against the marker rather than trusting its syntax.
|
|
61
62
|
if (remaining.length > 0) stackCandidates.push(remaining.slice(0, -1).join("\n").trimEnd());
|
|
62
63
|
for (const stack of stackCandidates) {
|
|
63
|
-
const
|
|
64
|
-
|
|
64
|
+
const input = { name, message, stack };
|
|
65
|
+
const fatalFingerprint = computeCrashFingerprint(input);
|
|
66
|
+
if (fatalFingerprint.fingerprint === markerFingerprint) return fatalFingerprint;
|
|
67
|
+
const handledFingerprint = computeHandledErrorFingerprint(input);
|
|
68
|
+
if (handledFingerprint.fingerprint === markerFingerprint) return handledFingerprint;
|
|
65
69
|
}
|
|
66
70
|
}
|
|
67
71
|
}
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* (automation); the default human output stays a concise per-daemon result.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import { replaceTabs, truncateToWidth } from "@gajae-code/tui";
|
|
16
|
+
import { sanitizeDisplayLine } from "@gajae-code/utils";
|
|
15
17
|
import type { DaemonAction, DaemonOperationResult, DaemonRecovery, DaemonStatus } from "./control-types";
|
|
16
18
|
|
|
17
19
|
/** Canonical daemon actions accepted as the leading verb. */
|
|
@@ -39,8 +41,14 @@ export function resolveDaemonAction(token: string | undefined): DaemonAction | u
|
|
|
39
41
|
return DAEMON_ACTION_ALIASES[token];
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
/**
|
|
43
|
-
export const DAEMON_EXIT = { ok: 0, failure: 1 } as const;
|
|
44
|
+
/** Public input failures exit 2; operational failures retain exit 1. */
|
|
45
|
+
export const DAEMON_EXIT = { ok: 0, failure: 1, usage: 2 } as const;
|
|
46
|
+
|
|
47
|
+
/** A refused result may follow a successful stop or spawn; snapshots and prose
|
|
48
|
+
* do not prove that no effect occurred. Only completed results prove applied. */
|
|
49
|
+
export function daemonOperationOutcome(result: DaemonOperationResult): "applied" | "unknown" {
|
|
50
|
+
return result.ok ? "applied" : "unknown";
|
|
51
|
+
}
|
|
44
52
|
|
|
45
53
|
/** Human-facing headline when a spawn/reload is refused by a live foreign identity. */
|
|
46
54
|
export const OWNERSHIP_MISMATCH_MESSAGE =
|
|
@@ -59,6 +67,10 @@ export function ownershipMismatchRecovery(): DaemonRecovery {
|
|
|
59
67
|
};
|
|
60
68
|
}
|
|
61
69
|
|
|
70
|
+
function safeDaemonText(value: string): string {
|
|
71
|
+
return truncateToWidth(replaceTabs(sanitizeDisplayLine(value).replaceAll(/[\u2028\u2029]/gu, " ")), 512);
|
|
72
|
+
}
|
|
73
|
+
|
|
62
74
|
function timestamp(ms: number | undefined): string | undefined {
|
|
63
75
|
if (ms === undefined) return undefined;
|
|
64
76
|
const date = new Date(ms);
|
|
@@ -73,31 +85,31 @@ export function formatDaemonStatus(status: DaemonStatus, opts: { verbose?: boole
|
|
|
73
85
|
const lines: string[] = [];
|
|
74
86
|
if (!status.configured) {
|
|
75
87
|
lines.push(`${status.kind}: not configured`);
|
|
76
|
-
if (status.runtime.warning) lines.push(` warning: ${status.runtime.warning}`);
|
|
88
|
+
if (status.runtime.warning) lines.push(` warning: ${safeDaemonText(status.runtime.warning)}`);
|
|
77
89
|
return lines.join("\n");
|
|
78
90
|
}
|
|
79
91
|
|
|
80
92
|
const meta: string[] = [];
|
|
81
93
|
if (status.pid !== undefined) meta.push(`pid ${status.pid}`);
|
|
82
|
-
if (status.ownerId) meta.push(`owner ${status.ownerId}`);
|
|
94
|
+
if (status.ownerId) meta.push(`owner ${safeDaemonText(status.ownerId)}`);
|
|
83
95
|
const rootCount = status.rootCount ?? status.roots?.length ?? 0;
|
|
84
96
|
if (rootCount > 0) meta.push(`${rootCount} root${rootCount === 1 ? "" : "s"}`);
|
|
85
97
|
|
|
86
98
|
let head = `${status.kind}: ${status.health}`;
|
|
87
99
|
if (meta.length > 0) head += ` (${meta.join(", ")})`;
|
|
88
|
-
if (status.detail) head += ` — ${status.detail}`;
|
|
100
|
+
if (status.detail) head += ` — ${safeDaemonText(status.detail)}`;
|
|
89
101
|
lines.push(head);
|
|
90
|
-
if (status.runtime.warning) lines.push(` warning: ${status.runtime.warning}`);
|
|
102
|
+
if (status.runtime.warning) lines.push(` warning: ${safeDaemonText(status.runtime.warning)}`);
|
|
91
103
|
|
|
92
104
|
if (opts.verbose) {
|
|
93
|
-
lines.push(` runtime: ${status.runtime.mode} (${status.runtime.execPath})`);
|
|
105
|
+
lines.push(` runtime: ${status.runtime.mode} (${safeDaemonText(status.runtime.execPath)})`);
|
|
94
106
|
const started = timestamp(status.startedAt);
|
|
95
107
|
if (started) lines.push(` started: ${started}`);
|
|
96
108
|
const heartbeat = timestamp(status.heartbeatAt);
|
|
97
109
|
if (heartbeat) lines.push(` heartbeat: ${heartbeat}`);
|
|
98
110
|
const roots = status.roots ?? [];
|
|
99
111
|
lines.push(` roots: ${rootCount}`);
|
|
100
|
-
for (const root of roots) lines.push(` - ${root}`);
|
|
112
|
+
for (const root of roots) lines.push(` - ${safeDaemonText(root)}`);
|
|
101
113
|
}
|
|
102
114
|
|
|
103
115
|
return lines.join("\n");
|
|
@@ -108,13 +120,15 @@ export function formatDaemonStatus(status: DaemonStatus, opts: { verbose?: boole
|
|
|
108
120
|
* <message>` head line, then any warnings, then actionable recovery steps.
|
|
109
121
|
*/
|
|
110
122
|
export function formatDaemonResult(result: DaemonOperationResult): string {
|
|
111
|
-
const lines = [
|
|
112
|
-
|
|
123
|
+
const lines = [
|
|
124
|
+
`${result.kind} ${result.action}: ${result.ok ? "ok" : "failed"} — ${safeDaemonText(result.message)}`,
|
|
125
|
+
];
|
|
126
|
+
for (const warning of result.warnings) lines.push(` warning: ${safeDaemonText(warning)}`);
|
|
113
127
|
if (result.recovery) {
|
|
114
|
-
lines.push(` ${result.recovery.summary}`);
|
|
128
|
+
lines.push(` ${safeDaemonText(result.recovery.summary)}`);
|
|
115
129
|
lines.push(" to recover:");
|
|
116
130
|
result.recovery.steps.forEach((step, i) => {
|
|
117
|
-
lines.push(` ${i + 1}. ${step}`);
|
|
131
|
+
lines.push(` ${i + 1}. ${safeDaemonText(step)}`);
|
|
118
132
|
});
|
|
119
133
|
}
|
|
120
134
|
return lines.join("\n");
|
|
@@ -458,7 +458,7 @@ Because unselected goals stay schedulable, ordinary source progress moves whole-
|
|
|
458
458
|
|
|
459
459
|
`adopt` runs in the target repository and starts a genuinely fresh run: new pending goals, fresh timestamps, the target repository's own binding, and no inherited receipts, quality gates, validation batches, steering or activated objective. The source brief and every selected objective are carried verbatim, together with the unresolved obligations recorded against those goals — requirements are carried forward in full, never summarized down. Dependency groups travel as explicit obligations with source-to-target id mapping: the source's batch metadata and receipts are not inherited, but the requirement to validate those goals together is.
|
|
460
460
|
|
|
461
|
-
Selection and authorization must be explicit. Only unfinished goals (`pending`, `failed`, `blocked`, `review_blocked`) may be handed off; the source run must be quiescent with no `active` goal; there is no implicit or wildcard selection; and a selection that takes only part of a validation batch, or splits a review blocker from the goal it still blocks, is rejected rather than silently dropping that obligation. Adoption fails closed on changed offered work, an altered or unfenced offer, an offer path that escapes the source worktree, a target the offer does not name, a linked worktree
|
|
461
|
+
Selection and authorization must be explicit. Only unfinished goals (`pending`, `failed`, `blocked`, `review_blocked`) may be handed off; the source run must be quiescent with no `active` goal; there is no implicit or wildcard selection; and a selection that takes only part of a validation batch, or splits a review blocker from the goal it still blocks, is rejected rather than silently dropping that obligation. Adoption fails closed on changed offered work, an altered or unfenced offer, an offer path that escapes the source worktree, a target the offer does not name, a different linked worktree from the exact named target, an occupied target session, and any duplicate or simultaneous adoption anywhere in the target repository. A linked worktree is a valid target when the offer names that exact worktree; the source and target may therefore be sibling worktrees, but aliases of the same source worktree remain self-transfers and are rejected using physical path identity, and an unavailable bound worktree root is refused rather than compared lexically. A retry reconciles the exact recorded operation or fails closed — a matching operation id is never authority to overwrite target work it did not write, and a published plan that has disappeared is never reconstructed.
|
|
462
462
|
|
|
463
463
|
Until publication completes, the target refuses to execute: a visible `goals.json` is not admission evidence, the adoption record is. `start`/`complete-goals` and `checkpoint` reject while an adoption remains pending.
|
|
464
464
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isCompiledBinary, logger, Snowflake } from "@gajae-code/utils";
|
|
2
|
+
import { isDesignedError } from "@gajae-code/utils/error-classification";
|
|
2
3
|
import { registerResourceOwner } from "../../runtime/process-lifecycle";
|
|
3
4
|
import type { ToolSession } from "../../tools";
|
|
4
5
|
import { ToolAbortError, ToolError } from "../../tools/tool-errors";
|
|
@@ -460,13 +461,11 @@ function reasonToError(reason: unknown, fallback: string): Error {
|
|
|
460
461
|
}
|
|
461
462
|
|
|
462
463
|
function errorFromPayload(payload: RunErrorPayload): Error {
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
const ctor = payload.isToolError ? ToolError : Error;
|
|
469
|
-
const error = new ctor(payload.message);
|
|
464
|
+
const error = payload.isDesigned
|
|
465
|
+
? payload.isAbort
|
|
466
|
+
? new ToolAbortError(payload.message || "Execution aborted")
|
|
467
|
+
: new ToolError(payload.message)
|
|
468
|
+
: new Error(payload.message);
|
|
470
469
|
if (payload.name) error.name = payload.name;
|
|
471
470
|
if (payload.stack) error.stack = payload.stack;
|
|
472
471
|
return error;
|
|
@@ -479,7 +478,8 @@ function toErrorPayload(error: unknown): RunErrorPayload {
|
|
|
479
478
|
message: error.message,
|
|
480
479
|
stack: error.stack,
|
|
481
480
|
isAbort: error.name === "AbortError" || error.name === "ToolAbortError",
|
|
482
|
-
isToolError: error instanceof ToolError
|
|
481
|
+
isToolError: error instanceof ToolError,
|
|
482
|
+
isDesigned: isDesignedError(error),
|
|
483
483
|
};
|
|
484
484
|
}
|
|
485
485
|
return { message: String(error) };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isDesignedError } from "@gajae-code/utils/error-classification";
|
|
2
|
+
import { ToolAbortError, ToolError } from "../../tools/tool-errors";
|
|
2
3
|
import { JsRuntime, type RuntimeHooks } from "./shared/runtime";
|
|
3
4
|
import type { RunErrorPayload, SessionSnapshot, ToolReply, Transport, WorkerInbound } from "./worker-protocol";
|
|
4
5
|
|
|
@@ -24,15 +25,19 @@ function errorPayload(error: unknown): RunErrorPayload {
|
|
|
24
25
|
message: error.message,
|
|
25
26
|
stack: error.stack,
|
|
26
27
|
isAbort: error.name === "AbortError" || error.name === "ToolAbortError",
|
|
27
|
-
isToolError: error
|
|
28
|
+
isToolError: error instanceof ToolError,
|
|
29
|
+
isDesigned: isDesignedError(error),
|
|
28
30
|
};
|
|
29
31
|
}
|
|
30
32
|
return { message: String(error) };
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
function errorFromPayload(payload: RunErrorPayload): Error {
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
+
const error = payload.isDesigned
|
|
37
|
+
? payload.isAbort
|
|
38
|
+
? new ToolAbortError(payload.message)
|
|
39
|
+
: new ToolError(payload.message)
|
|
40
|
+
: new Error(payload.message);
|
|
36
41
|
if (payload.name) error.name = payload.name;
|
|
37
42
|
if (payload.stack) error.stack = payload.stack;
|
|
38
43
|
return error;
|
|
@@ -13,6 +13,8 @@ export interface RunErrorPayload {
|
|
|
13
13
|
stack?: string;
|
|
14
14
|
isAbort?: boolean;
|
|
15
15
|
isToolError?: boolean;
|
|
16
|
+
/** Set only when the originating error carried the trusted designed marker. */
|
|
17
|
+
isDesigned?: boolean;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export type ToolReply = { ok: true; value: unknown } | { ok: false; error: RunErrorPayload };
|
|
@@ -192,6 +192,7 @@ type RunnerEmitResult<TEvent extends RunnerEmitEvent> = TEvent extends { type: "
|
|
|
192
192
|
type Handler = Extension["handlers"] extends Map<string, Array<infer T>> ? T : never;
|
|
193
193
|
type IndexedHandler = { ext: Extension; handler: Handler; registrationOrder: number };
|
|
194
194
|
type IndexedFunctionHook = IndexedHandler & { registration: FunctionHookRegistration };
|
|
195
|
+
type CommandAliasTarget = { extensionPath: string; commandName: string };
|
|
195
196
|
|
|
196
197
|
export type FunctionHookDispatchResult<TEvent extends ExtensionEvent> =
|
|
197
198
|
| { action: "continue"; event: TEvent; transformed?: boolean }
|
|
@@ -272,6 +273,7 @@ export class ExtensionRunner {
|
|
|
272
273
|
#getCredentialSessionId: () => string = () => "";
|
|
273
274
|
#isIdleFn: () => boolean = () => true;
|
|
274
275
|
#getActivePromptHandleFn: () => string | undefined = () => undefined;
|
|
276
|
+
#getSessionWorkLeaseFn: ExtensionContextActions["getSessionWorkLease"] = undefined;
|
|
275
277
|
#waitForIdleFn: () => Promise<void> = async () => {};
|
|
276
278
|
#abortFn: () => void | Promise<void> = () => {};
|
|
277
279
|
#abortPromptAndWaitFn: NonNullable<ExtensionContextActions["abortPromptAndWait"]> = async () => {
|
|
@@ -328,6 +330,9 @@ export class ExtensionRunner {
|
|
|
328
330
|
#reloadHandler: () => Promise<void> = async () => {};
|
|
329
331
|
#shutdownHandler: ShutdownHandler = () => {};
|
|
330
332
|
#commandDiagnostics: Array<{ type: string; message: string; path: string }> = [];
|
|
333
|
+
#commandAliases = new Map<string, RegisteredCommand>();
|
|
334
|
+
#commandAliasAssignments = new Map<string, string>();
|
|
335
|
+
#commandAliasTargets = new Map<string, CommandAliasTarget>();
|
|
331
336
|
#initialized = false;
|
|
332
337
|
/**
|
|
333
338
|
* Buffer for `credential_disabled` events received via {@link emitCredentialDisabled}
|
|
@@ -424,6 +429,7 @@ export class ExtensionRunner {
|
|
|
424
429
|
this.#getCredentialSessionId = contextActions.getCredentialSessionId ?? (() => "");
|
|
425
430
|
this.#isIdleFn = contextActions.isIdle;
|
|
426
431
|
this.#getActivePromptHandleFn = contextActions.getActivePromptHandle ?? (() => undefined);
|
|
432
|
+
this.#getSessionWorkLeaseFn = contextActions.getSessionWorkLease;
|
|
427
433
|
this.#abortFn = contextActions.abort;
|
|
428
434
|
this.#abortPromptAndWaitFn =
|
|
429
435
|
contextActions.abortPromptAndWait ??
|
|
@@ -701,16 +707,49 @@ export class ExtensionRunner {
|
|
|
701
707
|
|
|
702
708
|
getRegisteredCommands(reserved?: Set<string>): RegisteredCommand[] {
|
|
703
709
|
this.#commandDiagnostics = [];
|
|
710
|
+
this.#commandAliases.clear();
|
|
704
711
|
|
|
705
712
|
const commands = new Map<string, RegisteredCommand>();
|
|
706
713
|
for (const ext of this.extensions) {
|
|
707
714
|
for (const command of ext.commands.values()) {
|
|
708
715
|
if (reserved?.has(command.name)) {
|
|
709
|
-
const
|
|
710
|
-
this.#
|
|
711
|
-
|
|
712
|
-
|
|
716
|
+
const assignmentKey = `${ext.path}\u0000${command.name}`;
|
|
717
|
+
let alias = this.#commandAliasAssignments.get(assignmentKey);
|
|
718
|
+
const aliasTarget = alias ? this.#commandAliasTargets.get(alias) : undefined;
|
|
719
|
+
const aliasOwnedByCommand =
|
|
720
|
+
aliasTarget?.extensionPath === ext.path && aliasTarget.commandName === command.name;
|
|
721
|
+
if (
|
|
722
|
+
!alias ||
|
|
723
|
+
reserved.has(alias) ||
|
|
724
|
+
commands.has(alias) ||
|
|
725
|
+
(aliasTarget !== undefined && !aliasOwnedByCommand)
|
|
726
|
+
) {
|
|
727
|
+
let candidate = alias ?? `extension:${command.name}`;
|
|
728
|
+
while (
|
|
729
|
+
reserved.has(candidate) ||
|
|
730
|
+
commands.has(candidate) ||
|
|
731
|
+
this.#commandAliases.has(candidate) ||
|
|
732
|
+
(this.#commandAliasTargets.has(candidate) &&
|
|
733
|
+
(this.#commandAliasTargets.get(candidate)?.extensionPath !== ext.path ||
|
|
734
|
+
this.#commandAliasTargets.get(candidate)?.commandName !== command.name))
|
|
735
|
+
)
|
|
736
|
+
candidate = `extension:${candidate}`;
|
|
737
|
+
if (
|
|
738
|
+
alias &&
|
|
739
|
+
this.#commandAliasTargets.get(alias)?.extensionPath === ext.path &&
|
|
740
|
+
this.#commandAliasTargets.get(alias)?.commandName === command.name
|
|
741
|
+
) {
|
|
742
|
+
this.#commandAliasTargets.delete(alias);
|
|
743
|
+
}
|
|
744
|
+
alias = candidate;
|
|
745
|
+
this.#commandAliasAssignments.set(assignmentKey, alias);
|
|
713
746
|
}
|
|
747
|
+
this.#commandAliasTargets.set(alias, { extensionPath: ext.path, commandName: command.name });
|
|
748
|
+
const namespaced = { ...command, name: alias };
|
|
749
|
+
this.#commandAliases.set(alias, namespaced);
|
|
750
|
+
commands.set(alias, namespaced);
|
|
751
|
+
const message = `Extension command '${command.name}' from ${ext.path} was renamed to '${alias}' to avoid a built-in command collision.`;
|
|
752
|
+
this.#commandDiagnostics.push({ type: "info", message, path: ext.path });
|
|
714
753
|
continue;
|
|
715
754
|
}
|
|
716
755
|
|
|
@@ -725,6 +764,14 @@ export class ExtensionRunner {
|
|
|
725
764
|
}
|
|
726
765
|
|
|
727
766
|
getCommand(name: string): RegisteredCommand | undefined {
|
|
767
|
+
const aliased = this.#commandAliases.get(name);
|
|
768
|
+
if (aliased) return aliased;
|
|
769
|
+
const aliasTarget = this.#commandAliasTargets.get(name);
|
|
770
|
+
if (aliasTarget) {
|
|
771
|
+
const extension = this.extensions.find(ext => ext.path === aliasTarget.extensionPath);
|
|
772
|
+
const command = extension?.commands.get(aliasTarget.commandName);
|
|
773
|
+
if (command) return { ...command, name };
|
|
774
|
+
}
|
|
728
775
|
for (let index = this.extensions.length - 1; index >= 0; index -= 1) {
|
|
729
776
|
const command = this.extensions[index]?.commands.get(name);
|
|
730
777
|
if (command) {
|
|
@@ -754,6 +801,7 @@ export class ExtensionRunner {
|
|
|
754
801
|
return getModel();
|
|
755
802
|
},
|
|
756
803
|
getActivePromptHandle: () => this.#getActivePromptHandleFn(),
|
|
804
|
+
getSessionWorkLease: () => this.#getSessionWorkLeaseFn?.(),
|
|
757
805
|
isIdle: () => this.#isIdleFn(),
|
|
758
806
|
abort: () => this.#abortFn(),
|
|
759
807
|
abortPromptAndWait: (handle, options) => this.#abortPromptAndWaitFn(handle, options),
|
|
@@ -58,6 +58,7 @@ import type {
|
|
|
58
58
|
} from "../../session/client-bridge";
|
|
59
59
|
import type { CustomMessage } from "../../session/messages";
|
|
60
60
|
import type { ReadonlySessionManager, SessionManager } from "../../session/session-manager";
|
|
61
|
+
import type { SessionWorkLease } from "../../session/session-work-lease";
|
|
61
62
|
import type {
|
|
62
63
|
BashToolDetails,
|
|
63
64
|
BashToolInput,
|
|
@@ -482,6 +483,8 @@ export interface ExtensionContext {
|
|
|
482
483
|
isIdle(): boolean;
|
|
483
484
|
/** Stable resource ownership identifier for the active prompt run. */
|
|
484
485
|
getActivePromptHandle(): string | undefined;
|
|
486
|
+
/** The session's authoritative host-liveness lease. */
|
|
487
|
+
getSessionWorkLease?(): SessionWorkLease | undefined;
|
|
485
488
|
/** Abort the current agent operation */
|
|
486
489
|
abort(): void | Promise<void>;
|
|
487
490
|
/** Abort and prove whether resources for a specific prompt settled. */
|
|
@@ -1569,6 +1572,11 @@ export type SendUserMessageHandler = (
|
|
|
1569
1572
|
* grants root abort ownership.
|
|
1570
1573
|
*/
|
|
1571
1574
|
onQueuedPromoted?: (promotion: { startsOwnRun?: boolean; removed?: boolean }) => void;
|
|
1575
|
+
/**
|
|
1576
|
+
* Fired synchronously when a plain prompt's dispatch resolves at queue
|
|
1577
|
+
* time instead of run end (diverted into an in-flight run's steering).
|
|
1578
|
+
*/
|
|
1579
|
+
onDispatchDisposition?: (promotion: { startsOwnRun: boolean }) => void;
|
|
1572
1580
|
preflightSignal?: AbortSignal;
|
|
1573
1581
|
/** Internal SDK correlation owner for an exact queued follow-up. */
|
|
1574
1582
|
},
|
|
@@ -1660,6 +1668,8 @@ export interface ExtensionContextActions {
|
|
|
1660
1668
|
isIdle: () => boolean;
|
|
1661
1669
|
/** Stable resource ownership identifier for the active prompt run. */
|
|
1662
1670
|
getActivePromptHandle?: () => string | undefined;
|
|
1671
|
+
/** The session's authoritative host-liveness lease. */
|
|
1672
|
+
getSessionWorkLease?: () => SessionWorkLease | undefined;
|
|
1663
1673
|
abort: () => void | Promise<void>;
|
|
1664
1674
|
abortPromptAndWait?: (handle: string, options: { graceMs: number }) => Promise<RunSettlementProof>;
|
|
1665
1675
|
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as os from "node:os";
|
|
3
4
|
import * as path from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
exactUnlink,
|
|
7
|
+
linkNoReplacePath,
|
|
8
|
+
type NativeExactFileIdentity,
|
|
9
|
+
type NativeNoReplaceResult,
|
|
10
|
+
renameNoReplacePath,
|
|
11
|
+
} from "@gajae-code/natives";
|
|
4
12
|
import { compileGjcPluginBundle } from "./compiler";
|
|
5
13
|
import { migrateGjcPluginEntries } from "./migration";
|
|
6
14
|
import { gjcPluginProjectRoot, gjcPluginUserRoot } from "./paths";
|
|
@@ -11,6 +19,11 @@ const LOCK_FILENAME = "registry.lock";
|
|
|
11
19
|
const LOCK_TIMEOUT_MS = 5000;
|
|
12
20
|
const LOCK_RETRY_MS = 50;
|
|
13
21
|
|
|
22
|
+
export const RegistryLockTestHooks: {
|
|
23
|
+
beforeEviction?: (lockPath: string, raw: string) => void | Promise<void>;
|
|
24
|
+
beforePublish?: (stagingPath: string, lockPath: string) => void | Promise<void>;
|
|
25
|
+
} = {};
|
|
26
|
+
|
|
14
27
|
export function registryRootForScope(scope: GjcPluginScope, cwd: string): string {
|
|
15
28
|
return scope === "user" ? gjcPluginUserRoot() : gjcPluginProjectRoot(cwd);
|
|
16
29
|
}
|
|
@@ -323,57 +336,203 @@ export async function readRegistry(
|
|
|
323
336
|
if (!migrated.changed && discovered.length === 0) return registry;
|
|
324
337
|
// Re-check under the lock before persisting. Migration and legacy-root
|
|
325
338
|
// discovery are one transaction, never a normal runtime loader path.
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
await
|
|
333
|
-
|
|
339
|
+
// Contention (a concurrent install elsewhere) must not fail startup reads:
|
|
340
|
+
// the effective registry is already computed, so degrade to returning it
|
|
341
|
+
// unpersisted and let a later uncontended session write the migrated
|
|
342
|
+
// state. Mutating install paths keep their fail-loud install_conflict.
|
|
343
|
+
try {
|
|
344
|
+
return await withRegistryLock(scope, cwd, async () => {
|
|
345
|
+
const latest = await readRegistryRaw(scope, cwd);
|
|
346
|
+
const latestDiscovered = await discoverLegacyEntries(scope, cwd, latest.plugins);
|
|
347
|
+
const latestMigrated = await migrateGjcPluginEntries([...latest.plugins, ...latestDiscovered]);
|
|
348
|
+
if (latestMigrated.changed || latestDiscovered.length > 0) {
|
|
349
|
+
const next: GjcPluginRegistry = { ...latest, plugins: sortRegistryEntries(latestMigrated.entries) };
|
|
350
|
+
await writeRegistryUnlocked(next, cwd, scope);
|
|
351
|
+
return next;
|
|
352
|
+
}
|
|
353
|
+
return latest;
|
|
354
|
+
});
|
|
355
|
+
} catch (error) {
|
|
356
|
+
if (error instanceof GjcPluginLoadError && error.code === "install_conflict") {
|
|
357
|
+
return { ...registry, plugins: sortRegistryEntries(migrated.entries) };
|
|
334
358
|
}
|
|
335
|
-
|
|
359
|
+
throw error;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const LOCK_NONCE_PATTERN = /^[0-9a-f]{16}$/;
|
|
364
|
+
|
|
365
|
+
interface RegistryLockToken {
|
|
366
|
+
pid: number;
|
|
367
|
+
/** Host segment of a host-tagged token; null for legacy `pid-nonce` tokens. */
|
|
368
|
+
host: string | null;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function parseRegistryLockToken(raw: string): RegistryLockToken | null {
|
|
372
|
+
const parts = raw.trim().split("-");
|
|
373
|
+
if (parts.length < 2) return null;
|
|
374
|
+
const pid = Number(parts[0]);
|
|
375
|
+
const nonce = parts[parts.length - 1];
|
|
376
|
+
if (!Number.isInteger(pid) || pid <= 0 || !LOCK_NONCE_PATTERN.test(nonce)) return null;
|
|
377
|
+
// Hostnames may contain '-', so everything between the first and last
|
|
378
|
+
// segment is the host; a single middle segment that is empty (or absent)
|
|
379
|
+
// means a legacy token.
|
|
380
|
+
const host = parts.slice(1, -1).join("-");
|
|
381
|
+
return { pid, host: host.length > 0 ? host : null };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* A holder is assumed alive unless provably dead: ourselves, a live PID
|
|
386
|
+
* (EPERM counts — the process exists under another user), or a lock written
|
|
387
|
+
* on another host (a project registry can live on shared storage, where a
|
|
388
|
+
* local PID probe would be meaningless) all stay fail-closed.
|
|
389
|
+
*/
|
|
390
|
+
function registryLockHolderAlive(holder: RegistryLockToken): boolean {
|
|
391
|
+
if (holder.pid === process.pid) return true;
|
|
392
|
+
if (holder.host !== null && holder.host !== os.hostname()) return true;
|
|
393
|
+
try {
|
|
394
|
+
process.kill(holder.pid, 0);
|
|
395
|
+
return true;
|
|
396
|
+
} catch (error) {
|
|
397
|
+
return (error as NodeJS.ErrnoException)?.code === "EPERM";
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async function captureRegistryLockIdentity(lockPath: string): Promise<NativeExactFileIdentity | null> {
|
|
402
|
+
try {
|
|
403
|
+
const [bytes, stat, parent] = await Promise.all([
|
|
404
|
+
fs.readFile(lockPath),
|
|
405
|
+
fs.lstat(lockPath, { bigint: true }),
|
|
406
|
+
fs.stat(path.dirname(lockPath), { bigint: true }),
|
|
407
|
+
]);
|
|
408
|
+
if (!stat.isFile()) return null;
|
|
409
|
+
return {
|
|
410
|
+
dev: stat.dev,
|
|
411
|
+
ino: stat.ino,
|
|
412
|
+
nlink: stat.nlink,
|
|
413
|
+
parentDev: parent.dev,
|
|
414
|
+
parentIno: parent.ino,
|
|
415
|
+
size: stat.size,
|
|
416
|
+
mtimeNs: stat.mtimeNs,
|
|
417
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
418
|
+
};
|
|
419
|
+
} catch (error) {
|
|
420
|
+
if (isEnoent(error)) return null;
|
|
421
|
+
throw error;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async function evictRegistryLock(lockPath: string, raw: string): Promise<boolean> {
|
|
426
|
+
const identity = await captureRegistryLockIdentity(lockPath);
|
|
427
|
+
if (!identity) return false;
|
|
428
|
+
const expectedHash = createHash("sha256").update(raw).digest("hex");
|
|
429
|
+
if (identity.sha256 !== expectedHash) return false;
|
|
430
|
+
await RegistryLockTestHooks.beforeEviction?.(lockPath, raw);
|
|
431
|
+
const result = exactUnlink(lockPath, {
|
|
432
|
+
...identity,
|
|
433
|
+
quarantineName: `${LOCK_FILENAME}.evict-${process.pid}-${randomBytes(4).toString("hex")}`,
|
|
336
434
|
});
|
|
435
|
+
return result.ok || result.code === "cleanup_pending";
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function publishRegistryLock(stagingPath: string, lockPath: string): NativeNoReplaceResult {
|
|
439
|
+
let result = renameNoReplacePath(stagingPath, lockPath);
|
|
440
|
+
if (
|
|
441
|
+
!result.ok &&
|
|
442
|
+
result.mutationState === "not_committed" &&
|
|
443
|
+
(result.reason === "atomic_unavailable" || result.reason === "invalid_request")
|
|
444
|
+
) {
|
|
445
|
+
// linkat is the no-overwrite fallback on filesystems without a
|
|
446
|
+
// no-replace rename. Both paths expose the complete token only after the
|
|
447
|
+
// atomic claim, so a crash cannot publish an empty or partial lock.
|
|
448
|
+
result = linkNoReplacePath(stagingPath, lockPath);
|
|
449
|
+
}
|
|
450
|
+
return result;
|
|
337
451
|
}
|
|
338
452
|
|
|
339
453
|
async function acquireLock(lockPath: string): Promise<() => Promise<void>> {
|
|
340
454
|
await fs.mkdir(path.dirname(lockPath), { recursive: true });
|
|
341
|
-
const token = `${process.pid}-${randomBytes(8).toString("hex")}`;
|
|
455
|
+
const token = `${process.pid}-${os.hostname()}-${randomBytes(8).toString("hex")}`;
|
|
342
456
|
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
343
457
|
for (;;) {
|
|
458
|
+
const stagingPath = `${lockPath}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
|
|
459
|
+
let publication: NativeNoReplaceResult | undefined;
|
|
344
460
|
try {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
461
|
+
await fs.writeFile(stagingPath, token, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
462
|
+
await RegistryLockTestHooks.beforePublish?.(stagingPath, lockPath);
|
|
463
|
+
publication = publishRegistryLock(stagingPath, lockPath);
|
|
464
|
+
if (publication.ok) {
|
|
465
|
+
let released = false;
|
|
466
|
+
return async () => {
|
|
467
|
+
if (released) return;
|
|
468
|
+
released = true;
|
|
469
|
+
// Owner-safe release: only remove the lock if it is still ours.
|
|
470
|
+
try {
|
|
471
|
+
const current = await fs.readFile(lockPath, "utf8");
|
|
472
|
+
if (current === token) await fs.rm(lockPath, { force: true });
|
|
473
|
+
} catch {
|
|
474
|
+
// Lock already gone; nothing to release.
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
if (publication.reason !== "destination_exists") {
|
|
479
|
+
throw new Error(
|
|
480
|
+
`Failed to publish GJC plugin registry lock at ${lockPath}: ${publication.code ?? publication.reason}`,
|
|
481
|
+
);
|
|
350
482
|
}
|
|
351
|
-
let released = false;
|
|
352
|
-
return async () => {
|
|
353
|
-
if (released) return;
|
|
354
|
-
released = true;
|
|
355
|
-
// Owner-safe release: only remove the lock if it is still ours.
|
|
356
|
-
try {
|
|
357
|
-
const current = await fs.readFile(lockPath, "utf8");
|
|
358
|
-
if (current === token) await fs.rm(lockPath, { force: true });
|
|
359
|
-
} catch {
|
|
360
|
-
// Lock already gone; nothing to release.
|
|
361
|
-
}
|
|
362
|
-
};
|
|
363
483
|
} catch (error) {
|
|
364
|
-
if (
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
484
|
+
if (publication?.reason === "destination_exists") {
|
|
485
|
+
// Continue into stale inspection below.
|
|
486
|
+
} else if (
|
|
487
|
+
publication === undefined &&
|
|
488
|
+
["EACCES", "EPERM"].includes((error as NodeJS.ErrnoException)?.code ?? "")
|
|
489
|
+
) {
|
|
490
|
+
// A read-only registry directory can still contain a lock left by a
|
|
491
|
+
// dead writer. Inspect that lock and return the normal bounded
|
|
492
|
+
// install_conflict rather than surfacing the staging-file permission
|
|
493
|
+
// error immediately.
|
|
494
|
+
const lockExists = await fs.stat(lockPath).then(
|
|
495
|
+
() => true,
|
|
496
|
+
(error: unknown) => !isEnoent(error),
|
|
373
497
|
);
|
|
498
|
+
if (!lockExists) throw error;
|
|
499
|
+
} else throw error;
|
|
500
|
+
} finally {
|
|
501
|
+
if (publication?.mutationState !== "unknown") await fs.rm(stagingPath, { force: true }).catch(() => {});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Stale recovery: a writer that died mid-transaction never releases
|
|
505
|
+
// its lock, and without recovery that lock poisons every later
|
|
506
|
+
// session until manual cleanup. Evict only a provably dead holder:
|
|
507
|
+
// a host-tagged token on this host. Legacy `pid-nonce` tokens have no
|
|
508
|
+
// trustworthy host identity, so they always fail closed: a local PID
|
|
509
|
+
// probe cannot prove that a remote holder on shared storage is dead.
|
|
510
|
+
// Live holders still fail closed below — they may legitimately outlive
|
|
511
|
+
// our timeout.
|
|
512
|
+
const raw = await fs.readFile(lockPath, "utf8").catch(() => null);
|
|
513
|
+
const holder = raw === null ? null : parseRegistryLockToken(raw);
|
|
514
|
+
if (raw !== null && holder !== null && holder.host !== null && !registryLockHolderAlive(holder)) {
|
|
515
|
+
try {
|
|
516
|
+
// exactUnlink verifies the observed inode, parent, size, mtime,
|
|
517
|
+
// and bytes inside the removal primitive. A replacement published
|
|
518
|
+
// after this read is therefore never consumed by stale recovery.
|
|
519
|
+
await evictRegistryLock(lockPath, raw);
|
|
520
|
+
} catch {
|
|
521
|
+
// Raced with another evictor or a fresh holder, or the removal
|
|
522
|
+
// itself failed (lockfile owned by another user, read-only mount,
|
|
523
|
+
// EBUSY/EPERM on Windows). Fall through to the shared deadline
|
|
524
|
+
// and backoff below either way: a removal that keeps failing must
|
|
525
|
+
// end in the bounded install_conflict timeout, never an unbounded
|
|
526
|
+
// spin.
|
|
374
527
|
}
|
|
375
|
-
await new Promise(resolve => setTimeout(resolve, LOCK_RETRY_MS));
|
|
376
528
|
}
|
|
529
|
+
if (Date.now() > deadline) {
|
|
530
|
+
throw new GjcPluginLoadError(
|
|
531
|
+
"install_conflict",
|
|
532
|
+
`Timed out acquiring GJC plugin registry lock at ${lockPath}; remove it manually if no install is running`,
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
await new Promise(resolve => setTimeout(resolve, LOCK_RETRY_MS));
|
|
377
536
|
}
|
|
378
537
|
}
|
|
379
538
|
|