@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
|
@@ -44,6 +44,9 @@ import { fetchModelsDevPayload } from "@gajae-code/ai/provider-models/openai-com
|
|
|
44
44
|
import { isDirectXaiReasoningEffortModel } from "@gajae-code/ai/providers/openai-completions-compat";
|
|
45
45
|
import {
|
|
46
46
|
detectDiscoveredApiFamily,
|
|
47
|
+
isSafeCatalogModelId,
|
|
48
|
+
MODELS_LIST_REQUEST_TIMEOUT_MS,
|
|
49
|
+
readBoundedModelsJson,
|
|
47
50
|
resolveLoopbackOpenAIBaseUrl,
|
|
48
51
|
} from "@gajae-code/ai/utils/discovery/openai-compatible";
|
|
49
52
|
|
|
@@ -144,6 +147,36 @@ function redactDiscoveryUrl(value: string | URL): string {
|
|
|
144
147
|
}
|
|
145
148
|
}
|
|
146
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Scrub resolved credential material from a discovery failure before it is
|
|
152
|
+
* published to discovery state, cache provenance, or the logger. Mutates
|
|
153
|
+
* Error messages and stacks in place to preserve class/identity; wraps non-Errors.
|
|
154
|
+
*/
|
|
155
|
+
export function scrubDiscoveryError(error: unknown, secrets: ReadonlyArray<string | undefined>): unknown {
|
|
156
|
+
const redact = (text: string): string => {
|
|
157
|
+
let scrubbed = text;
|
|
158
|
+
for (const secret of secrets) {
|
|
159
|
+
if (secret) scrubbed = scrubbed.split(secret).join("[redacted]");
|
|
160
|
+
}
|
|
161
|
+
return scrubbed;
|
|
162
|
+
};
|
|
163
|
+
if (error instanceof Error) {
|
|
164
|
+
const seen = new Set<Error>();
|
|
165
|
+
const scrubError = (current: Error): void => {
|
|
166
|
+
if (seen.has(current)) return;
|
|
167
|
+
seen.add(current);
|
|
168
|
+
current.message = redact(current.message);
|
|
169
|
+
if (typeof current.stack === "string") current.stack = redact(current.stack);
|
|
170
|
+
if (current.cause instanceof Error) scrubError(current.cause);
|
|
171
|
+
else if (typeof current.cause === "string") current.cause = redact(current.cause);
|
|
172
|
+
};
|
|
173
|
+
scrubError(error);
|
|
174
|
+
return error;
|
|
175
|
+
}
|
|
176
|
+
if (typeof error === "string") return redact(error);
|
|
177
|
+
return error;
|
|
178
|
+
}
|
|
179
|
+
|
|
147
180
|
/** Whether a structured transport code proves that no listener accepted the connection. */
|
|
148
181
|
function isConnectionRefusedDiscoveryCode(value: unknown): boolean {
|
|
149
182
|
if (typeof value === "string") {
|
|
@@ -742,13 +775,26 @@ function registryModelMetadataWithoutApiSpecificFields(model: Model<Api>): Parti
|
|
|
742
775
|
return metadata;
|
|
743
776
|
}
|
|
744
777
|
|
|
745
|
-
function registrySelectorResolvesToModel(selector: string, models: readonly Model<Api>[]): boolean {
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
778
|
+
export function registrySelectorResolvesToModel(selector: string, models: readonly Model<Api>[]): boolean {
|
|
779
|
+
const normalizedSelector = selector.trim().toLowerCase();
|
|
780
|
+
if (
|
|
781
|
+
models.some(
|
|
782
|
+
model =>
|
|
783
|
+
model.id.toLowerCase() === normalizedSelector ||
|
|
784
|
+
`${model.provider}/${model.id}`.toLowerCase() === normalizedSelector,
|
|
785
|
+
)
|
|
786
|
+
)
|
|
787
|
+
return true;
|
|
788
|
+
const suffix = splitSelectorThinkingSuffix(normalizedSelector);
|
|
789
|
+
const baseSelector = suffix.thinkingLevel === undefined ? normalizedSelector : suffix.selector;
|
|
749
790
|
const parsed = parseModelString(baseSelector);
|
|
750
|
-
if (parsed)
|
|
751
|
-
|
|
791
|
+
if (parsed)
|
|
792
|
+
return models.some(
|
|
793
|
+
model => model.provider.toLowerCase() === parsed.provider && model.id.toLowerCase() === parsed.id,
|
|
794
|
+
);
|
|
795
|
+
return models.some(
|
|
796
|
+
model => model.id.toLowerCase() === baseSelector || model.id.toLowerCase().endsWith(`/${baseSelector}`),
|
|
797
|
+
);
|
|
752
798
|
}
|
|
753
799
|
|
|
754
800
|
function filterMaterializedRegistryProfiles(
|
|
@@ -3249,7 +3295,7 @@ export class ModelRegistry {
|
|
|
3249
3295
|
);
|
|
3250
3296
|
if (
|
|
3251
3297
|
evidence !== undefined &&
|
|
3252
|
-
state?.status === "ok" &&
|
|
3298
|
+
(state?.status === "ok" || state?.status === "empty") &&
|
|
3253
3299
|
discovery.fetched &&
|
|
3254
3300
|
currentAuthGeneration === evidence.authGeneration &&
|
|
3255
3301
|
currentEndpoint === evidence.endpoint
|
|
@@ -3287,8 +3333,8 @@ export class ModelRegistry {
|
|
|
3287
3333
|
}
|
|
3288
3334
|
|
|
3289
3335
|
#profileAvailabilityEvidenceFingerprint(): string {
|
|
3290
|
-
return JSON.stringify(
|
|
3291
|
-
[...this.#descriptorDiscoveryEvidence.entries()]
|
|
3336
|
+
return JSON.stringify({
|
|
3337
|
+
descriptor: [...this.#descriptorDiscoveryEvidence.entries()]
|
|
3292
3338
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
3293
3339
|
.map(([provider, evidence]) => [
|
|
3294
3340
|
provider,
|
|
@@ -3300,7 +3346,15 @@ export class ModelRegistry {
|
|
|
3300
3346
|
[...evidence.modelIds].sort(),
|
|
3301
3347
|
evidence.profileModelIds === undefined ? undefined : [...evidence.profileModelIds].sort(),
|
|
3302
3348
|
]),
|
|
3303
|
-
|
|
3349
|
+
configured: [...this.#configuredDiscoveryEvidence.entries()]
|
|
3350
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
3351
|
+
.map(([provider, evidence]) => [
|
|
3352
|
+
provider,
|
|
3353
|
+
evidence.authGeneration,
|
|
3354
|
+
evidence.endpoint,
|
|
3355
|
+
[...evidence.modelIds].sort(),
|
|
3356
|
+
]),
|
|
3357
|
+
});
|
|
3304
3358
|
}
|
|
3305
3359
|
|
|
3306
3360
|
#mergeDiscoveredModels(discovered: readonly Model<Api>[]): void {
|
|
@@ -3496,8 +3550,14 @@ export class ModelRegistry {
|
|
|
3496
3550
|
await this.#discoverModelsByProviderType(provider, apiKey),
|
|
3497
3551
|
);
|
|
3498
3552
|
} catch (error) {
|
|
3499
|
-
|
|
3500
|
-
|
|
3553
|
+
// ONE credential-safe discovery error boundary: transport
|
|
3554
|
+
// layers may echo request details (including the bearer)
|
|
3555
|
+
// in failure text. Scrub resolved credential material
|
|
3556
|
+
// before the error reaches discovery state, cache
|
|
3557
|
+
// provenance, or the logger below.
|
|
3558
|
+
const scrubbedError = scrubDiscoveryError(error, [apiKey, preflightApiKey]);
|
|
3559
|
+
discoveryFailureEvidence = scrubbedError;
|
|
3560
|
+
throw scrubbedError;
|
|
3501
3561
|
}
|
|
3502
3562
|
},
|
|
3503
3563
|
getEvidenceGeneration: provider => this.#getProviderEvidenceGeneration(provider.provider, preflightApiKey),
|
|
@@ -3569,24 +3629,35 @@ export class ModelRegistry {
|
|
|
3569
3629
|
};
|
|
3570
3630
|
}
|
|
3571
3631
|
|
|
3572
|
-
#discoverModelsByProviderType(
|
|
3632
|
+
async #discoverModelsByProviderType(
|
|
3573
3633
|
providerConfig: DiscoveryProviderConfig,
|
|
3574
3634
|
apiKey: string | undefined,
|
|
3575
3635
|
): Promise<Model<Api>[]> {
|
|
3636
|
+
let models: Model<Api>[];
|
|
3576
3637
|
switch (providerConfig.discovery.type) {
|
|
3577
3638
|
case "ollama":
|
|
3578
|
-
|
|
3639
|
+
models = await this.#discoverOllamaModels(providerConfig, apiKey);
|
|
3640
|
+
break;
|
|
3579
3641
|
case "llama.cpp":
|
|
3580
|
-
|
|
3642
|
+
models = await this.#discoverLlamaCppModels(providerConfig, apiKey);
|
|
3643
|
+
break;
|
|
3581
3644
|
case "lm-studio":
|
|
3582
3645
|
case "omlx":
|
|
3583
3646
|
case "vllm":
|
|
3584
3647
|
case "sglang":
|
|
3585
3648
|
case "openai-models-list":
|
|
3586
|
-
|
|
3649
|
+
models = await this.#discoverOpenAIModelsList(providerConfig, apiKey);
|
|
3650
|
+
break;
|
|
3587
3651
|
case "models-dev":
|
|
3588
|
-
|
|
3652
|
+
models = await this.#discoverModelsDevProvider(providerConfig);
|
|
3653
|
+
break;
|
|
3654
|
+
}
|
|
3655
|
+
if (providerConfig.discovery.type === "openai-models-list" && models.length === 0) {
|
|
3656
|
+
throw new Error(
|
|
3657
|
+
`Model discovery for ${redactDiscoveryUrl(providerConfig.baseUrl ?? "")} returned no models; add --model <id> or fix the endpoint catalog.`,
|
|
3658
|
+
);
|
|
3589
3659
|
}
|
|
3660
|
+
return models;
|
|
3590
3661
|
}
|
|
3591
3662
|
|
|
3592
3663
|
async #discoverBuiltInProviderModels(
|
|
@@ -4222,7 +4293,7 @@ export class ModelRegistry {
|
|
|
4222
4293
|
...(providerConfig.discovery.type === "vllm" || providerConfig.discovery.type === "sglang"
|
|
4223
4294
|
? { redirect: "error" as const }
|
|
4224
4295
|
: {}),
|
|
4225
|
-
signal: AbortSignal.timeout(hardenedLocalDiscovery ? 500 :
|
|
4296
|
+
signal: AbortSignal.timeout(hardenedLocalDiscovery ? 500 : MODELS_LIST_REQUEST_TIMEOUT_MS),
|
|
4226
4297
|
});
|
|
4227
4298
|
if (!response.ok) {
|
|
4228
4299
|
if (response.status === 401 || response.status === 403) {
|
|
@@ -4234,16 +4305,31 @@ export class ModelRegistry {
|
|
|
4234
4305
|
}
|
|
4235
4306
|
throw new Error(`HTTP ${response.status} from ${redactDiscoveryUrl(modelsUrl)}`);
|
|
4236
4307
|
}
|
|
4237
|
-
|
|
4308
|
+
// Shared 1MB bounded reader (same as the setup probe): a timeout
|
|
4309
|
+
// bounds wall clock, not bytes buffered.
|
|
4310
|
+
let payload: unknown;
|
|
4311
|
+
try {
|
|
4312
|
+
payload = await readBoundedModelsJson(response);
|
|
4313
|
+
} catch {
|
|
4314
|
+
throw new Error(`Malformed OpenAI models-list response from ${redactDiscoveryUrl(modelsUrl)}`);
|
|
4315
|
+
}
|
|
4238
4316
|
if (!isRecord(payload) || !Array.isArray(payload.data)) {
|
|
4239
4317
|
throw new Error(`Malformed OpenAI models-list response from ${redactDiscoveryUrl(modelsUrl)}`);
|
|
4240
4318
|
}
|
|
4241
4319
|
const models = payload.data;
|
|
4242
4320
|
const discovered: Model<Api>[] = [];
|
|
4243
4321
|
for (const item of models) {
|
|
4244
|
-
|
|
4245
|
-
|
|
4322
|
+
// Shared catalog admission: the setup probe applies the same
|
|
4323
|
+
// check, so IDs that appear only between requests cannot enter
|
|
4324
|
+
// the live catalog or cache unsanitized.
|
|
4325
|
+
if (!isRecord(item) || !isSafeCatalogModelId(item.id)) continue;
|
|
4326
|
+
const id = item.id.trim();
|
|
4246
4327
|
const referenceModel = resolveCustomModelReference(id);
|
|
4328
|
+
// Names render directly in the model selector: apply the same
|
|
4329
|
+
// text-safety admission, falling back to the reference name or
|
|
4330
|
+
// the safe ID.
|
|
4331
|
+
const rawName = typeof item.name === "string" ? item.name.trim() : "";
|
|
4332
|
+
const name = rawName && isSafeCatalogModelId(rawName) ? rawName : (referenceModel?.name ?? id);
|
|
4247
4333
|
const discoveredMaxTokens = firstPositiveDiscoveryNumber(
|
|
4248
4334
|
item.max_completion_tokens,
|
|
4249
4335
|
item.max_tokens,
|
|
@@ -4253,7 +4339,7 @@ export class ModelRegistry {
|
|
|
4253
4339
|
discovered.push(
|
|
4254
4340
|
enrichModelThinking({
|
|
4255
4341
|
id,
|
|
4256
|
-
name
|
|
4342
|
+
name,
|
|
4257
4343
|
api,
|
|
4258
4344
|
provider: providerConfig.provider,
|
|
4259
4345
|
baseUrl: requestBaseUrl,
|
|
@@ -5226,8 +5312,10 @@ export class ModelRegistry {
|
|
|
5226
5312
|
}
|
|
5227
5313
|
|
|
5228
5314
|
/**
|
|
5229
|
-
* Get
|
|
5230
|
-
* This is a fast check that doesn't refresh OAuth tokens.
|
|
5315
|
+
* Get selectable models with auth configured.
|
|
5316
|
+
* This is a fast check that doesn't refresh OAuth tokens. A current,
|
|
5317
|
+
* authoritative live catalog also limits each provider to its enrolled ids;
|
|
5318
|
+
* bundled entries remain the fallback until that evidence exists.
|
|
5231
5319
|
*/
|
|
5232
5320
|
getAvailable(): Model<Api>[] {
|
|
5233
5321
|
this.#synchronizeEnvironmentCredentials();
|
|
@@ -5241,12 +5329,82 @@ export class ModelRegistry {
|
|
|
5241
5329
|
) {
|
|
5242
5330
|
return this.#availableModelsCache;
|
|
5243
5331
|
}
|
|
5244
|
-
|
|
5332
|
+
const authoritativeDiscoveryIds = new Map<string, ReadonlySet<string> | undefined>();
|
|
5333
|
+
const bundledIdsByProvider = new Map<string, Set<string>>();
|
|
5334
|
+
this.#availableModelsCache = this.#models.filter(model => {
|
|
5335
|
+
if (!this.#isModelAvailable(model, disabledProviders)) return false;
|
|
5336
|
+
|
|
5337
|
+
let liveIds = authoritativeDiscoveryIds.get(model.provider);
|
|
5338
|
+
if (!authoritativeDiscoveryIds.has(model.provider)) {
|
|
5339
|
+
liveIds = this.#getAuthoritativeDiscoveredModelIds(model.provider);
|
|
5340
|
+
authoritativeDiscoveryIds.set(model.provider, liveIds);
|
|
5341
|
+
}
|
|
5342
|
+
// Undefined means discovery has not run successfully for the current
|
|
5343
|
+
// provider context. Keep the bundled catalog as the explicit fallback.
|
|
5344
|
+
if (liveIds === undefined) return true;
|
|
5345
|
+
if (this.#hasCustomModelOverlay(model.provider, model.id)) return true;
|
|
5346
|
+
|
|
5347
|
+
const activity = this.#providerActivity.get(model.provider);
|
|
5348
|
+
if (!activity?.staticModelIds.has(model.id)) return liveIds.has(model.id);
|
|
5349
|
+
|
|
5350
|
+
let bundledIds = bundledIdsByProvider.get(model.provider);
|
|
5351
|
+
if (!bundledIds) {
|
|
5352
|
+
bundledIds = new Set(
|
|
5353
|
+
(getBundledModels(model.provider as Parameters<typeof getBundledModels>[0]) as Model<Api>[]).map(
|
|
5354
|
+
candidate => candidate.id,
|
|
5355
|
+
),
|
|
5356
|
+
);
|
|
5357
|
+
bundledIdsByProvider.set(model.provider, bundledIds);
|
|
5358
|
+
}
|
|
5359
|
+
return !bundledIds.has(model.id) || liveIds.has(model.id);
|
|
5360
|
+
});
|
|
5245
5361
|
this.#availableModelsDisabledProviders = disabledProviderKey;
|
|
5246
5362
|
this.#availableModelsEnvFingerprint = envFingerprint;
|
|
5247
5363
|
return this.#availableModelsCache;
|
|
5248
5364
|
}
|
|
5249
5365
|
|
|
5366
|
+
/**
|
|
5367
|
+
* Return the current live model ids only when discovery produced an
|
|
5368
|
+
* authoritative catalog for the provider and endpoint. An unavailable or
|
|
5369
|
+
* failed discovery returns undefined so callers retain the bundled fallback.
|
|
5370
|
+
*/
|
|
5371
|
+
#getAuthoritativeDiscoveredModelIds(provider: string): ReadonlySet<string> | undefined {
|
|
5372
|
+
const descriptorEvidence = this.#descriptorDiscoveryEvidence.get(provider);
|
|
5373
|
+
if (descriptorEvidence?.profileFresh && descriptorEvidence.profileModelIds !== undefined) {
|
|
5374
|
+
try {
|
|
5375
|
+
if (
|
|
5376
|
+
descriptorEvidence.authGeneration === this.#getProviderEvidenceGeneration(provider) &&
|
|
5377
|
+
descriptorEvidence.profileEndpoint ===
|
|
5378
|
+
this.#normalizeDiscoveryEvidenceEndpoint(this.#getProviderBaseUrlForDiscovery(provider) ?? "")
|
|
5379
|
+
) {
|
|
5380
|
+
return descriptorEvidence.profileModelIds;
|
|
5381
|
+
}
|
|
5382
|
+
} catch {
|
|
5383
|
+
// A provider context that can no longer be verified must use its fallback.
|
|
5384
|
+
}
|
|
5385
|
+
}
|
|
5386
|
+
|
|
5387
|
+
const configuredEvidence = this.#configuredDiscoveryEvidence.get(provider);
|
|
5388
|
+
if (!configuredEvidence) return undefined;
|
|
5389
|
+
const discoveryState = this.#discoveryManager.getState(provider);
|
|
5390
|
+
if (
|
|
5391
|
+
discoveryState?.status !== "ok" &&
|
|
5392
|
+
discoveryState?.status !== "cached" &&
|
|
5393
|
+
discoveryState?.status !== "empty"
|
|
5394
|
+
) {
|
|
5395
|
+
return undefined;
|
|
5396
|
+
}
|
|
5397
|
+
try {
|
|
5398
|
+
return configuredEvidence.authGeneration === this.#getProviderEvidenceGeneration(provider) &&
|
|
5399
|
+
configuredEvidence.endpoint ===
|
|
5400
|
+
this.#normalizeDiscoveryEvidenceEndpoint(this.#getProviderBaseUrlForDiscovery(provider) ?? "")
|
|
5401
|
+
? configuredEvidence.modelIds
|
|
5402
|
+
: undefined;
|
|
5403
|
+
} catch {
|
|
5404
|
+
return undefined;
|
|
5405
|
+
}
|
|
5406
|
+
}
|
|
5407
|
+
|
|
5250
5408
|
#synchronizeEnvironmentCredentials(): void {
|
|
5251
5409
|
// Runtime registrations own a provider while their credential is present.
|
|
5252
5410
|
// Refresh them first so a missing runtime env key can hand ownership back to
|
|
@@ -369,6 +369,24 @@ const AUTOROUTING_PROVENANCE_JSON_SCHEMA: JsonSchemaObject = {
|
|
|
369
369
|
required: ["schema", "source", "declarationFingerprint", "tiersFingerprint"],
|
|
370
370
|
};
|
|
371
371
|
|
|
372
|
+
// SDK prompt deadline defaults. The `sdk.promptDeadlineMs` /
|
|
373
|
+
// `sdk.promptMaxRuntimeMs` schema entries below and the SDK bus/host lease
|
|
374
|
+
// fallbacks read these same constants, so a Settings lookup that misses — no
|
|
375
|
+
// settings object, an unwritten key, a non-finite value — cannot arm a deadline
|
|
376
|
+
// that disagrees with the declared default (#5583).
|
|
377
|
+
export const DEFAULT_SDK_PROMPT_DEADLINE_MS = 3_600_000;
|
|
378
|
+
export const DEFAULT_SDK_PROMPT_MAX_RUNTIME_MS = 21_600_000;
|
|
379
|
+
|
|
380
|
+
/** Resolve a `sdk.promptDeadlineMs` lookup, falling back on any non-finite value. */
|
|
381
|
+
export function resolveSdkPromptDeadlineMs(value: unknown): number {
|
|
382
|
+
return typeof value === "number" && Number.isFinite(value) ? value : DEFAULT_SDK_PROMPT_DEADLINE_MS;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** Resolve a `sdk.promptMaxRuntimeMs` lookup, falling back on any non-finite value. */
|
|
386
|
+
export function resolveSdkPromptMaxRuntimeMs(value: unknown): number {
|
|
387
|
+
return typeof value === "number" && Number.isFinite(value) ? value : DEFAULT_SDK_PROMPT_MAX_RUNTIME_MS;
|
|
388
|
+
}
|
|
389
|
+
|
|
372
390
|
export const SETTINGS_SCHEMA = {
|
|
373
391
|
// ────────────────────────────────────────────────────────────────────────
|
|
374
392
|
// General settings (no UI)
|
|
@@ -416,16 +434,22 @@ export const SETTINGS_SCHEMA = {
|
|
|
416
434
|
// SDK-owned prompt deadline. Hidden from the UI; ACP has no separate timeout.
|
|
417
435
|
"sdk.promptDeadlineMs": {
|
|
418
436
|
type: "number",
|
|
419
|
-
default:
|
|
437
|
+
default: DEFAULT_SDK_PROMPT_DEADLINE_MS,
|
|
420
438
|
description: "SDK-owned prompt deadline; ACP has no separate timeout.",
|
|
421
439
|
validate: (value: number) => Number.isSafeInteger(value) && value >= 60_000 && value <= 86_400_000,
|
|
422
440
|
},
|
|
423
441
|
"sdk.promptMaxRuntimeMs": {
|
|
424
442
|
type: "number",
|
|
425
|
-
default:
|
|
443
|
+
default: DEFAULT_SDK_PROMPT_MAX_RUNTIME_MS,
|
|
426
444
|
description: "Hard maximum runtime for an SDK prompt from acceptance, bounding progress-aware renewals.",
|
|
427
445
|
validate: (value: number) => Number.isSafeInteger(value) && value >= 60_000 && value <= 86_400_000,
|
|
428
446
|
},
|
|
447
|
+
"sdk.flushWorktreeOnDeadline": {
|
|
448
|
+
type: "boolean",
|
|
449
|
+
default: true,
|
|
450
|
+
description:
|
|
451
|
+
"Autosave uncommitted changes in the session's linked worktree by default as a WIP commit when a prompt deadline retires a prompt; primary checkouts require an explicit true opt-in.",
|
|
452
|
+
},
|
|
429
453
|
"sdk.masterOrphanGraceMs": {
|
|
430
454
|
type: "number",
|
|
431
455
|
default: 120_000,
|
|
@@ -275,11 +275,16 @@ export async function assertCoordinatorSessionLocations(
|
|
|
275
275
|
]);
|
|
276
276
|
// Reauthorize both persisted locations independently. A managed worktree may
|
|
277
277
|
// differ from the requested cwd, but neither may escape the current roots.
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
278
|
+
const escaped: string[] = [];
|
|
279
|
+
if (!roots.some(root => isInsideCanonicalRoot(canonicalCwd, root, platform))) escaped.push(`cwd ${canonicalCwd}`);
|
|
280
|
+
if (!roots.some(root => isInsideCanonicalRoot(canonicalWorkspace, root, platform)))
|
|
281
|
+
escaped.push(`workspace ${canonicalWorkspace}`);
|
|
282
|
+
if (escaped.length > 0)
|
|
283
|
+
// The message stays exact because isSessionAuthorityError() in server.ts matches it
|
|
284
|
+
// verbatim; the escaping location rides in `cause` so a caller that logs can name it.
|
|
285
|
+
throw new Error("coordinator_workdir_outside_allowed_roots", {
|
|
286
|
+
cause: new Error(`outside allowed roots: ${escaped.join(", ")}`),
|
|
287
|
+
});
|
|
283
288
|
}
|
|
284
289
|
|
|
285
290
|
export async function assertCoordinatorArtifactPath(
|
|
@@ -184,6 +184,23 @@ export function buildCoordinatorAskAnswerSchema(
|
|
|
184
184
|
return {
|
|
185
185
|
title: "CoordinatorQuestionAnswerV1",
|
|
186
186
|
type: "object",
|
|
187
|
+
// `additionalProperties: false` is scoped by the SIBLING `properties` map, not by
|
|
188
|
+
// the `oneOf` branches: a standards-compliant validator evaluates the keyword
|
|
189
|
+
// against this node alone. Without this map every answer field was "additional",
|
|
190
|
+
// so selection, custom-answer, and clarification payloads were all rejected
|
|
191
|
+
// client-side before the server saw them (#5801). Each branch stays closed too,
|
|
192
|
+
// so the union still rejects field combinations this map alone would allow.
|
|
193
|
+
properties: {
|
|
194
|
+
selected: selectedBase,
|
|
195
|
+
other: { type: "boolean", description: "set true to provide a free-text answer in `custom`" },
|
|
196
|
+
custom: boundedAnswerText("free-text answer; required when `other` is true"),
|
|
197
|
+
action: {
|
|
198
|
+
type: "string",
|
|
199
|
+
enum: ["answer", "clarify"],
|
|
200
|
+
description: "set to `clarify` to ask about the choices without answering the round",
|
|
201
|
+
},
|
|
202
|
+
question: boundedAnswerText("clarification question; required when action is `clarify`"),
|
|
203
|
+
},
|
|
187
204
|
additionalProperties: false,
|
|
188
205
|
oneOf: [
|
|
189
206
|
{
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
+
import { logger } from "@gajae-code/utils";
|
|
4
5
|
import { withFileLock } from "../config/file-lock";
|
|
5
6
|
import { ensureCoordinatorDirectory, syncCoordinatorDirectory, writeCoordinatorAtomic } from "./durability";
|
|
6
7
|
import type { PrivateAskGateCodecV1, PublicReason } from "./question-gate-codec";
|
|
@@ -567,7 +568,7 @@ async function readTransactionJson<T>(file: string): Promise<T | null> {
|
|
|
567
568
|
source = await fs.readFile(file, "utf8");
|
|
568
569
|
} catch (error) {
|
|
569
570
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
570
|
-
throw
|
|
571
|
+
throw error;
|
|
571
572
|
}
|
|
572
573
|
try {
|
|
573
574
|
const value: unknown = JSON.parse(source);
|
|
@@ -605,6 +606,71 @@ function migrateLegacyTransactionV1(transaction: CoordinatorSessionTransactionV1
|
|
|
605
606
|
}
|
|
606
607
|
const authorities = canonical.gate_authorities;
|
|
607
608
|
const endpointIncarnation = typeof broker?.endpoint_incarnation === "string" ? broker.endpoint_incarnation : null;
|
|
609
|
+
// Report ids predate COORDINATOR_REPORT_ID_PATTERN. A legacy id is rejected by
|
|
610
|
+
// the reader forever, and because the reader is the namespace-wide scan, one
|
|
611
|
+
// historical report bricks every coordination read. Rekey legacy reports onto
|
|
612
|
+
// the canonical digest form instead of leaving the WAL permanently unreadable.
|
|
613
|
+
const reports = canonical.reports;
|
|
614
|
+
const migratedReportIds = new Map<string, string>();
|
|
615
|
+
if (reports && typeof reports === "object" && !Array.isArray(reports)) {
|
|
616
|
+
const table = reports as Record<string, Record<string, unknown>>;
|
|
617
|
+
for (const [reportId, report] of Object.entries(table)) {
|
|
618
|
+
if (COORDINATOR_REPORT_ID_PATTERN.test(reportId)) continue;
|
|
619
|
+
if (!report || typeof report !== "object" || Array.isArray(report)) continue;
|
|
620
|
+
const operationId = typeof report.operation_id === "string" ? report.operation_id : reportId;
|
|
621
|
+
let migratedId = `report-${digest(`legacy-report\0${record.session_id}\0${operationId}`)}`;
|
|
622
|
+
const existing = table[migratedId];
|
|
623
|
+
if (existing) {
|
|
624
|
+
const existingOperationId = typeof existing.operation_id === "string" ? existing.operation_id : null;
|
|
625
|
+
if (existingOperationId !== operationId) {
|
|
626
|
+
// A malformed legacy WAL can collide with the operation-derived digest.
|
|
627
|
+
// Preserve both records under distinct canonical ids rather than silently
|
|
628
|
+
// discarding an unrelated report.
|
|
629
|
+
migratedId = `report-${digest(`legacy-report\0${record.session_id}\0${operationId}\0${reportId}`)}`;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
if (table[migratedId]) {
|
|
633
|
+
// The canonical report already wins this deterministic collision. The
|
|
634
|
+
// operation/outbox rewrites below keep every surviving reference coherent.
|
|
635
|
+
logger.warn("Coordinator legacy report id collision resolved", {
|
|
636
|
+
sessionId: record.session_id,
|
|
637
|
+
legacyReportId: reportId,
|
|
638
|
+
canonicalReportId: migratedId,
|
|
639
|
+
operationId,
|
|
640
|
+
});
|
|
641
|
+
migratedReportIds.set(reportId, migratedId);
|
|
642
|
+
delete table[reportId];
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
report.report_id = migratedId;
|
|
646
|
+
table[migratedId] = report;
|
|
647
|
+
migratedReportIds.set(reportId, migratedId);
|
|
648
|
+
delete table[reportId];
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
if (migratedReportIds.size > 0) {
|
|
652
|
+
const requests = record.requests as Record<string, unknown>;
|
|
653
|
+
const operations = requests.operations;
|
|
654
|
+
if (operations && typeof operations === "object" && !Array.isArray(operations)) {
|
|
655
|
+
for (const operation of Object.values(operations as Record<string, Record<string, unknown>>)) {
|
|
656
|
+
if (!operation || typeof operation !== "object" || Array.isArray(operation)) continue;
|
|
657
|
+
if (typeof operation.local_id === "string")
|
|
658
|
+
operation.local_id = migratedReportIds.get(operation.local_id) ?? operation.local_id;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
const outbox = record.outbox as Record<string, Record<string, unknown>>;
|
|
662
|
+
for (const event of Object.values(outbox)) {
|
|
663
|
+
if (!event || typeof event !== "object" || Array.isArray(event) || event.entity !== "report") continue;
|
|
664
|
+
if (typeof event.entity_id === "string")
|
|
665
|
+
event.entity_id = migratedReportIds.get(event.entity_id) ?? event.entity_id;
|
|
666
|
+
const payload = event.payload;
|
|
667
|
+
if (payload && typeof payload === "object" && !Array.isArray(payload)) {
|
|
668
|
+
const reportId = (payload as Record<string, unknown>).report_id;
|
|
669
|
+
if (typeof reportId === "string")
|
|
670
|
+
(payload as Record<string, unknown>).report_id = migratedReportIds.get(reportId) ?? reportId;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
608
674
|
if (authorities && typeof authorities === "object" && !Array.isArray(authorities)) {
|
|
609
675
|
for (const authority of Object.values(authorities as Record<string, unknown>)) {
|
|
610
676
|
if (!authority || typeof authority !== "object" || Array.isArray(authority)) continue;
|
|
@@ -1322,9 +1388,16 @@ export async function listCanonicalActiveSessions(
|
|
|
1322
1388
|
const active: string[] = [];
|
|
1323
1389
|
for (const sessionId of sessionIds) {
|
|
1324
1390
|
if (options.signal?.aborted) throw options.signal.reason ?? new Error("aborted");
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1391
|
+
let transaction: CoordinatorSessionTransactionV1 | null;
|
|
1392
|
+
try {
|
|
1393
|
+
transaction = await readTransactionJson<CoordinatorSessionTransactionV1>(transactionPath(paths, sessionId));
|
|
1394
|
+
if (!transaction) continue;
|
|
1395
|
+
assertTransaction(transaction, path.basename(paths.root), sessionId);
|
|
1396
|
+
} catch (error) {
|
|
1397
|
+
if (!(error instanceof Error) || error.message !== "state_corrupt") throw error;
|
|
1398
|
+
logger.warn("Coordinator active-session scan skipped corrupt WAL", { sessionId });
|
|
1399
|
+
continue;
|
|
1400
|
+
}
|
|
1328
1401
|
const hasActiveTurn = Object.values(transaction.canonical.turns).some(turn =>
|
|
1329
1402
|
["queued", "delivering", "active", "waiting_for_answer", "completing"].includes(turn.status),
|
|
1330
1403
|
);
|
|
@@ -1418,14 +1491,74 @@ export async function advanceSchedulerCursor(
|
|
|
1418
1491
|
export async function readSessionTransaction(
|
|
1419
1492
|
paths: CoordinatorStatePaths,
|
|
1420
1493
|
sessionId: string,
|
|
1494
|
+
options: { signal?: AbortSignal } = {},
|
|
1421
1495
|
): Promise<CoordinatorSessionTransactionV1 | null> {
|
|
1496
|
+
options.signal?.throwIfAborted();
|
|
1422
1497
|
const transaction = await readTransactionJson<CoordinatorSessionTransactionV1>(transactionPath(paths, sessionId));
|
|
1498
|
+
options.signal?.throwIfAborted();
|
|
1423
1499
|
if (!transaction) return null;
|
|
1500
|
+
// Atomic WAL replacement permits lock-free snapshots. Normalize only in
|
|
1501
|
+
// memory; durable report migration belongs to explicit projection recovery.
|
|
1424
1502
|
assertTransaction(transaction, path.basename(paths.root), sessionId);
|
|
1425
|
-
normalizeOutbox(transaction);
|
|
1426
1503
|
return transaction;
|
|
1427
1504
|
}
|
|
1428
1505
|
|
|
1506
|
+
export async function repairLegacyReportIds(
|
|
1507
|
+
paths: CoordinatorStatePaths,
|
|
1508
|
+
sessionId: string,
|
|
1509
|
+
options: { signal?: AbortSignal } = {},
|
|
1510
|
+
): Promise<CoordinatorSessionTransactionV1 | null> {
|
|
1511
|
+
options.signal?.throwIfAborted();
|
|
1512
|
+
const file = transactionPath(paths, sessionId);
|
|
1513
|
+
const snapshot = await readTransactionJson<CoordinatorSessionTransactionV1>(file);
|
|
1514
|
+
options.signal?.throwIfAborted();
|
|
1515
|
+
if (!snapshot) return null;
|
|
1516
|
+
const hasLegacyReports = Object.keys(snapshot.canonical?.reports ?? {}).some(
|
|
1517
|
+
reportId => !COORDINATOR_REPORT_ID_PATTERN.test(reportId),
|
|
1518
|
+
);
|
|
1519
|
+
if (!hasLegacyReports) {
|
|
1520
|
+
// Atomic WAL replacement permits lock-free snapshots. In-memory schema
|
|
1521
|
+
// normalization alone must never turn an observation into a durable write.
|
|
1522
|
+
assertTransaction(snapshot, path.basename(paths.root), sessionId);
|
|
1523
|
+
return snapshot;
|
|
1524
|
+
}
|
|
1525
|
+
// Only legacy report rekeying needs a durable repair. Re-read under the lock
|
|
1526
|
+
// so a concurrent writer cannot be overwritten; never wait behind that writer
|
|
1527
|
+
// during namespace observation. The caller can skip a contended peer and retry.
|
|
1528
|
+
return await withFileLock(
|
|
1529
|
+
transactionLockPath(paths, sessionId),
|
|
1530
|
+
async () => {
|
|
1531
|
+
const transaction = await readTransactionJson<CoordinatorSessionTransactionV1>(file);
|
|
1532
|
+
if (!transaction) return null;
|
|
1533
|
+
const legacyReportIds = Object.keys(transaction.canonical?.reports ?? {}).filter(
|
|
1534
|
+
reportId => !COORDINATOR_REPORT_ID_PATTERN.test(reportId),
|
|
1535
|
+
);
|
|
1536
|
+
assertTransaction(transaction, path.basename(paths.root), sessionId);
|
|
1537
|
+
normalizeOutbox(transaction);
|
|
1538
|
+
if (legacyReportIds.some(reportId => !Object.hasOwn(transaction.canonical.reports, reportId))) {
|
|
1539
|
+
// Legacy report ids and their references are repaired as one durable WAL
|
|
1540
|
+
// transaction. Bump the revision so scheduler/projection consumers cannot
|
|
1541
|
+
// mistake the repaired image for the pre-migration one.
|
|
1542
|
+
transaction.revision += 1;
|
|
1543
|
+
transaction.projection.scheduler_pending_revision = transaction.revision;
|
|
1544
|
+
transaction.projection.scheduler_digest = digest(
|
|
1545
|
+
JSON.stringify({
|
|
1546
|
+
session_id: transaction.session_id,
|
|
1547
|
+
revision: transaction.revision,
|
|
1548
|
+
active: transaction.canonical.queue.active_turn_id !== null,
|
|
1549
|
+
state: transaction.canonical.desired_session_state,
|
|
1550
|
+
}),
|
|
1551
|
+
);
|
|
1552
|
+
assertTransaction(transaction, path.basename(paths.root), sessionId);
|
|
1553
|
+
options.signal?.throwIfAborted();
|
|
1554
|
+
await writeAtomic(file, transaction);
|
|
1555
|
+
}
|
|
1556
|
+
return transaction;
|
|
1557
|
+
},
|
|
1558
|
+
{ ...lockOptions(options.signal), retries: 1, retryDelayMs: 0 },
|
|
1559
|
+
);
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1429
1562
|
export async function withSessionTransaction<T>(
|
|
1430
1563
|
paths: CoordinatorStatePaths,
|
|
1431
1564
|
sessionId: string,
|
|
@@ -2303,10 +2436,19 @@ export async function enumeratePublicDeliveries(
|
|
|
2303
2436
|
after_order_key: afterOrderKey,
|
|
2304
2437
|
});
|
|
2305
2438
|
} catch (error) {
|
|
2306
|
-
if (
|
|
2439
|
+
if (error instanceof Error && (error.message === "resource_gone" || error.message === "state_corrupt")) {
|
|
2440
|
+
if (error.message === "state_corrupt")
|
|
2441
|
+
logger.warn("Coordinator delivery scan skipped corrupt WAL", { sessionId });
|
|
2442
|
+
} else throw error;
|
|
2443
|
+
}
|
|
2444
|
+
let endpointIncarnation: string | undefined;
|
|
2445
|
+
try {
|
|
2446
|
+
endpointIncarnation = (await readSessionTransaction(paths, sessionId))?.canonical.session.broker
|
|
2447
|
+
.endpoint_incarnation;
|
|
2448
|
+
} catch (error) {
|
|
2449
|
+
if (!(error instanceof Error) || error.message !== "state_corrupt") throw error;
|
|
2450
|
+
logger.warn("Coordinator delivery scan skipped corrupt WAL", { sessionId });
|
|
2307
2451
|
}
|
|
2308
|
-
const endpointIncarnation = (await readSessionTransaction(paths, sessionId))?.canonical.session.broker
|
|
2309
|
-
.endpoint_incarnation;
|
|
2310
2452
|
for (const claim of batch)
|
|
2311
2453
|
claims.push({
|
|
2312
2454
|
...claim,
|
|
@@ -2349,6 +2491,30 @@ export function compactTransaction(transaction: CoordinatorSessionTransactionV1,
|
|
|
2349
2491
|
);
|
|
2350
2492
|
const pinnedRequests = new Set<object>(pinnedPrompts);
|
|
2351
2493
|
const pinnedTurns = new Set(pinnedPrompts.map(request => request.coordinator_turn_id));
|
|
2494
|
+
// An active acknowledged turn proves its runtime receipt through the prompt
|
|
2495
|
+
// request that carries it. Dropping that request on retention leaves the WAL
|
|
2496
|
+
// failing its own `assertTransaction` receipt invariant on the next read, so
|
|
2497
|
+
// the session — and every namespace-wide scan that touches it — is corrupt
|
|
2498
|
+
// forever. Retention must never outrank an invariant the reader enforces.
|
|
2499
|
+
const activeTurnStatuses = new Set(["delivering", "active", "waiting_for_answer", "completing"]);
|
|
2500
|
+
const receiptBearingPromptIds = new Set(
|
|
2501
|
+
Object.entries(transaction.requests.prompts)
|
|
2502
|
+
.filter(([, request]) => {
|
|
2503
|
+
const turn = request.coordinator_turn_id
|
|
2504
|
+
? transaction.canonical.turns[request.coordinator_turn_id]
|
|
2505
|
+
: undefined;
|
|
2506
|
+
if (!turn || !activeTurnStatuses.has(turn.status)) return false;
|
|
2507
|
+
const delivery = turn.delivery as Record<string, unknown>;
|
|
2508
|
+
if (delivery.prompt_acknowledged !== true) return false;
|
|
2509
|
+
return (
|
|
2510
|
+
["accepted", "linked", "terminal", "completed"].includes(request.phase) &&
|
|
2511
|
+
request.runtime_receipt?.accepted === true &&
|
|
2512
|
+
request.runtime_receipt.command_id === delivery.runtime_command_id &&
|
|
2513
|
+
request.runtime_receipt.turn_id === delivery.runtime_turn_id
|
|
2514
|
+
);
|
|
2515
|
+
})
|
|
2516
|
+
.map(([id]) => id),
|
|
2517
|
+
);
|
|
2352
2518
|
for (const [id, event] of Object.entries(transaction.outbox))
|
|
2353
2519
|
if (
|
|
2354
2520
|
event.emitted &&
|
|
@@ -2362,6 +2528,7 @@ export function compactTransaction(transaction: CoordinatorSessionTransactionV1,
|
|
|
2362
2528
|
if (
|
|
2363
2529
|
request.phase === "completed" &&
|
|
2364
2530
|
!pinnedRequests.has(request) &&
|
|
2531
|
+
!receiptBearingPromptIds.has(id) &&
|
|
2365
2532
|
old(request.updated_at) &&
|
|
2366
2533
|
JSON.stringify(transaction.canonical).includes(id) === false
|
|
2367
2534
|
)
|