@gajae-code/coding-agent 0.15.6 → 0.16.1
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 +99 -0
- package/dist/types/async/job-manager.d.ts +32 -1
- package/dist/types/browser-backend.d.ts +12 -0
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/root-flags.d.ts +14 -0
- package/dist/types/cli/update-cli.d.ts +14 -0
- package/dist/types/cli.d.ts +7 -0
- package/dist/types/commands/launch.d.ts +10 -0
- package/dist/types/commands/sdk.d.ts +2 -0
- package/dist/types/config/autorouting-tier-map.d.ts +24 -0
- package/dist/types/config/billing-path.d.ts +25 -0
- package/dist/types/config/config-schema-version.d.ts +13 -1
- package/dist/types/config/file-lock.d.ts +5 -0
- package/dist/types/config/model-registry.d.ts +22 -0
- package/dist/types/config/settings-schema.d.ts +27 -8
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/coordinator-mcp/projection-scan.d.ts +8 -0
- package/dist/types/crash/record-loader.d.ts +15 -0
- package/dist/types/extensibility/extensions/types.d.ts +8 -3
- package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
- package/dist/types/gjc-runtime/launch-worktree.d.ts +25 -1
- package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +22 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +66 -1
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
- package/dist/types/main.d.ts +1 -0
- package/dist/types/master-mode/context.d.ts +21 -0
- package/dist/types/master-mode/first-request.d.ts +34 -0
- package/dist/types/master-mode/snapshot.d.ts +32 -0
- package/dist/types/modes/acp/acp-agent.d.ts +21 -21
- package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
- package/dist/types/modes/components/model-selector.d.ts +4 -0
- package/dist/types/modes/components/oauth-selector.d.ts +1 -0
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/components/sessions-dashboard.d.ts +2 -1
- package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
- package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
- package/dist/types/modes/components/status-line/segments.d.ts +10 -0
- package/dist/types/modes/interactive-mode.d.ts +16 -2
- package/dist/types/modes/print-mode.d.ts +17 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/modes/ui-language.d.ts +7 -1
- package/dist/types/sdk/broker/broker.d.ts +79 -2
- package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
- package/dist/types/sdk/broker/ensure.d.ts +3 -0
- package/dist/types/sdk/broker/index.d.ts +3 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +6 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +40 -0
- package/dist/types/sdk/broker/master-capability.d.ts +10 -0
- package/dist/types/sdk/broker/session-index.d.ts +40 -11
- package/dist/types/sdk/broker/session-scope.d.ts +72 -0
- package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
- package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
- package/dist/types/sdk/broker/startup-budget.d.ts +52 -0
- package/dist/types/sdk/broker/state-version.d.ts +21 -1
- package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
- package/dist/types/sdk/bus/discord-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/index.d.ts +9 -1
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +1 -1
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +1 -1
- package/dist/types/sdk/bus/slack-conversation.d.ts +1 -1
- package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/cli/index.d.ts +1 -0
- package/dist/types/sdk/cli/master-cli.d.ts +63 -0
- package/dist/types/sdk/cli/rows.d.ts +12 -5
- package/dist/types/sdk/cli/session-cli.d.ts +20 -3
- package/dist/types/sdk/client/client.d.ts +3 -2
- package/dist/types/sdk/host/control/dispatch.d.ts +10 -0
- package/dist/types/sdk/host/control/index.d.ts +1 -1
- package/dist/types/sdk/host/control/operations.d.ts +2 -0
- package/dist/types/sdk/host/control/runtime-gate.d.ts +21 -7
- package/dist/types/sdk/host/events.d.ts +1 -0
- package/dist/types/sdk/host/host.d.ts +10 -0
- package/dist/types/sdk/host/query/cursor.d.ts +4 -2
- package/dist/types/sdk/host/query/handlers.d.ts +1 -0
- package/dist/types/sdk/host/query/revision-store.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +70 -17
- package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
- package/dist/types/sdk/lifecycle/service.d.ts +15 -3
- package/dist/types/sdk/lifecycle-session.d.ts +3 -1
- package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -5
- package/dist/types/sdk/receipt-state.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +6 -0
- package/dist/types/sdk/session.d.ts +16 -0
- package/dist/types/sdk/startup-capability.d.ts +2 -1
- package/dist/types/sdk/turn-result.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +52 -7
- package/dist/types/session/credential-store-errors.d.ts +1 -0
- package/dist/types/session/fold-coordinator.d.ts +29 -2
- package/dist/types/session/session-manager.d.ts +79 -2
- package/dist/types/session/yield-queue.d.ts +5 -4
- package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
- package/dist/types/tools/ask.d.ts +1 -0
- package/dist/types/tools/bash.d.ts +16 -0
- package/dist/types/tools/browser/launch.d.ts +10 -0
- package/dist/types/tools/browser/registry.d.ts +8 -0
- package/dist/types/tools/gh.d.ts +8 -0
- package/dist/types/tools/index.d.ts +10 -10
- package/dist/types/tools/job.d.ts +3 -1
- package/dist/types/utils/external-editor.d.ts +7 -0
- package/examples/extensions/README.md +1 -1
- package/examples/extensions/pirate.ts +7 -4
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +13 -1
- package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
- package/src/async/job-manager.ts +107 -9
- package/src/browser-backend.ts +29 -0
- package/src/cli/accounts-cli.ts +9 -5
- package/src/cli/args.ts +17 -0
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/plugin-cli.ts +29 -0
- package/src/cli/root-flags.ts +5 -0
- package/src/cli/update-cli.ts +140 -23
- package/src/commands/launch.ts +86 -18
- package/src/commands/sdk.ts +135 -28
- package/src/config/autorouting-tier-map.ts +12 -0
- package/src/config/billing-path.ts +42 -0
- package/src/config/config-schema-version.ts +18 -1
- package/src/config/file-lock.ts +158 -83
- package/src/config/model-preset-registry.ts +178 -23
- package/src/config/model-profiles.ts +5 -5
- package/src/config/model-registry.ts +291 -42
- package/src/config/model-resolver.ts +5 -0
- package/src/config/settings-schema.ts +27 -8
- package/src/config/settings.ts +51 -15
- package/src/coordinator-mcp/projection-scan.ts +27 -6
- package/src/coordinator-mcp/question-state.ts +9 -11
- package/src/coordinator-mcp/server.ts +115 -21
- package/src/crash/index-store.ts +2 -0
- package/src/crash/record-loader.ts +10 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
- package/src/extensibility/extensions/runner.ts +27 -0
- package/src/extensibility/extensions/types.ts +8 -3
- package/src/extensibility/gjc-plugins/installer.ts +51 -11
- package/src/extensibility/runtime-skill-discovery.ts +110 -3
- package/src/extensibility/skills.ts +22 -12
- package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
- package/src/gjc-runtime/launch-worktree.ts +285 -4
- package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
- package/src/gjc-runtime/session-state-lock.ts +213 -14
- package/src/gjc-runtime/session-state-sidecar.ts +938 -39
- package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
- package/src/gjc-runtime/tmux-owner-isolation.ts +18 -3
- package/src/gjc-runtime/tmux-sessions.ts +252 -13
- package/src/hooks/skill-state.ts +6 -0
- package/src/internal-urls/docs-index.generated.ts +15 -15
- package/src/internal-urls/issue-pr-protocol.ts +4 -2
- package/src/main.ts +61 -3
- package/src/master-mode/context.ts +50 -0
- package/src/master-mode/first-request.ts +65 -0
- package/src/master-mode/snapshot.ts +205 -0
- package/src/modes/acp/acp-agent.ts +610 -161
- package/src/modes/components/footer.ts +4 -2
- package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
- package/src/modes/components/model-selector.ts +76 -8
- package/src/modes/components/oauth-selector.ts +6 -1
- package/src/modes/components/session-selector.ts +23 -15
- package/src/modes/components/sessions-dashboard.ts +8 -5
- package/src/modes/components/settings-selector.ts +1 -0
- package/src/modes/components/status-line/model-name.ts +53 -0
- package/src/modes/components/status-line/priority-row.ts +107 -0
- package/src/modes/components/status-line/segments.ts +23 -9
- package/src/modes/components/tool-status-header.ts +151 -15
- package/src/modes/controllers/command-controller.ts +1 -1
- package/src/modes/controllers/event-controller.ts +30 -4
- package/src/modes/controllers/extension-ui-controller.ts +13 -13
- package/src/modes/controllers/input-controller.ts +30 -2
- package/src/modes/controllers/selector-controller.ts +200 -98
- package/src/modes/interactive-mode.ts +36 -12
- package/src/modes/print-mode.ts +64 -0
- package/src/modes/prompt-action-autocomplete.ts +30 -4
- package/src/modes/runtime-init.ts +12 -9
- package/src/modes/theme/theme.ts +5 -0
- package/src/modes/types.ts +2 -0
- package/src/modes/ui-language.ts +49 -2
- package/src/modes/utils/ui-helpers.ts +22 -3
- package/src/prompts/agent-fragments/browser-aside.md +22 -0
- package/src/prompts/system/master-mode.md +28 -0
- package/src/prompts/tools/ask.md +1 -1
- package/src/prompts/tools/bash.md +1 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk/acp/adapter.ts +5 -0
- package/src/sdk/broker/broker.ts +1646 -27
- package/src/sdk/broker/endpoint-authority.ts +86 -0
- package/src/sdk/broker/ensure.ts +81 -3
- package/src/sdk/broker/index.ts +3 -0
- package/src/sdk/broker/lifecycle-ledger.ts +6 -0
- package/src/sdk/broker/lifecycle.ts +440 -75
- package/src/sdk/broker/master-capability.ts +232 -0
- package/src/sdk/broker/session-index.ts +121 -17
- package/src/sdk/broker/session-scope.ts +272 -0
- package/src/sdk/broker/spawn-authority.ts +1031 -0
- package/src/sdk/broker/spawn-substrate.ts +591 -0
- package/src/sdk/broker/startup-budget.ts +144 -2
- package/src/sdk/broker/state-version.ts +43 -14
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/broker/worktree-occupancy.ts +90 -0
- package/src/sdk/bus/chat-daemon-control.ts +5 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +136 -18
- package/src/sdk/bus/discord-daemon.ts +50 -10
- package/src/sdk/bus/index.ts +84 -7
- package/src/sdk/bus/kind-aware-reconciliation.ts +296 -48
- package/src/sdk/bus/lifecycle-commands.ts +4 -0
- package/src/sdk/bus/notification-service.ts +22 -5
- package/src/sdk/bus/prompt-reconciliation.ts +7 -2
- package/src/sdk/bus/reconciliation-store.ts +55 -31
- package/src/sdk/bus/slack-conversation.ts +1 -1
- package/src/sdk/bus/slack-daemon.ts +72 -38
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/cli/index.ts +1 -0
- package/src/sdk/cli/master-cli.ts +259 -0
- package/src/sdk/cli/rows.ts +41 -15
- package/src/sdk/cli/session-cli.ts +297 -26
- package/src/sdk/client/client.ts +14 -8
- package/src/sdk/host/control/dispatch.ts +101 -40
- package/src/sdk/host/control/index.ts +2 -0
- package/src/sdk/host/control/operations.ts +2 -0
- package/src/sdk/host/control/runtime-gate.ts +203 -11
- package/src/sdk/host/events.ts +1 -0
- package/src/sdk/host/host.ts +29 -2
- package/src/sdk/host/query/cursor.ts +52 -7
- package/src/sdk/host/query/handlers.ts +33 -12
- package/src/sdk/host/query/revision-store.ts +12 -0
- package/src/sdk/host/session-runtime.test.ts +673 -14
- package/src/sdk/host/session-runtime.ts +674 -103
- package/src/sdk/host/surface-policy.ts +3 -3
- package/src/sdk/lifecycle/broker-client.ts +28 -0
- package/src/sdk/lifecycle/service.ts +279 -20
- package/src/sdk/lifecycle-session.ts +17 -4
- package/src/sdk/prompt-deadline-manager.ts +2 -1
- package/src/sdk/protocol/operation-inventory.generated.json +105 -3
- package/src/sdk/protocol/operation-registry.ts +14 -4
- package/src/sdk/receipt-state.ts +10 -0
- package/src/sdk/router/session-router.ts +219 -118
- package/src/sdk/session.ts +416 -195
- package/src/sdk/startup-capability.ts +1 -0
- package/src/sdk/turn-result.ts +22 -0
- package/src/session/agent-session.ts +1628 -318
- package/src/session/blob-store.ts +15 -5
- package/src/session/credential-store-errors.ts +2 -0
- package/src/session/fold-coordinator.ts +40 -3
- package/src/session/internal/managed-session-storage.ts +153 -78
- package/src/session/session-manager.ts +850 -97
- package/src/session/yield-queue.ts +24 -10
- package/src/setup/frictionless-onboarding.ts +2 -0
- package/src/slash-commands/builtin-registry.ts +41 -0
- package/src/task/executor.ts +9 -4
- package/src/telemetry/events.ts +1138 -12
- package/src/tools/ask-contract.test.ts +64 -0
- package/src/tools/ask-contract.ts +36 -1
- package/src/tools/ask.ts +10 -8
- package/src/tools/bash-interactive.ts +11 -2
- package/src/tools/bash.ts +412 -75
- package/src/tools/browser/launch.ts +37 -0
- package/src/tools/browser/registry.ts +99 -9
- package/src/tools/cron.ts +6 -4
- package/src/tools/descriptors.test.ts +15 -0
- package/src/tools/descriptors.ts +5 -1
- package/src/tools/gh.ts +104 -48
- package/src/tools/github-cache.ts +2 -2
- package/src/tools/index.ts +10 -6
- package/src/tools/job.ts +4 -1
- package/src/tools/monitor.ts +12 -9
- package/src/tools/resolve.ts +30 -10
- package/src/tools/tool-catalog.generated.ts +2 -2
- package/src/utils/external-editor.ts +72 -1
package/src/config/settings.ts
CHANGED
|
@@ -70,6 +70,7 @@ import {
|
|
|
70
70
|
type GroupPrefix,
|
|
71
71
|
type GroupTypeMap,
|
|
72
72
|
getDefault,
|
|
73
|
+
normalizeConfigSchemaVersion,
|
|
73
74
|
reconcileSettingsSchema,
|
|
74
75
|
SETTINGS_SCHEMA,
|
|
75
76
|
type SettingPath,
|
|
@@ -748,6 +749,11 @@ export class Settings implements NotificationSettingsReader {
|
|
|
748
749
|
return !this.#persist || !this.#hasRecoveredConfigSyntax;
|
|
749
750
|
}
|
|
750
751
|
|
|
752
|
+
/** Whether this instance can publish a mutation to a durable config file. */
|
|
753
|
+
canPersistDurableConfig(): boolean {
|
|
754
|
+
return this.#persist && !this.#hasRecoveredConfigSyntax;
|
|
755
|
+
}
|
|
756
|
+
|
|
751
757
|
/**
|
|
752
758
|
* Set a setting value (sync).
|
|
753
759
|
* Updates global settings and reserves its background persistence slot before
|
|
@@ -1520,15 +1526,19 @@ export class Settings implements NotificationSettingsReader {
|
|
|
1520
1526
|
this.#hasInvalidNotificationGlobal = true;
|
|
1521
1527
|
}
|
|
1522
1528
|
}
|
|
1529
|
+
// ONE normalized marker feeds every version decision (future-schema,
|
|
1530
|
+
// migration-pending, and the ordered registry in #migrateRawSettings): a
|
|
1531
|
+
// malformed value must never be read as a future schema here — which would
|
|
1532
|
+
// skip legacy migration entirely — and as unmigrated by the registry.
|
|
1533
|
+
const configSchemaVersion = normalizeConfigSchemaVersion(parsedRaw.configSchemaVersion);
|
|
1523
1534
|
this.#futureSchemaVersion =
|
|
1524
1535
|
filePath === this.#configPath &&
|
|
1525
|
-
|
|
1526
|
-
|
|
1536
|
+
configSchemaVersion !== undefined &&
|
|
1537
|
+
configSchemaVersion > CONFIG_SCHEMA_VERSION;
|
|
1527
1538
|
|
|
1528
|
-
const configSchemaVersion = parsedRaw.configSchemaVersion;
|
|
1529
1539
|
if (
|
|
1530
1540
|
filePath === this.#configPath &&
|
|
1531
|
-
(
|
|
1541
|
+
(configSchemaVersion === undefined || configSchemaVersion < CONFIG_SCHEMA_VERSION)
|
|
1532
1542
|
) {
|
|
1533
1543
|
this.#schemaMigrationPending = true;
|
|
1534
1544
|
}
|
|
@@ -2082,7 +2092,7 @@ export class Settings implements NotificationSettingsReader {
|
|
|
2082
2092
|
return false;
|
|
2083
2093
|
}
|
|
2084
2094
|
const schemaVersion = (root as Record<string, unknown> | null | undefined)?.configSchemaVersion;
|
|
2085
|
-
if (
|
|
2095
|
+
if ((normalizeConfigSchemaVersion(schemaVersion) ?? 0) > CONFIG_SCHEMA_VERSION) {
|
|
2086
2096
|
this.#warnLegacyFallbackMigration(
|
|
2087
2097
|
`Settings: database workflow migration skipped: ${this.#configPath} is a future config schema (configSchemaVersion ${schemaVersion} > ${CONFIG_SCHEMA_VERSION}); leaving legacy rows for the next load`,
|
|
2088
2098
|
);
|
|
@@ -2333,7 +2343,7 @@ export class Settings implements NotificationSettingsReader {
|
|
|
2333
2343
|
// sets #futureSchemaVersion, so it must check the target schema
|
|
2334
2344
|
// itself and never patch it or consume the legacy source.
|
|
2335
2345
|
const targetSchemaVersion = (tx.root as Record<string, unknown> | null | undefined)?.configSchemaVersion;
|
|
2336
|
-
if (
|
|
2346
|
+
if ((normalizeConfigSchemaVersion(targetSchemaVersion) ?? 0) > CONFIG_SCHEMA_VERSION) {
|
|
2337
2347
|
this.#warnLegacyFallbackMigration(
|
|
2338
2348
|
`Settings: config-root workflow migration skipped: ${target} is a future config schema (configSchemaVersion ${targetSchemaVersion} > ${CONFIG_SCHEMA_VERSION})`,
|
|
2339
2349
|
);
|
|
@@ -3575,7 +3585,7 @@ export class Settings implements NotificationSettingsReader {
|
|
|
3575
3585
|
// read-only across Settings; never patch it (mirrors the config-root
|
|
3576
3586
|
// migration guard) so a future schema cannot inherit stale keys.
|
|
3577
3587
|
const targetSchemaVersion = (tx.root as Record<string, unknown> | null | undefined)?.configSchemaVersion;
|
|
3578
|
-
if (
|
|
3588
|
+
if ((normalizeConfigSchemaVersion(targetSchemaVersion) ?? 0) > CONFIG_SCHEMA_VERSION) {
|
|
3579
3589
|
this.#warnLegacyFallbackMigration(
|
|
3580
3590
|
`Settings: project workflow migration skipped: ${target} is a future config schema (configSchemaVersion ${targetSchemaVersion} > ${CONFIG_SCHEMA_VERSION})`,
|
|
3581
3591
|
);
|
|
@@ -4092,7 +4102,7 @@ export class Settings implements NotificationSettingsReader {
|
|
|
4092
4102
|
markerPath = `${source}.fallback-invalid`,
|
|
4093
4103
|
): Promise<void> {
|
|
4094
4104
|
const targetSchemaVersion = (tx.root as Record<string, unknown> | null | undefined)?.configSchemaVersion;
|
|
4095
|
-
if (
|
|
4105
|
+
if ((normalizeConfigSchemaVersion(targetSchemaVersion) ?? 0) > CONFIG_SCHEMA_VERSION) return;
|
|
4096
4106
|
if (tx.root !== undefined && (tx.root === null || typeof tx.root !== "object" || Array.isArray(tx.root))) {
|
|
4097
4107
|
return;
|
|
4098
4108
|
}
|
|
@@ -4821,7 +4831,7 @@ export class Settings implements NotificationSettingsReader {
|
|
|
4821
4831
|
// guard) would rewrite configuration an older binary treats as
|
|
4822
4832
|
// read-only.
|
|
4823
4833
|
const targetSchemaVersion = (tx.root as Record<string, unknown> | null | undefined)?.configSchemaVersion;
|
|
4824
|
-
if (
|
|
4834
|
+
if ((normalizeConfigSchemaVersion(targetSchemaVersion) ?? 0) > CONFIG_SCHEMA_VERSION) {
|
|
4825
4835
|
return;
|
|
4826
4836
|
}
|
|
4827
4837
|
// Unset only while the target still matches the migration's fallback
|
|
@@ -5399,11 +5409,41 @@ export class Settings implements NotificationSettingsReader {
|
|
|
5399
5409
|
|
|
5400
5410
|
/** Apply registered schema migrations once, using configSchemaVersion as the durable marker. */
|
|
5401
5411
|
#migrateRawSettings(raw: RawSettings): RawSettings {
|
|
5402
|
-
const
|
|
5412
|
+
const rawVersion = raw.configSchemaVersion;
|
|
5413
|
+
const configuredVersion = normalizeConfigSchemaVersion(rawVersion);
|
|
5414
|
+
if (rawVersion !== undefined && rawVersion !== configuredVersion) {
|
|
5415
|
+
logger.warn("Settings: config has a malformed configSchemaVersion; migrating from the normalized version", {
|
|
5416
|
+
configSchemaVersion: String(rawVersion),
|
|
5417
|
+
normalized: String(configuredVersion),
|
|
5418
|
+
});
|
|
5419
|
+
}
|
|
5403
5420
|
if (configuredVersion === CONFIG_SCHEMA_VERSION) return raw;
|
|
5404
5421
|
if (typeof configuredVersion === "number" && configuredVersion > CONFIG_SCHEMA_VERSION) return raw;
|
|
5405
5422
|
|
|
5406
|
-
//
|
|
5423
|
+
// Ordered migration registry. Each step runs ONLY for files below its
|
|
5424
|
+
// target version: the v0 steps are not all idempotent (ask.timeout's
|
|
5425
|
+
// milliseconds-to-seconds conversion would re-divide a valid v1 value),
|
|
5426
|
+
// so a v1 file must skip straight to the v2 step.
|
|
5427
|
+
const fromVersion = typeof configuredVersion === "number" ? configuredVersion : 0;
|
|
5428
|
+
if (fromVersion < 1) this.#migrateRawSettingsV0ToV1(raw);
|
|
5429
|
+
// v1 -> v2: interruptMode -> toolInterruptPolicy.
|
|
5430
|
+
if (fromVersion < 2 && "interruptMode" in raw) {
|
|
5431
|
+
if (!("toolInterruptPolicy" in raw)) {
|
|
5432
|
+
const legacy = raw.interruptMode;
|
|
5433
|
+
if (legacy === "wait") raw.toolInterruptPolicy = "finish_tools";
|
|
5434
|
+
else if (legacy === "immediate") raw.toolInterruptPolicy = "abort_tools";
|
|
5435
|
+
// Any other value is malformed: drop it and let the schema default apply.
|
|
5436
|
+
}
|
|
5437
|
+
delete raw.interruptMode;
|
|
5438
|
+
}
|
|
5439
|
+
|
|
5440
|
+
raw.configSchemaVersion = CONFIG_SCHEMA_VERSION;
|
|
5441
|
+
|
|
5442
|
+
return raw;
|
|
5443
|
+
}
|
|
5444
|
+
|
|
5445
|
+
/** v0 -> v1 transforms. Runs only for files that predate schema version 1. */
|
|
5446
|
+
#migrateRawSettingsV0ToV1(raw: RawSettings): void {
|
|
5407
5447
|
// queueMode -> steeringMode
|
|
5408
5448
|
normalizeSessionDirectoryMigration(raw);
|
|
5409
5449
|
if ("queueMode" in raw && !("steeringMode" in raw)) {
|
|
@@ -5568,10 +5608,6 @@ export class Settings implements NotificationSettingsReader {
|
|
|
5568
5608
|
delete providersObj.imageCustomKeyEnv;
|
|
5569
5609
|
}
|
|
5570
5610
|
}
|
|
5571
|
-
|
|
5572
|
-
raw.configSchemaVersion = CONFIG_SCHEMA_VERSION;
|
|
5573
|
-
|
|
5574
|
-
return raw;
|
|
5575
5611
|
}
|
|
5576
5612
|
|
|
5577
5613
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -36,6 +36,8 @@ function projectionPlatform(): NodeJS.Platform {
|
|
|
36
36
|
|
|
37
37
|
/** Post-filter parse-candidate cap. Exhaustion returns an explicit incomplete result. */
|
|
38
38
|
export const COORDINATOR_JSON_SCAN_CAP = 10_000;
|
|
39
|
+
/** Number of fresh authoritative attempts allowed for a scan that observed churn. */
|
|
40
|
+
export const COORDINATOR_JSON_SCAN_RETRY_ATTEMPTS = 3;
|
|
39
41
|
|
|
40
42
|
export interface ProjectionScanStat {
|
|
41
43
|
size: number | bigint;
|
|
@@ -193,10 +195,10 @@ function trimProjectionRootPath(dir: string): string {
|
|
|
193
195
|
}
|
|
194
196
|
|
|
195
197
|
/**
|
|
196
|
-
*
|
|
197
|
-
* authoritative by checking its identity
|
|
198
|
+
* Node exposes no descriptor-relative `openat` binding on Windows or macOS. Keep the
|
|
199
|
+
* root pathname authoritative by checking its identity around every path operation.
|
|
198
200
|
*/
|
|
199
|
-
async function
|
|
201
|
+
async function openProjectionDirectoryPathBracketed(dir: string): Promise<ProjectionScanDirectory> {
|
|
200
202
|
const rootPath = trimProjectionRootPath(dir);
|
|
201
203
|
let opened: import("node:fs").BigIntStats;
|
|
202
204
|
try {
|
|
@@ -224,8 +226,8 @@ async function openProjectionDirectoryWindowsSafe(dir: string): Promise<Projecti
|
|
|
224
226
|
};
|
|
225
227
|
|
|
226
228
|
const entryPath = (entry: string): string => {
|
|
227
|
-
// Directory entries come from readdir and are therefore single components
|
|
228
|
-
//
|
|
229
|
+
// Directory entries come from readdir and are therefore single components.
|
|
230
|
+
// Keep the invariant explicit so an injected or corrupted entry can
|
|
229
231
|
// never turn a child operation into mutable pathname traversal.
|
|
230
232
|
if (
|
|
231
233
|
entry.length === 0 ||
|
|
@@ -288,7 +290,7 @@ async function openProjectionDirectoryWindowsSafe(dir: string): Promise<Projecti
|
|
|
288
290
|
*/
|
|
289
291
|
async function openProjectionDirectorySafe(dir: string): Promise<ProjectionScanDirectory> {
|
|
290
292
|
const platform = projectionPlatform();
|
|
291
|
-
if (platform === "win32") return
|
|
293
|
+
if (platform === "win32" || platform === "darwin") return openProjectionDirectoryPathBracketed(dir);
|
|
292
294
|
if (platform !== "linux") throw new ProjectionScanUnsupportedError();
|
|
293
295
|
const noFollow = fs.constants.O_NOFOLLOW;
|
|
294
296
|
const nonBlock = fs.constants.O_NONBLOCK;
|
|
@@ -569,3 +571,22 @@ export async function listCoordinatorJsonFiles(
|
|
|
569
571
|
await authority?.close().catch(() => undefined);
|
|
570
572
|
}
|
|
571
573
|
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Retry only scans invalidated by observed directory/file churn. A candidate cap,
|
|
577
|
+
* unsupported authority, parse error, or any other failure remains fail-closed.
|
|
578
|
+
* Each attempt acquires a new authority, so no partial result crosses attempts.
|
|
579
|
+
*/
|
|
580
|
+
export async function listCoordinatorJsonFilesWithRetry(
|
|
581
|
+
dir: string,
|
|
582
|
+
io: ProjectionScanFs = defaultFs,
|
|
583
|
+
cap: number = COORDINATOR_JSON_SCAN_CAP,
|
|
584
|
+
maxAttempts: number = COORDINATOR_JSON_SCAN_RETRY_ATTEMPTS,
|
|
585
|
+
): Promise<ProjectionScanResult> {
|
|
586
|
+
const attempts = Math.max(1, Math.floor(maxAttempts));
|
|
587
|
+
let scan = await listCoordinatorJsonFiles(dir, io, cap);
|
|
588
|
+
for (let attempt = 1; attempt < attempts && scan.incomplete && scan.raced > 0; attempt++) {
|
|
589
|
+
scan = await listCoordinatorJsonFiles(dir, io, cap);
|
|
590
|
+
}
|
|
591
|
+
return scan;
|
|
592
|
+
}
|
|
@@ -1029,7 +1029,7 @@ function assertTransaction(transaction: CoordinatorSessionTransactionV1, namespa
|
|
|
1029
1029
|
const rejectedHistorical = request.phase === "rejected";
|
|
1030
1030
|
if (
|
|
1031
1031
|
rejectedHistorical
|
|
1032
|
-
?
|
|
1032
|
+
? request.safe_receipt?.status !== "rejected"
|
|
1033
1033
|
: question.answer_request_id !== request.request_id ||
|
|
1034
1034
|
question.claim_fence_epoch !== request.claim_fence_epoch
|
|
1035
1035
|
)
|
|
@@ -1083,7 +1083,7 @@ function assertTransaction(transaction: CoordinatorSessionTransactionV1, namespa
|
|
|
1083
1083
|
event.transaction_revision > transaction.revision ||
|
|
1084
1084
|
typeof event.kind !== "string" ||
|
|
1085
1085
|
event.kind.length === 0 ||
|
|
1086
|
-
!
|
|
1086
|
+
!isOutboxEntity(event.entity) ||
|
|
1087
1087
|
typeof event.entity_id !== "string" ||
|
|
1088
1088
|
event.entity_id.length === 0 ||
|
|
1089
1089
|
!isRecord(event.payload) ||
|
|
@@ -1296,7 +1296,7 @@ export async function listCanonicalActiveSessions(
|
|
|
1296
1296
|
paths.registryLock,
|
|
1297
1297
|
async () => {
|
|
1298
1298
|
const registry = await readJson<NamespaceRegistryV1>(paths.registry);
|
|
1299
|
-
if (
|
|
1299
|
+
if (registry?.schema_version !== 1 || registry.namespace_id !== path.basename(paths.root))
|
|
1300
1300
|
throw new Error("state_corrupt");
|
|
1301
1301
|
return Object.values(registry.roster ?? {})
|
|
1302
1302
|
.filter(entry => entry.active || entry.dirty)
|
|
@@ -1333,7 +1333,7 @@ export async function readDeliveryDiscoveryCursor(
|
|
|
1333
1333
|
paths.registryLock,
|
|
1334
1334
|
async () => {
|
|
1335
1335
|
const registry = await readJson<NamespaceRegistryV1>(paths.registry);
|
|
1336
|
-
if (
|
|
1336
|
+
if (registry?.schema_version !== 1 || registry.namespace_id !== path.basename(paths.root))
|
|
1337
1337
|
throw new Error("state_corrupt");
|
|
1338
1338
|
return registry.delivery_discovery_cursor ?? "";
|
|
1339
1339
|
},
|
|
@@ -1660,7 +1660,7 @@ export async function startCreationRemote(
|
|
|
1660
1660
|
): Promise<CreationRequestV1> {
|
|
1661
1661
|
return await withNamespaceRegistry(paths, async registry => {
|
|
1662
1662
|
const request = registry.creations[keyDigest];
|
|
1663
|
-
if (!request
|
|
1663
|
+
if (!request?.sidecar_verifier) throw new Error("state_corrupt");
|
|
1664
1664
|
if (
|
|
1665
1665
|
request.phase === "claimed" &&
|
|
1666
1666
|
(request.sidecar_verifier.key_id !== expectedVerifier.key_id ||
|
|
@@ -1687,8 +1687,7 @@ export async function reconcileCreationRemoteVerifier(
|
|
|
1687
1687
|
): Promise<CreationRequestV1> {
|
|
1688
1688
|
return await withNamespaceRegistry(paths, async registry => {
|
|
1689
1689
|
const request = registry.creations[keyDigest];
|
|
1690
|
-
if (!request
|
|
1691
|
-
throw new Error("terminal_uncertain");
|
|
1690
|
+
if (!request?.sidecar_verifier || request.phase !== "remote_started") throw new Error("terminal_uncertain");
|
|
1692
1691
|
if (!/^[a-f0-9]{64}$/.test(usedKeyId)) throw new Error("terminal_uncertain");
|
|
1693
1692
|
if (usedKeyId === candidate.key_id) request.sidecar_verifier = candidate;
|
|
1694
1693
|
else if (usedKeyId !== request.sidecar_verifier.key_id) throw new Error("terminal_uncertain");
|
|
@@ -2241,7 +2240,7 @@ export async function enumeratePublicDeliveries(
|
|
|
2241
2240
|
paths.registryLock,
|
|
2242
2241
|
async () => {
|
|
2243
2242
|
const registry = await readJson<NamespaceRegistryV1>(paths.registry);
|
|
2244
|
-
if (
|
|
2243
|
+
if (registry?.schema_version !== 1 || registry.namespace_id !== path.basename(paths.root))
|
|
2245
2244
|
throw new Error("state_corrupt");
|
|
2246
2245
|
return [...new Set([...Object.keys(registry.roster ?? {}), ...Object.keys(registry.retained_sessions ?? {})])]
|
|
2247
2246
|
.filter(name => COORDINATOR_SESSION_ID_PATTERN.test(name))
|
|
@@ -2526,7 +2525,7 @@ export async function replaceCreationRetirementIntent(
|
|
|
2526
2525
|
if (request.phase !== "remote_started" && request.phase !== "uncertain" && request.phase !== "retired")
|
|
2527
2526
|
throw new Error("retire_not_allowed");
|
|
2528
2527
|
const staged = request.retirement_intent;
|
|
2529
|
-
if (
|
|
2528
|
+
if (staged?.phase !== "intent" || staged.broker_proof) throw new Error("state_corrupt");
|
|
2530
2529
|
if (staged.retirement_key_digest !== retirementKeyDigest) throw new Error("idempotency_conflict");
|
|
2531
2530
|
assertCreationRetirementProofMatches(request, proof, false);
|
|
2532
2531
|
staged.phase = "pre_effect_rejected";
|
|
@@ -2716,8 +2715,7 @@ export async function removeSessionTransaction(
|
|
|
2716
2715
|
// exporter lease expires instead of deleting an undelivered event.
|
|
2717
2716
|
if (Object.values(transaction.outbox).some(event => event.public_delivery.state !== "acknowledged"))
|
|
2718
2717
|
return false;
|
|
2719
|
-
await
|
|
2720
|
-
await syncCoordinatorDirectory(path.dirname(file));
|
|
2718
|
+
await removeCoordinatorStateFile(file);
|
|
2721
2719
|
delete registry.roster?.[sessionId];
|
|
2722
2720
|
delete registry.retained_sessions?.[sessionId];
|
|
2723
2721
|
return true;
|
|
@@ -83,7 +83,7 @@ import {
|
|
|
83
83
|
requireCoordinatorMutation,
|
|
84
84
|
safeOpenCoordinatorArtifact,
|
|
85
85
|
} from "./policy";
|
|
86
|
-
import {
|
|
86
|
+
import { listCoordinatorJsonFilesWithRetry } from "./projection-scan";
|
|
87
87
|
import {
|
|
88
88
|
answerBindingMatches,
|
|
89
89
|
buildCoordinatorAskAnswerSchema,
|
|
@@ -484,6 +484,18 @@ interface CoordinatorEventInput {
|
|
|
484
484
|
}
|
|
485
485
|
|
|
486
486
|
const UNOBSERVED_COMPENSATION_CODE = "broker_compensation_unobserved";
|
|
487
|
+
const OBSERVED_BROKER_FAILURE_CODES = new Set([
|
|
488
|
+
"worktree_preparation_timeout",
|
|
489
|
+
"dependency_preparation_timeout",
|
|
490
|
+
"readiness_timeout",
|
|
491
|
+
"spawn_failed",
|
|
492
|
+
"worktree_in_use",
|
|
493
|
+
"startup_admission_timeout",
|
|
494
|
+
"startup_admission_refused",
|
|
495
|
+
"incarnation_unavailable",
|
|
496
|
+
"invalid_input",
|
|
497
|
+
"broker_request_unavailable",
|
|
498
|
+
]);
|
|
487
499
|
|
|
488
500
|
const MISSING_FINAL_RESPONSE_ADVISORY = "completion_missing_final_response";
|
|
489
501
|
const PROMPT_ACK_TIMEOUT_REASON = "runtime_prompt_ack_timeout";
|
|
@@ -835,7 +847,7 @@ function toolSchema(name: CoordinatorToolName): {
|
|
|
835
847
|
properties: {
|
|
836
848
|
after_seq: { type: "integer", minimum: 0 },
|
|
837
849
|
session_id: sessionId,
|
|
838
|
-
event_types: { type: "array", items: { type: "string" } },
|
|
850
|
+
event_types: { type: "array", items: { type: "string", enum: [...COORDINATOR_EVENT_KINDS] } },
|
|
839
851
|
timeout_ms: {
|
|
840
852
|
type: "number",
|
|
841
853
|
description: "Bounded event long-poll timeout in milliseconds, capped at 30 seconds.",
|
|
@@ -1216,6 +1228,14 @@ const PUBLIC_ERROR_MESSAGES: Record<string, string> = {
|
|
|
1216
1228
|
broker_discovery_unavailable: "SDK broker discovery is unavailable.",
|
|
1217
1229
|
broker_request_unavailable: "SDK broker request is unavailable.",
|
|
1218
1230
|
broker_transport_unavailable: "SDK broker transport is unavailable.",
|
|
1231
|
+
startup_admission_timeout: "SDK host startup was not admitted before the queue wait cutoff.",
|
|
1232
|
+
startup_admission_refused: "SDK host startup was refused because the broker no longer owns the session root.",
|
|
1233
|
+
worktree_in_use: "The requested worktree is already held by another live session.",
|
|
1234
|
+
worktree_preparation_timeout: "Worktree preparation exceeded its deadline before the session host was spawned.",
|
|
1235
|
+
dependency_preparation_timeout: "Dependency preparation exceeded its deadline before the session host was spawned.",
|
|
1236
|
+
spawn_failed: "Session host could not be spawned.",
|
|
1237
|
+
readiness_timeout: "Session did not become ready before the SDK readiness deadline.",
|
|
1238
|
+
incarnation_unavailable: "OS process incarnation authority is unavailable.",
|
|
1219
1239
|
session_closing: "Coordinator session is closing.",
|
|
1220
1240
|
session_unavailable: "Coordinator session is unavailable.",
|
|
1221
1241
|
session_not_activatable: "Coordinator session cannot be activated.",
|
|
@@ -1498,7 +1518,7 @@ function publicSdkAcknowledgement(result: RuntimePromptAcknowledgement): Record<
|
|
|
1498
1518
|
}
|
|
1499
1519
|
|
|
1500
1520
|
async function listJsonFiles(dir: string): Promise<unknown[]> {
|
|
1501
|
-
const scan = await
|
|
1521
|
+
const scan = await listCoordinatorJsonFilesWithRetry(dir);
|
|
1502
1522
|
if (scan.capped) throw new Error("coordinator_projection_scan_incomplete");
|
|
1503
1523
|
if (scan.skippedEmpty > 0 || scan.skippedDebris > 0) {
|
|
1504
1524
|
logger.warn("Coordinator projection scan skipped debris", {
|
|
@@ -1530,7 +1550,10 @@ function brokerSessionId(record: Record<string, unknown>): string | null {
|
|
|
1530
1550
|
}
|
|
1531
1551
|
|
|
1532
1552
|
function brokerSessionScope(record: Record<string, unknown>): string | null {
|
|
1533
|
-
|
|
1553
|
+
// Locator v2 records the canonical workspace directory as `cwd`; the removed
|
|
1554
|
+
// `repo` field is never translated, so reading it here silently dropped every
|
|
1555
|
+
// broker row and failed open with an empty scoped listing.
|
|
1556
|
+
return firstString(asRecord(record.locator) ?? {}, ["cwd"]);
|
|
1534
1557
|
}
|
|
1535
1558
|
|
|
1536
1559
|
function sameCanonicalPath(left: string, right: string, platform: NodeJS.Platform): boolean {
|
|
@@ -2634,11 +2657,6 @@ async function writeActiveTurn(namespaceDir: string, turn: TurnRecord): Promise<
|
|
|
2634
2657
|
});
|
|
2635
2658
|
}
|
|
2636
2659
|
|
|
2637
|
-
async function clearActiveTurn(namespaceDir: string, turn: TurnRecord): Promise<void> {
|
|
2638
|
-
const active = asRecord(await readJsonFile(activeTurnFile(namespaceDir, turn.session_id)));
|
|
2639
|
-
if (active?.turn_id === turn.turn_id) await removeCoordinatorFile(activeTurnFile(namespaceDir, turn.session_id));
|
|
2640
|
-
}
|
|
2641
|
-
|
|
2642
2660
|
async function readSessionState(namespaceDir: string, sessionId: string): Promise<CoordinatorSessionState | null> {
|
|
2643
2661
|
return (await readJsonFile(sessionStateFile(namespaceDir, sessionId))) as CoordinatorSessionState | null;
|
|
2644
2662
|
}
|
|
@@ -2658,9 +2676,6 @@ async function writeSessionStateUnlocked(
|
|
|
2658
2676
|
} = {},
|
|
2659
2677
|
): Promise<CoordinatorSessionState> {
|
|
2660
2678
|
const persisted = await readSessionState(namespaceDir, sessionId);
|
|
2661
|
-
const session = asRecord(
|
|
2662
|
-
await readJsonFile(path.join(namespaceDir, "sessions", `${safeExternalId("session", sessionId)}.json`)),
|
|
2663
|
-
);
|
|
2664
2679
|
// An overwrite is a canonical lifecycle repair, not proof that tool activity ended.
|
|
2665
2680
|
const previous = options.overwrite ? null : persisted;
|
|
2666
2681
|
const hasCurrentTurn = Object.hasOwn(options, "currentTurnId");
|
|
@@ -5044,6 +5059,58 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
5044
5059
|
const keyDigest = createHash("sha256").update(idempotencyKey).digest("hex");
|
|
5045
5060
|
return path.join(namespaceDir, "idempotency", `${keyDigest}.json`);
|
|
5046
5061
|
}
|
|
5062
|
+
const idempotencyFlights = new Map<string, { requestDigest: string; promise: Promise<Record<string, unknown>> }>();
|
|
5063
|
+
type IdempotencyFlightAdmission =
|
|
5064
|
+
| { kind: "conflict"; response: Record<string, unknown> }
|
|
5065
|
+
| { kind: "joined"; promise: Promise<Record<string, unknown>> }
|
|
5066
|
+
| { kind: "owner"; run: (operation: Promise<Record<string, unknown>>) => Promise<Record<string, unknown>> };
|
|
5067
|
+
function admitIdempotencyFlight(
|
|
5068
|
+
tool: string,
|
|
5069
|
+
idempotencyKey: string,
|
|
5070
|
+
canonicalArgs: Record<string, unknown>,
|
|
5071
|
+
): IdempotencyFlightAdmission {
|
|
5072
|
+
const keyDigest = createHash("sha256").update(idempotencyKey).digest("hex");
|
|
5073
|
+
const requestDigest = createHash("sha256")
|
|
5074
|
+
.update(canonicalJson({ tool, args: canonicalArgs }))
|
|
5075
|
+
.digest("hex");
|
|
5076
|
+
const existingFlight = idempotencyFlights.get(keyDigest);
|
|
5077
|
+
if (existingFlight) {
|
|
5078
|
+
if (existingFlight.requestDigest !== requestDigest)
|
|
5079
|
+
return {
|
|
5080
|
+
kind: "conflict",
|
|
5081
|
+
response: {
|
|
5082
|
+
ok: false,
|
|
5083
|
+
error: { code: "idempotency_conflict", message: "idempotency key was used with a different request" },
|
|
5084
|
+
},
|
|
5085
|
+
};
|
|
5086
|
+
return { kind: "joined", promise: existingFlight.promise };
|
|
5087
|
+
}
|
|
5088
|
+
const result = Promise.withResolvers<Record<string, unknown>>();
|
|
5089
|
+
const flight = { requestDigest, promise: result.promise };
|
|
5090
|
+
idempotencyFlights.set(keyDigest, flight);
|
|
5091
|
+
return {
|
|
5092
|
+
kind: "owner",
|
|
5093
|
+
run: async operation => {
|
|
5094
|
+
void operation.then(result.resolve, result.reject);
|
|
5095
|
+
try {
|
|
5096
|
+
return await result.promise;
|
|
5097
|
+
} finally {
|
|
5098
|
+
if (idempotencyFlights.get(keyDigest) === flight) idempotencyFlights.delete(keyDigest);
|
|
5099
|
+
}
|
|
5100
|
+
},
|
|
5101
|
+
};
|
|
5102
|
+
}
|
|
5103
|
+
async function withIdempotencyFlight(
|
|
5104
|
+
tool: string,
|
|
5105
|
+
idempotencyKey: string,
|
|
5106
|
+
canonicalArgs: Record<string, unknown>,
|
|
5107
|
+
operation: () => Promise<Record<string, unknown>>,
|
|
5108
|
+
): Promise<Record<string, unknown>> {
|
|
5109
|
+
const admission = admitIdempotencyFlight(tool, idempotencyKey, canonicalArgs);
|
|
5110
|
+
if (admission.kind === "conflict") return admission.response;
|
|
5111
|
+
if (admission.kind === "joined") return await admission.promise;
|
|
5112
|
+
return await admission.run(operation());
|
|
5113
|
+
}
|
|
5047
5114
|
async function withOrderedSessionStateLocks<T>(
|
|
5048
5115
|
lockFiles: readonly string[],
|
|
5049
5116
|
operation: () => Promise<T>,
|
|
@@ -5075,6 +5142,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
5075
5142
|
recoverInProgress = false,
|
|
5076
5143
|
isNonterminal: (response: Record<string, unknown>) => boolean = isRouterRequestAmbiguous,
|
|
5077
5144
|
lockAlreadyHeld = false,
|
|
5145
|
+
flightAlreadyOwned = false,
|
|
5078
5146
|
): Promise<Record<string, unknown>> {
|
|
5079
5147
|
const keyDigest = createHash("sha256").update(idempotencyKey).digest("hex");
|
|
5080
5148
|
const requestDigest = createHash("sha256")
|
|
@@ -5179,7 +5247,11 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
5179
5247
|
});
|
|
5180
5248
|
return response;
|
|
5181
5249
|
};
|
|
5182
|
-
|
|
5250
|
+
const executeWithLock = async (): Promise<Record<string, unknown>> =>
|
|
5251
|
+
lockAlreadyHeld ? await execute() : await withSessionStateLock(lockFile, execute);
|
|
5252
|
+
return flightAlreadyOwned
|
|
5253
|
+
? await executeWithLock()
|
|
5254
|
+
: await withIdempotencyFlight(tool, idempotencyKey, canonicalArgs, executeWithLock);
|
|
5183
5255
|
}
|
|
5184
5256
|
|
|
5185
5257
|
async function brokerSession(
|
|
@@ -5268,7 +5340,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
5268
5340
|
} catch (error) {
|
|
5269
5341
|
const sessionId = remoteSession.value;
|
|
5270
5342
|
if (!sessionId) {
|
|
5271
|
-
if (error instanceof SdkClientError && error.code
|
|
5343
|
+
if (error instanceof SdkClientError && OBSERVED_BROKER_FAILURE_CODES.has(error.code)) throw error;
|
|
5272
5344
|
throw new SdkClientError(
|
|
5273
5345
|
UNOBSERVED_COMPENSATION_CODE,
|
|
5274
5346
|
"Coordinator creation outcome could not be identified for cleanup.",
|
|
@@ -5281,11 +5353,28 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
5281
5353
|
sessionId,
|
|
5282
5354
|
);
|
|
5283
5355
|
} catch (closeError) {
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
|
|
5356
|
+
let listed: Record<string, unknown>;
|
|
5357
|
+
try {
|
|
5358
|
+
listed = await paginatedBrokerSessionList(cwd, { cwd });
|
|
5359
|
+
} catch (listError) {
|
|
5360
|
+
throw new SdkClientError(
|
|
5361
|
+
UNOBSERVED_COMPENSATION_CODE,
|
|
5362
|
+
`Coordinator creation failed after remote session ${sessionId} was created, and compensation failed.`,
|
|
5363
|
+
{ primary: error, compensation: closeError, list: listError, session_id: sessionId },
|
|
5364
|
+
);
|
|
5365
|
+
}
|
|
5366
|
+
const rows = jsonRecords(Array.isArray(listed.sessions) ? listed.sessions : []);
|
|
5367
|
+
const row = rows.find(candidate => brokerSessionId(candidate) === sessionId);
|
|
5368
|
+
const deadUncertain =
|
|
5369
|
+
row !== undefined &&
|
|
5370
|
+
row.live !== true &&
|
|
5371
|
+
(row.terminalUncertain === true || (row as Record<string, unknown>).terminal_uncertain === true);
|
|
5372
|
+
if (!deadUncertain)
|
|
5373
|
+
throw new SdkClientError(
|
|
5374
|
+
UNOBSERVED_COMPENSATION_CODE,
|
|
5375
|
+
`Coordinator creation failed after remote session ${sessionId} was created, and compensation failed.`,
|
|
5376
|
+
{ primary: error, compensation: closeError, session_id: sessionId },
|
|
5377
|
+
);
|
|
5289
5378
|
}
|
|
5290
5379
|
throw error;
|
|
5291
5380
|
}
|
|
@@ -6713,7 +6802,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
6713
6802
|
// any stale pointer directly: once the turn projection is rewritten
|
|
6714
6803
|
// terminal, readActiveTurn() intentionally rejects it and cannot
|
|
6715
6804
|
// discover the legacy active-turn file to clear.
|
|
6716
|
-
await
|
|
6805
|
+
await removeCoordinatorFile(activeTurnFile(namespaceDir, sessionId));
|
|
6717
6806
|
}
|
|
6718
6807
|
// A runtime sidecar can advance a live session while the canonical WAL still
|
|
6719
6808
|
// needs projection repair. Preserve that observed lifecycle state (and its
|
|
@@ -8780,7 +8869,10 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
8780
8869
|
code === "live_session"
|
|
8781
8870
|
);
|
|
8782
8871
|
};
|
|
8783
|
-
|
|
8872
|
+
const flight = admitIdempotencyFlight(name, retirementKey, canonicalArgs);
|
|
8873
|
+
if (flight.kind === "conflict") return flight.response;
|
|
8874
|
+
if (flight.kind === "joined") return await flight.promise;
|
|
8875
|
+
const retirementOperation = withOrderedSessionStateLocks(
|
|
8784
8876
|
[idempotencyLockFile(creationKey), idempotencyLockFile(retirementKey)],
|
|
8785
8877
|
async () =>
|
|
8786
8878
|
await withToolIdempotency(
|
|
@@ -8964,8 +9056,10 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
8964
9056
|
true,
|
|
8965
9057
|
isRetirementRetryable,
|
|
8966
9058
|
true,
|
|
9059
|
+
true,
|
|
8967
9060
|
),
|
|
8968
9061
|
);
|
|
9062
|
+
return await flight.run(retirementOperation);
|
|
8969
9063
|
}
|
|
8970
9064
|
if (name === "gjc_coordinator_activate_session") {
|
|
8971
9065
|
requireCoordinatorMutation(config, "sessions", args);
|
package/src/crash/index-store.ts
CHANGED
|
@@ -613,6 +613,7 @@ export function applyCrashEvent(index: CrashIndex, event: CrashEvent, now: numbe
|
|
|
613
613
|
}
|
|
614
614
|
|
|
615
615
|
// occurrence
|
|
616
|
+
if (event.provenance === "eval" || event.provenance === "bun_test") return false;
|
|
616
617
|
// Clear before deduplication: a replayed occurrence after main-index publish
|
|
617
618
|
// but before retirement-sidecar publish must still supersede a stale tombstone.
|
|
618
619
|
index.retiredFingerprints = index.retiredFingerprints.filter(fingerprint => fingerprint !== event.fingerprint);
|
|
@@ -698,6 +699,7 @@ async function recoverAndRecomputeRetainedCounts(index: CrashIndex, crashLogPath
|
|
|
698
699
|
const recordsByFingerprint = new Map<string, LoadedCrashRecord[]>();
|
|
699
700
|
const seenRecordIds = new Set<string>();
|
|
700
701
|
for (const record of parseRecoverableCrashRecords(contents)) {
|
|
702
|
+
if (record.provenance === "eval" || record.provenance === "bun_test") continue;
|
|
701
703
|
if (seenRecordIds.has(record.recordId)) continue;
|
|
702
704
|
seenRecordIds.add(record.recordId);
|
|
703
705
|
const records = recordsByFingerprint.get(record.fingerprint) ?? [];
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
CRASH_FINGERPRINT_VERSION,
|
|
16
16
|
CRASH_RECORD_MARKER,
|
|
17
17
|
type CrashFingerprint,
|
|
18
|
+
type CrashProvenance,
|
|
18
19
|
computeCrashFingerprint,
|
|
19
20
|
parseCrashRecordMarker,
|
|
20
21
|
} from "@gajae-code/utils";
|
|
@@ -33,6 +34,7 @@ export interface LoadedCrashRecord {
|
|
|
33
34
|
readonly body: string;
|
|
34
35
|
/** The `Name: message` part of the header line. */
|
|
35
36
|
readonly headline: string;
|
|
37
|
+
readonly provenance?: CrashProvenance;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
interface ParsedCrashRecord {
|
|
@@ -78,6 +80,7 @@ function parseCandidates(contents: string): ParsedCrashRecord[] {
|
|
|
78
80
|
let at = 0;
|
|
79
81
|
let buffer: string[] = [];
|
|
80
82
|
let started = false;
|
|
83
|
+
let provenance: CrashProvenance | undefined;
|
|
81
84
|
let pending: ParsedCrashRecord | undefined;
|
|
82
85
|
for (const line of contents.split("\n")) {
|
|
83
86
|
if (pending) {
|
|
@@ -92,6 +95,12 @@ function parseCandidates(contents: string): ParsedCrashRecord[] {
|
|
|
92
95
|
continue;
|
|
93
96
|
}
|
|
94
97
|
headline = header[2] ?? "";
|
|
98
|
+
const label = line.slice(line.indexOf("[") + 1, line.indexOf("]"));
|
|
99
|
+
const provenanceValue = label
|
|
100
|
+
.split(";")
|
|
101
|
+
.find(part => part.startsWith("provenance="))
|
|
102
|
+
?.slice(11);
|
|
103
|
+
provenance = provenanceValue === "eval" || provenanceValue === "bun_test" ? provenanceValue : undefined;
|
|
95
104
|
at = parsedAt;
|
|
96
105
|
buffer = [];
|
|
97
106
|
started = true;
|
|
@@ -116,6 +125,7 @@ function parseCandidates(contents: string): ParsedCrashRecord[] {
|
|
|
116
125
|
messageClass: fingerprint?.messageClass ?? "",
|
|
117
126
|
body: lines.join("\n").trimEnd(),
|
|
118
127
|
headline,
|
|
128
|
+
...(provenance === undefined ? {} : { provenance }),
|
|
119
129
|
},
|
|
120
130
|
complete: false,
|
|
121
131
|
bound: marker.version === CRASH_FINGERPRINT_VERSION && fingerprint !== undefined,
|
|
@@ -378,7 +378,7 @@ Auto-answer has a clarity cap: unless the architect confidence is `high` and unc
|
|
|
378
378
|
|
|
379
379
|
When the user's answer is free-text that carries reasoning, constraints, or scope decisions, do not forward it to scoring as a lossy one-line label. First structure it into a compact interpretation using the canonical sections — **Decision**, **Reasoning**, **Constraints (user-stated)**, **Out of scope (user-stated)**, and **Codebase context (verified)** (omit empty sections). Then confirm with exactly one `ask` that nothing is lost or misrepresented: the `ask` question body MUST render the full structured interpretation — every non-empty canonical section, verbatim — before the confirmation prompt. The user is approving that specific interpretation, so it must be visible inside the question body; never ask "does this capture it?" / "이 해석이 맞아?" without first displaying the interpretation itself. A confirmation `ask` whose body omits the interpretation it is asking about is a hard error: re-issue it with the interpretation shown. Apply `language.instruction` when present.
|
|
380
380
|
|
|
381
|
-
Offer options such as **Send as-is**, **Add a constraint**, **Mark something out of scope**, **Add context**, and **Rewrite**, plus free-text. If the user picks anything other than "Send as-is", collect the exact missing text with one follow-up `ask` (never infer it from the option label), fold it into the structured interpretation, and re-confirm. Do not advance to scoring while the user is still saying something is missing.
|
|
381
|
+
Offer options such as **Send as-is**, **Add a constraint**, **Mark something out of scope**, **Add context**, and **Rewrite**, plus free-text through the Ask UI's automatic Other/custom-input control. Never add the automatic custom-input label or a pseudo-option that imitates it, such as **Other (type your own)**, **Other (specify)**, **type your own**, **custom input**, **직접 입력**, or **직접 입력으로 추가**; a plain domain value such as **Other** is valid when it is a genuine answer, not a request for free text, and none of these custom-input labels may receive `recommended`. If the user picks anything other than "Send as-is", collect the exact missing text with one follow-up `ask` whose question body asks for the text and whose options are only real non-custom choices such as cancel/keep-current (never infer it from the option label), fold it into the structured interpretation, and re-confirm. Do not advance to scoring while the user is still saying something is missing.
|
|
382
382
|
|
|
383
383
|
Skip Refine for short answers with no attached reasoning (e.g. "Yes" / "No" / a single proper noun), for pre-built option picks where the structure is already explicit, for auto-confirmed code/brownfield facts, and for architect auto-answers (already structured by Step 2b′). A refined answer counts as direct user judgment: record the round in `refined_rounds` and reset `auto_answer_streak` to 0. Feed the confirmed structured interpretation — not the raw free text — into Step 2c scoring and established-facts maintenance.
|
|
384
384
|
|