@gajae-code/coding-agent 0.16.4 → 0.16.6
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 +54 -0
- package/dist/types/capability/types.d.ts +4 -0
- package/dist/types/cli/customize-doctor.d.ts +22 -0
- package/dist/types/config/file-lock.d.ts +31 -3
- package/dist/types/coordinator-mcp/question-state.d.ts +7 -0
- package/dist/types/coordinator-mcp/server.d.ts +7 -0
- package/dist/types/discovery/helpers.d.ts +33 -0
- package/dist/types/exec/bash-executor.d.ts +17 -1
- package/dist/types/exec/bash-shell-guardian.d.ts +15 -0
- package/dist/types/exec/bash-shell-runtime-entry.d.ts +2 -0
- package/dist/types/exec/bash-shell-signals.d.ts +2 -0
- package/dist/types/exec/bash-shell-supervisor-entry.d.ts +1 -0
- package/dist/types/exec/bash-shell-supervisor.d.ts +10 -0
- package/dist/types/exec/bash-shell-worker-entry.d.ts +2 -0
- package/dist/types/exec/bash-shell-worker-protocol.d.ts +63 -0
- package/dist/types/exec/bash-shell-worker.d.ts +1 -0
- package/dist/types/exec/isolated-shell.d.ts +19 -0
- package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +9 -0
- package/dist/types/extensibility/extensions/function-hooks.d.ts +169 -0
- package/dist/types/extensibility/extensions/index.d.ts +1 -0
- package/dist/types/extensibility/extensions/runner.d.ts +46 -6
- package/dist/types/extensibility/extensions/types.d.ts +16 -0
- package/dist/types/extensibility/extensions/wrapper.d.ts +3 -1
- package/dist/types/extensibility/gjc-plugins/constrained-hooks.d.ts +35 -2
- package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
- package/dist/types/extensibility/hooks/types.d.ts +4 -0
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +4 -1
- package/dist/types/extensibility/skill-management.d.ts +9 -4
- package/dist/types/extensibility/skills.d.ts +2 -0
- package/dist/types/gjc-runtime/empty-delete-gc.d.ts +4 -1
- package/dist/types/gjc-runtime/linux-proc.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +12 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +41 -3
- package/dist/types/gjc-runtime/ultragoal-succession.d.ts +251 -0
- package/dist/types/modes/components/bash-execution.d.ts +1 -0
- package/dist/types/modes/components/execution-shared.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/types.d.ts +6 -0
- package/dist/types/sdk/broker/ensure.d.ts +32 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +0 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
- package/dist/types/sdk/broker/startup-failure.d.ts +4 -1
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -0
- package/dist/types/sdk/bus/chat-effect-journal.d.ts +17 -2
- package/dist/types/sdk/bus/conversation-store.d.ts +11 -0
- package/dist/types/sdk/bus/lifecycle-commands.d.ts +8 -8
- package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/topic-registry.d.ts +8 -0
- package/dist/types/sdk/client/client.d.ts +6 -0
- package/dist/types/sdk/host/host.d.ts +3 -1
- package/dist/types/sdk/host/session-runtime.d.ts +4 -2
- package/dist/types/sdk/lifecycle/service.d.ts +1 -0
- package/dist/types/sdk/router/session-router.d.ts +10 -2
- package/dist/types/sdk/session.d.ts +5 -1
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/fallback-chain-controller.d.ts +15 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
- package/dist/types/session/messages.d.ts +1 -0
- package/dist/types/session/session-manager.d.ts +3 -1
- package/dist/types/setup/paseo/announce.d.ts +4 -3
- package/dist/types/skill-state/canonical-skills.d.ts +4 -0
- package/dist/types/system-prompt.d.ts +8 -0
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tools/github-cache.d.ts +2 -1
- package/dist/types/tools/index.d.ts +2 -0
- package/package.json +9 -7
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/src/capability/index.ts +14 -2
- package/src/capability/types.ts +4 -0
- package/src/cli/customize-doctor.ts +155 -12
- package/src/cli.ts +22 -0
- package/src/commands/sdk.ts +74 -31
- package/src/config/file-lock-gc.ts +40 -2
- package/src/config/file-lock.ts +198 -14
- package/src/config/model-preset-registry.ts +115 -16
- package/src/config/model-registry.ts +16 -12
- package/src/coordinator-mcp/question-state.ts +33 -2
- package/src/coordinator-mcp/server.ts +477 -101
- package/src/crash/sanitize.ts +1 -1
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +22 -0
- package/src/discovery/builtin.ts +50 -62
- package/src/discovery/helpers.ts +297 -32
- package/src/exec/bash-executor.ts +330 -50
- package/src/exec/bash-shell-guardian.ts +388 -0
- package/src/exec/bash-shell-runtime-entry.ts +5 -0
- package/src/exec/bash-shell-signals.ts +29 -0
- package/src/exec/bash-shell-supervisor-entry.ts +3 -0
- package/src/exec/bash-shell-supervisor.ts +359 -0
- package/src/exec/bash-shell-worker-entry.ts +5 -0
- package/src/exec/bash-shell-worker-protocol.ts +36 -0
- package/src/exec/bash-shell-worker.ts +116 -0
- package/src/exec/isolated-shell.ts +393 -0
- package/src/extensibility/extensions/function-hooks-internal.ts +73 -0
- package/src/extensibility/extensions/function-hooks.ts +959 -0
- package/src/extensibility/extensions/index.ts +22 -0
- package/src/extensibility/extensions/loader.ts +53 -2
- package/src/extensibility/extensions/runner.ts +895 -264
- package/src/extensibility/extensions/types.ts +42 -0
- package/src/extensibility/extensions/wrapper.ts +56 -15
- package/src/extensibility/gjc-plugins/compiler.ts +20 -2
- package/src/extensibility/gjc-plugins/constrained-hooks.ts +321 -23
- package/src/extensibility/gjc-plugins/registry.ts +113 -1
- package/src/extensibility/gjc-plugins/runtime-quarantine.ts +20 -2
- package/src/extensibility/gjc-plugins/schema.ts +121 -4
- package/src/extensibility/gjc-plugins/types.ts +25 -0
- package/src/extensibility/hooks/loader.ts +133 -16
- package/src/extensibility/hooks/types.ts +28 -0
- package/src/extensibility/runtime-skill-discovery.ts +80 -47
- package/src/extensibility/skill-management.ts +200 -52
- package/src/extensibility/skills.ts +29 -9
- package/src/gjc-runtime/empty-delete-gc.ts +94 -4
- package/src/gjc-runtime/gc-runtime.ts +29 -0
- package/src/gjc-runtime/linux-proc.ts +14 -0
- package/src/gjc-runtime/session-state-lock.ts +51 -8
- package/src/gjc-runtime/session-state-sidecar.ts +27 -11
- package/src/gjc-runtime/state-writer.ts +30 -29
- package/src/gjc-runtime/ultragoal-runtime.ts +189 -47
- package/src/gjc-runtime/ultragoal-succession.ts +1667 -0
- package/src/gjc-runtime/workflow-manifest.generated.json +52 -1
- package/src/gjc-runtime/workflow-manifest.ts +16 -0
- package/src/harness-control-plane/session-lease.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/lsp/config.ts +10 -3
- package/src/lsp/index.ts +1 -1
- package/src/lsp/path-trust.ts +69 -14
- package/src/main.ts +6 -1
- package/src/modes/acp/acp-agent.ts +16 -3
- package/src/modes/components/bash-execution.ts +4 -1
- package/src/modes/components/execution-shared.ts +4 -1
- package/src/modes/components/session-observer-overlay.ts +1 -1
- package/src/modes/components/tool-status-header.ts +3 -1
- package/src/modes/controllers/command-controller.ts +1 -0
- package/src/modes/controllers/event-controller.ts +3 -2
- package/src/modes/controllers/extension-ui-controller.ts +1 -0
- package/src/modes/controllers/input-controller.ts +6 -3
- package/src/modes/interactive-mode.ts +6 -5
- package/src/modes/print-mode.ts +5 -1
- package/src/modes/prompt-action-autocomplete.ts +54 -27
- package/src/modes/runtime-init.ts +2 -0
- package/src/modes/types.ts +16 -3
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/prompts/tools/subagent.md +1 -0
- package/src/runtime-mcp/manager.ts +20 -2
- package/src/runtime-mcp/pool.test.ts +20 -11
- package/src/sdk/broker/broker.ts +119 -28
- package/src/sdk/broker/ensure.ts +248 -41
- package/src/sdk/broker/lifecycle-ledger.ts +7 -17
- package/src/sdk/broker/lifecycle.ts +56 -6
- package/src/sdk/broker/startup-failure.ts +15 -6
- package/src/sdk/bus/chat-daemon-control.ts +3 -1
- package/src/sdk/bus/chat-daemon-runtime.ts +35 -5
- package/src/sdk/bus/chat-effect-journal.ts +132 -7
- package/src/sdk/bus/conversation-store.ts +29 -0
- package/src/sdk/bus/lifecycle-commands.ts +17 -11
- package/src/sdk/bus/slack-conversation.ts +1 -0
- package/src/sdk/bus/slack-daemon.ts +387 -48
- package/src/sdk/bus/telegram-daemon.ts +16 -2
- package/src/sdk/bus/topic-registry.ts +19 -0
- package/src/sdk/cli/master-cli.ts +1 -1
- package/src/sdk/cli/session-cli.ts +2 -2
- package/src/sdk/client/client.ts +66 -17
- package/src/sdk/host/host.ts +29 -9
- package/src/sdk/host/query/revision-store.ts +19 -13
- package/src/sdk/host/reverse-leases.ts +7 -4
- package/src/sdk/host/session-runtime.test.ts +50 -8
- package/src/sdk/host/session-runtime.ts +158 -51
- package/src/sdk/lifecycle/service.ts +4 -0
- package/src/sdk/protocol/operation-inventory.generated.json +11 -0
- package/src/sdk/router/session-router.ts +89 -38
- package/src/sdk/session.ts +155 -26
- package/src/sdk/transport/relay.ts +35 -13
- package/src/session/agent-session.ts +117 -52
- package/src/session/fallback-chain-controller.ts +38 -0
- package/src/session/internal/managed-session-storage.ts +19 -5
- package/src/session/messages.ts +5 -0
- package/src/session/session-manager.ts +29 -5
- package/src/session-import/redact.ts +5 -1
- package/src/setup/paseo/announce.ts +49 -43
- package/src/setup/paseo/setup-deps.ts +1 -1
- package/src/skill-state/canonical-skills.ts +12 -0
- package/src/skills/skills.test.ts +41 -21
- package/src/system-prompt.ts +25 -3
- package/src/task/executor.ts +5 -0
- package/src/task/index.ts +4 -0
- package/src/tools/bash.ts +28 -11
- package/src/tools/gh.ts +26 -3
- package/src/tools/github-cache.ts +68 -16
- package/src/tools/index.ts +2 -0
- package/src/tools/job.ts +13 -2
- package/src/tools/read.ts +14 -2
- package/src/tools/render-utils.ts +1 -3
- package/src/tools/resource-gc.ts +28 -8
- package/src/tools/skill-discovery.ts +5 -0
- package/src/tools/skill.ts +15 -0
- package/src/tools/subagent.ts +3 -2
- package/src/tools/tool-catalog.generated.ts +2 -2
- package/src/tools/ultragoal-ask-guard.ts +2 -2
|
@@ -11,7 +11,11 @@ import {
|
|
|
11
11
|
COORDINATOR_MCP_TOOL_NAMES,
|
|
12
12
|
type CoordinatorToolName,
|
|
13
13
|
} from "../coordinator/contract";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
SessionStateLockUnavailableError,
|
|
16
|
+
tryWithSessionStateFileLock,
|
|
17
|
+
withSessionStateFileLock,
|
|
18
|
+
} from "../gjc-runtime/session-state-lock";
|
|
15
19
|
import {
|
|
16
20
|
canonicalCoordinatorSidecarPayload,
|
|
17
21
|
classifyRuntimeToolActivity,
|
|
@@ -311,6 +315,13 @@ interface CoordinatorServices {
|
|
|
311
315
|
afterCanonicalReportCommit?: (sessionId: string) => void | Promise<void>;
|
|
312
316
|
/** Test barrier invoked after a canonical report safe response is durable and before outer idempotency completion. */
|
|
313
317
|
afterCanonicalReportSafeResponse?: (sessionId: string, response: Record<string, unknown>) => void | Promise<void>;
|
|
318
|
+
/** Test barriers around delegate admission and outer response durability. */
|
|
319
|
+
afterDelegateAdmission?: (sessionId: string) => void | Promise<void>;
|
|
320
|
+
afterDelegateCreationWalCommitted?: (sessionId: string) => void | Promise<void>;
|
|
321
|
+
afterDelegateCreationCompleted?: () => void | Promise<void>;
|
|
322
|
+
beforeDelegateResponseCommit?: () => void | Promise<void>;
|
|
323
|
+
afterDelegateResponseCommit?: () => void | Promise<void>;
|
|
324
|
+
readCoordinatorSessionEntries?: (directory: string) => Promise<string[]>;
|
|
314
325
|
}
|
|
315
326
|
|
|
316
327
|
type CoordinatorModelResolution =
|
|
@@ -942,7 +953,11 @@ function toolSchema(name: CoordinatorToolName): {
|
|
|
942
953
|
mpreset,
|
|
943
954
|
model: modelPin,
|
|
944
955
|
worktree,
|
|
945
|
-
await_completion: {
|
|
956
|
+
await_completion: {
|
|
957
|
+
type: "boolean",
|
|
958
|
+
description:
|
|
959
|
+
"Defaults to false: return after durable admission without completion observation. If true, observe until terminal, waiting_for_answer, or timeout. Once a complete response is committed, an identical admitted retry replays it instead of observing again.",
|
|
960
|
+
},
|
|
946
961
|
timeout_ms: {
|
|
947
962
|
type: "number",
|
|
948
963
|
description:
|
|
@@ -995,9 +1010,9 @@ function workflowSkill(workflow: DelegateWorkflow): "ralplan" | "ultragoal" {
|
|
|
995
1010
|
function delegateToolDescription(workflow: DelegateWorkflow): string {
|
|
996
1011
|
switch (workflow) {
|
|
997
1012
|
case "plan":
|
|
998
|
-
return "Delegate consensus planning to GJC: start a session and
|
|
1013
|
+
return "Delegate consensus planning to GJC: start or reuse a session and submit /skill:ralplan. Return durable admission by default; await_completion=true additionally returns bounded completion observation, stopping at terminal, waiting_for_answer, or timeout. Replay remains subject to current authorization, path, model, mpreset, and worktree validation.";
|
|
999
1014
|
case "execute":
|
|
1000
|
-
return "Delegate execution to GJC: start a session and
|
|
1015
|
+
return "Delegate execution to GJC: start or reuse a session and submit /skill:ultragoal. Return durable admission by default; await_completion=true additionally returns bounded completion observation, stopping at terminal, waiting_for_answer, or timeout. Replay remains subject to current authorization, path, model, mpreset, and worktree validation.";
|
|
1001
1016
|
}
|
|
1002
1017
|
}
|
|
1003
1018
|
|
|
@@ -1267,6 +1282,21 @@ const PUBLIC_ERROR_MESSAGES: Record<string, string> = {
|
|
|
1267
1282
|
ownership_mismatch: "Coordinator ownership does not match the request.",
|
|
1268
1283
|
};
|
|
1269
1284
|
|
|
1285
|
+
interface DelegateAdmissionCheckpoint {
|
|
1286
|
+
kind: "delegate_admission";
|
|
1287
|
+
session_id: string;
|
|
1288
|
+
turn_id: string;
|
|
1289
|
+
response: Record<string, unknown>;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
interface DelegateResponsePinV1 {
|
|
1293
|
+
session_id: string;
|
|
1294
|
+
coordinator_turn_id: string;
|
|
1295
|
+
prompt_key_digest: string;
|
|
1296
|
+
runtime_command_id: string;
|
|
1297
|
+
runtime_turn_id: string;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1270
1300
|
interface CoordinatorToolIdempotencyRecord {
|
|
1271
1301
|
schema_version: 1;
|
|
1272
1302
|
tool: string;
|
|
@@ -1274,6 +1304,8 @@ interface CoordinatorToolIdempotencyRecord {
|
|
|
1274
1304
|
request_digest: string;
|
|
1275
1305
|
state: "in_progress" | "completed";
|
|
1276
1306
|
response?: Record<string, unknown>;
|
|
1307
|
+
admission?: DelegateAdmissionCheckpoint;
|
|
1308
|
+
delegate_response_pin?: DelegateResponsePinV1;
|
|
1277
1309
|
created_at: string;
|
|
1278
1310
|
completed_at?: string;
|
|
1279
1311
|
}
|
|
@@ -3340,10 +3372,22 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
3340
3372
|
// gate canonical question-state initialization.
|
|
3341
3373
|
void startupCodexWakeReplay.catch(() => undefined);
|
|
3342
3374
|
let questionStateReady: Promise<void> | null = null;
|
|
3375
|
+
let delegatePinRecovery: Promise<void> | null = null;
|
|
3376
|
+
let delegatePinCursor: { sessionId: string; promptKey: string | null } | null = null;
|
|
3343
3377
|
|
|
3344
|
-
function ensureQuestionStateReady(): Promise<void> {
|
|
3378
|
+
async function ensureQuestionStateReady(): Promise<void> {
|
|
3345
3379
|
questionStateReady ??= initializeCoordinatorNamespace(questionPaths);
|
|
3346
|
-
|
|
3380
|
+
await questionStateReady;
|
|
3381
|
+
delegatePinRecovery ??= reconcileCompletedDelegateResponsePins().finally(() => {
|
|
3382
|
+
delegatePinRecovery = null;
|
|
3383
|
+
});
|
|
3384
|
+
try {
|
|
3385
|
+
await delegatePinRecovery;
|
|
3386
|
+
} catch (error) {
|
|
3387
|
+
// Pin cleanup is maintenance after a durable response commit. Keep the
|
|
3388
|
+
// server available, but leave recovery incomplete so the next call retries.
|
|
3389
|
+
logger.warn("Coordinator delegate response pin recovery failed", { error: String(error) });
|
|
3390
|
+
}
|
|
3347
3391
|
}
|
|
3348
3392
|
|
|
3349
3393
|
let retainedDeliveryRecovery: Promise<void> | null = null;
|
|
@@ -5138,7 +5182,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
5138
5182
|
tool: string,
|
|
5139
5183
|
idempotencyKey: string,
|
|
5140
5184
|
canonicalArgs: Record<string, unknown>,
|
|
5141
|
-
operation: () => Promise<Record<string, unknown>>,
|
|
5185
|
+
operation: (recovering: boolean) => Promise<Record<string, unknown>>,
|
|
5142
5186
|
recoverInProgress = false,
|
|
5143
5187
|
isNonterminal: (response: Record<string, unknown>) => boolean = isRouterRequestAmbiguous,
|
|
5144
5188
|
lockAlreadyHeld = false,
|
|
@@ -5150,6 +5194,49 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
5150
5194
|
.digest("hex");
|
|
5151
5195
|
const file = idempotencyFile(idempotencyKey);
|
|
5152
5196
|
const lockFile = idempotencyLockFile(idempotencyKey);
|
|
5197
|
+
const releaseResponsePin = async (record: Partial<CoordinatorToolIdempotencyRecord>): Promise<void> => {
|
|
5198
|
+
if (!workflowForDelegateTool(tool) || record.delegate_response_pin === undefined) return;
|
|
5199
|
+
try {
|
|
5200
|
+
if (!(await releaseDelegateResponsePin(record.delegate_response_pin, idempotencyKey)))
|
|
5201
|
+
throw new Error("delegate_response_pin_mismatch");
|
|
5202
|
+
} catch (error) {
|
|
5203
|
+
// Cleanup cannot invalidate an already committed snapshot. Keep the
|
|
5204
|
+
// pin on failure and retry cleanup during readiness or replay.
|
|
5205
|
+
logger.warn("Coordinator delegate response pin cleanup failed", {
|
|
5206
|
+
sessionId: record.delegate_response_pin?.session_id,
|
|
5207
|
+
error: String(error),
|
|
5208
|
+
});
|
|
5209
|
+
}
|
|
5210
|
+
};
|
|
5211
|
+
const commitResponse = async (record: CoordinatorToolIdempotencyRecord): Promise<void> => {
|
|
5212
|
+
if (workflowForDelegateTool(tool)) await services.beforeDelegateResponseCommit?.();
|
|
5213
|
+
let durableRecord = record;
|
|
5214
|
+
if (workflowForDelegateTool(tool)) {
|
|
5215
|
+
const latestFile = await readCoordinatorIdempotencyFile(file);
|
|
5216
|
+
if (latestFile.kind !== "record") throw new Error("terminal_uncertain");
|
|
5217
|
+
const latest = latestFile.value;
|
|
5218
|
+
const pin = delegateResponsePin(latest.delegate_response_pin);
|
|
5219
|
+
if (
|
|
5220
|
+
latest.schema_version !== 1 ||
|
|
5221
|
+
latest.tool !== tool ||
|
|
5222
|
+
latest.key_digest !== keyDigest ||
|
|
5223
|
+
latest.request_digest !== requestDigest ||
|
|
5224
|
+
latest.state !== "in_progress"
|
|
5225
|
+
)
|
|
5226
|
+
throw new Error("terminal_uncertain");
|
|
5227
|
+
if (record.response?.ok === true && !pin) throw new Error("terminal_uncertain");
|
|
5228
|
+
durableRecord = pin
|
|
5229
|
+
? {
|
|
5230
|
+
...record,
|
|
5231
|
+
...(latest.admission ? { admission: latest.admission as DelegateAdmissionCheckpoint } : {}),
|
|
5232
|
+
delegate_response_pin: pin,
|
|
5233
|
+
}
|
|
5234
|
+
: record;
|
|
5235
|
+
}
|
|
5236
|
+
await writeCoordinatorIdempotencyFile(file, durableRecord);
|
|
5237
|
+
if (workflowForDelegateTool(tool)) await services.afterDelegateResponseCommit?.();
|
|
5238
|
+
await releaseResponsePin(durableRecord);
|
|
5239
|
+
};
|
|
5153
5240
|
const execute = async (): Promise<Record<string, unknown>> => {
|
|
5154
5241
|
const existingFile = await readCoordinatorIdempotencyFile(file);
|
|
5155
5242
|
if (existingFile.kind === "corrupt")
|
|
@@ -5175,7 +5262,10 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
5175
5262
|
};
|
|
5176
5263
|
if (existing.state === "completed") {
|
|
5177
5264
|
const replay = asRecord(existing.response);
|
|
5178
|
-
if (replay)
|
|
5265
|
+
if (replay) {
|
|
5266
|
+
await releaseResponsePin(existing);
|
|
5267
|
+
return boundedToolResponse(tool, replay);
|
|
5268
|
+
}
|
|
5179
5269
|
return {
|
|
5180
5270
|
ok: false,
|
|
5181
5271
|
error: {
|
|
@@ -5198,7 +5288,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
5198
5288
|
// operation and changing volatile projection timestamps.
|
|
5199
5289
|
const persistedResponse = asRecord(existing.response);
|
|
5200
5290
|
if (persistedResponse && !isNonterminal(persistedResponse)) {
|
|
5201
|
-
await
|
|
5291
|
+
await commitResponse({
|
|
5202
5292
|
...existing,
|
|
5203
5293
|
state: "completed",
|
|
5204
5294
|
response: persistedResponse,
|
|
@@ -5206,12 +5296,12 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
5206
5296
|
} as CoordinatorToolIdempotencyRecord);
|
|
5207
5297
|
return boundedToolResponse(tool, persistedResponse);
|
|
5208
5298
|
}
|
|
5209
|
-
const rawResponse = await operation().catch(error => sdkError(error));
|
|
5299
|
+
const rawResponse = await operation(true).catch(error => sdkError(error));
|
|
5210
5300
|
const response = boundedToolResponse(tool, rawResponse);
|
|
5211
5301
|
// The receipt keeps its original key and request digests, so a
|
|
5212
5302
|
// reused key still conflicts and a later settled answer still seals.
|
|
5213
5303
|
if (isNonterminal(rawResponse)) return response;
|
|
5214
|
-
await
|
|
5304
|
+
await commitResponse({
|
|
5215
5305
|
...existing,
|
|
5216
5306
|
state: "completed",
|
|
5217
5307
|
response,
|
|
@@ -5236,10 +5326,10 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
5236
5326
|
created_at: new Date().toISOString(),
|
|
5237
5327
|
};
|
|
5238
5328
|
await writeCoordinatorIdempotencyFile(file, started);
|
|
5239
|
-
const rawResponse = await operation().catch(error => sdkError(error));
|
|
5329
|
+
const rawResponse = await operation(false).catch(error => sdkError(error));
|
|
5240
5330
|
const response = boundedToolResponse(tool, rawResponse);
|
|
5241
5331
|
if (isNonterminal(rawResponse)) return response;
|
|
5242
|
-
await
|
|
5332
|
+
await commitResponse({
|
|
5243
5333
|
...started,
|
|
5244
5334
|
state: "completed",
|
|
5245
5335
|
response,
|
|
@@ -6336,11 +6426,159 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
6336
6426
|
return payload;
|
|
6337
6427
|
}
|
|
6338
6428
|
|
|
6429
|
+
function delegateResponsePin(value: unknown): DelegateResponsePinV1 | null {
|
|
6430
|
+
const pin = asRecord(value);
|
|
6431
|
+
if (
|
|
6432
|
+
!pin ||
|
|
6433
|
+
typeof pin.session_id !== "string" ||
|
|
6434
|
+
!SAFE_EXTERNAL_ID_PATTERN.test(pin.session_id) ||
|
|
6435
|
+
typeof pin.coordinator_turn_id !== "string" ||
|
|
6436
|
+
!TURN_ID_PATTERN.test(pin.coordinator_turn_id) ||
|
|
6437
|
+
typeof pin.prompt_key_digest !== "string" ||
|
|
6438
|
+
!/^[a-f0-9]{64}$/.test(pin.prompt_key_digest) ||
|
|
6439
|
+
typeof pin.runtime_command_id !== "string" ||
|
|
6440
|
+
!SAFE_EXTERNAL_ID_PATTERN.test(pin.runtime_command_id) ||
|
|
6441
|
+
typeof pin.runtime_turn_id !== "string" ||
|
|
6442
|
+
!SAFE_EXTERNAL_ID_PATTERN.test(pin.runtime_turn_id)
|
|
6443
|
+
)
|
|
6444
|
+
return null;
|
|
6445
|
+
return pin as unknown as DelegateResponsePinV1;
|
|
6446
|
+
}
|
|
6447
|
+
|
|
6448
|
+
/** A persisted final response, never admission alone, permits source compaction. */
|
|
6449
|
+
async function releaseDelegateResponsePin(rawPin: unknown, idempotencyKey: string): Promise<boolean> {
|
|
6450
|
+
const pin = delegateResponsePin(rawPin);
|
|
6451
|
+
if (!pin) return false;
|
|
6452
|
+
if (!(await readSessionTransaction(questionPaths, pin.session_id))) return true;
|
|
6453
|
+
let released = false;
|
|
6454
|
+
await withSessionTransaction(questionPaths, pin.session_id, async transaction => {
|
|
6455
|
+
const request = transaction.requests.prompts[pin.prompt_key_digest];
|
|
6456
|
+
if (request?.delegate_response_pending !== true) {
|
|
6457
|
+
released = true;
|
|
6458
|
+
return;
|
|
6459
|
+
}
|
|
6460
|
+
if (
|
|
6461
|
+
request.sdk_idempotency_key === idempotencyKey &&
|
|
6462
|
+
request.coordinator_turn_id === pin.coordinator_turn_id &&
|
|
6463
|
+
request.runtime_receipt?.accepted === true &&
|
|
6464
|
+
request.runtime_receipt.command_id === pin.runtime_command_id &&
|
|
6465
|
+
request.runtime_receipt.turn_id === pin.runtime_turn_id
|
|
6466
|
+
) {
|
|
6467
|
+
delete request.delegate_response_pending;
|
|
6468
|
+
released = true;
|
|
6469
|
+
}
|
|
6470
|
+
}).catch(error => {
|
|
6471
|
+
if (!(error instanceof Error) || error.message !== "resource_gone") throw error;
|
|
6472
|
+
released = true;
|
|
6473
|
+
});
|
|
6474
|
+
return released;
|
|
6475
|
+
}
|
|
6476
|
+
|
|
6477
|
+
async function reconcileCompletedDelegateResponsePins(): Promise<void> {
|
|
6478
|
+
const roster = await readSchedulerRoster(questionPaths);
|
|
6479
|
+
let persisted: string[];
|
|
6480
|
+
try {
|
|
6481
|
+
persisted = await (services.readCoordinatorSessionEntries ?? fs.readdir)(questionPaths.sessions);
|
|
6482
|
+
} catch (error) {
|
|
6483
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
6484
|
+
persisted = [];
|
|
6485
|
+
}
|
|
6486
|
+
const sessionIds = [
|
|
6487
|
+
...new Set([
|
|
6488
|
+
...roster.roster.map(entry => entry.session_id),
|
|
6489
|
+
...persisted.filter(entry => COORDINATOR_SESSION_ID_PATTERN.test(entry)),
|
|
6490
|
+
]),
|
|
6491
|
+
].sort();
|
|
6492
|
+
// Readiness recurs even after a clean scan: another coordinator may create
|
|
6493
|
+
// a pin later. Bound WAL reads and receipt inspections per pass, and resume
|
|
6494
|
+
// within a session when its pins exhaust the shared budget.
|
|
6495
|
+
const cursor = delegatePinCursor;
|
|
6496
|
+
const nextIndex = sessionIds.findIndex(
|
|
6497
|
+
id => cursor === null || (cursor.promptKey !== null ? id >= cursor.sessionId : id > cursor.sessionId),
|
|
6498
|
+
);
|
|
6499
|
+
const start = nextIndex < 0 ? 0 : nextIndex;
|
|
6500
|
+
let remainingPins = 32;
|
|
6501
|
+
const failures: unknown[] = [];
|
|
6502
|
+
for (let offset = 0; offset < Math.min(sessionIds.length, 32) && remainingPins > 0; offset++) {
|
|
6503
|
+
const sessionId = sessionIds[(start + offset) % sessionIds.length]!;
|
|
6504
|
+
const afterPrompt = offset === 0 && sessionId === cursor?.sessionId ? cursor.promptKey : null;
|
|
6505
|
+
delegatePinCursor = { sessionId, promptKey: null };
|
|
6506
|
+
let transaction: CoordinatorSessionTransactionV1 | null;
|
|
6507
|
+
try {
|
|
6508
|
+
transaction = await readSessionTransaction(questionPaths, sessionId);
|
|
6509
|
+
} catch (error) {
|
|
6510
|
+
failures.push(error);
|
|
6511
|
+
continue;
|
|
6512
|
+
}
|
|
6513
|
+
if (!transaction) continue;
|
|
6514
|
+
for (const promptKey of Object.keys(transaction.requests.prompts).sort()) {
|
|
6515
|
+
if (afterPrompt !== null && promptKey <= afterPrompt) continue;
|
|
6516
|
+
const request = transaction.requests.prompts[promptKey]!;
|
|
6517
|
+
if (request.delegate_response_pending !== true) continue;
|
|
6518
|
+
if (remainingPins === 0) break;
|
|
6519
|
+
remainingPins--;
|
|
6520
|
+
delegatePinCursor = { sessionId, promptKey };
|
|
6521
|
+
const keyDigest = createHash("sha256").update(request.sdk_idempotency_key).digest("hex");
|
|
6522
|
+
const file = path.join(namespaceDir, "idempotency", `${keyDigest}.json`);
|
|
6523
|
+
try {
|
|
6524
|
+
// Receipts are atomically replaced. An in-progress delegate may own
|
|
6525
|
+
// its caller-key lock throughout observation; maintenance must not wait
|
|
6526
|
+
// for it. Completed candidates still need a locked authoritative read.
|
|
6527
|
+
const candidate = await readCoordinatorIdempotencyFile(file);
|
|
6528
|
+
if (candidate.kind === "corrupt") throw new Error("delegate_response_receipt_corrupt");
|
|
6529
|
+
if (candidate.kind !== "record" || candidate.value.state !== "completed") continue;
|
|
6530
|
+
// A completed receipt can still have a live writer finishing cleanup.
|
|
6531
|
+
// Busy candidates remain pinned until a later round-robin pass.
|
|
6532
|
+
await tryWithSessionStateFileLock(
|
|
6533
|
+
path.join(namespaceDir, "idempotency-locks", `${keyDigest}.json`),
|
|
6534
|
+
async () => {
|
|
6535
|
+
const current = await readCoordinatorIdempotencyFile(file);
|
|
6536
|
+
if (current.kind === "missing") return;
|
|
6537
|
+
if (current.kind === "corrupt") throw new Error("delegate_response_receipt_corrupt");
|
|
6538
|
+
if (current.value.state !== "completed") return;
|
|
6539
|
+
// Match replay's response requirement before relinquishing canonical
|
|
6540
|
+
// recovery evidence. Public response subfields may be truncated.
|
|
6541
|
+
if (!asRecord(current.value.response)) throw new Error("delegate_response_receipt_corrupt");
|
|
6542
|
+
const pin = delegateResponsePin(current.value.delegate_response_pin);
|
|
6543
|
+
if (
|
|
6544
|
+
current.value.key_digest !== keyDigest ||
|
|
6545
|
+
pin?.session_id !== sessionId ||
|
|
6546
|
+
pin.prompt_key_digest !== promptKey ||
|
|
6547
|
+
!(await releaseDelegateResponsePin(pin, request.sdk_idempotency_key))
|
|
6548
|
+
)
|
|
6549
|
+
throw new Error("delegate_response_pin_mismatch");
|
|
6550
|
+
},
|
|
6551
|
+
);
|
|
6552
|
+
} catch (error) {
|
|
6553
|
+
failures.push(error);
|
|
6554
|
+
}
|
|
6555
|
+
}
|
|
6556
|
+
if (remainingPins > 0) delegatePinCursor = { sessionId, promptKey: null };
|
|
6557
|
+
}
|
|
6558
|
+
if (failures.length > 0) throw new AggregateError(failures, "delegate_response_pin_recovery_incomplete");
|
|
6559
|
+
}
|
|
6560
|
+
|
|
6561
|
+
function hasInitialDelegatePromptAuthority(
|
|
6562
|
+
transaction: CoordinatorSessionTransactionV1 | null,
|
|
6563
|
+
request: CoordinatorSessionTransactionV1["recovery"]["initial_delegate_request"],
|
|
6564
|
+
): boolean {
|
|
6565
|
+
const pending = transaction?.recovery.initial_delegate_request;
|
|
6566
|
+
return (
|
|
6567
|
+
pending !== undefined &&
|
|
6568
|
+
request !== undefined &&
|
|
6569
|
+
pending.key_digest === request.key_digest &&
|
|
6570
|
+
pending.request_digest === request.request_digest
|
|
6571
|
+
);
|
|
6572
|
+
}
|
|
6573
|
+
|
|
6339
6574
|
async function claimCanonicalPrompt(
|
|
6340
6575
|
sessionId: string,
|
|
6341
6576
|
prompt: string,
|
|
6342
6577
|
operation: "turn.prompt" | "turn.follow_up" | "turn.abort_and_prompt",
|
|
6343
6578
|
idempotencyKey: string,
|
|
6579
|
+
retainDelegateResponse = false,
|
|
6580
|
+
requireExisting = false,
|
|
6581
|
+
initialDelegateRequest?: CoordinatorSessionTransactionV1["recovery"]["initial_delegate_request"],
|
|
6344
6582
|
): Promise<string> {
|
|
6345
6583
|
await ensureQuestionTransaction(sessionId);
|
|
6346
6584
|
const keyDigest = createHash("sha256").update(`${idempotencyKey}\0${operation}`).digest("hex");
|
|
@@ -6350,6 +6588,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
6350
6588
|
if (existing) {
|
|
6351
6589
|
if (existing.request_digest !== requestDigest) throw new Error("idempotency_conflict");
|
|
6352
6590
|
if (existing.phase === "uncertain") throw new Error("terminal_uncertain");
|
|
6591
|
+
if (retainDelegateResponse) existing.delegate_response_pending = true;
|
|
6353
6592
|
// An accepted receipt is durable authority. It is recovered below instead
|
|
6354
6593
|
// of sending the same remote idempotency key a second time.
|
|
6355
6594
|
if (existing.phase === "accepted" || existing.phase === "completed" || existing.phase === "claimed") return;
|
|
@@ -6358,6 +6597,10 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
6358
6597
|
return;
|
|
6359
6598
|
}
|
|
6360
6599
|
}
|
|
6600
|
+
// Check under the WAL lock as well as at the delegate boundary: an
|
|
6601
|
+
// unpinned historical receipt could compact between those two reads.
|
|
6602
|
+
const initialAuthority = hasInitialDelegatePromptAuthority(transaction, initialDelegateRequest);
|
|
6603
|
+
if (requireExisting && !initialAuthority) throw new Error("terminal_uncertain");
|
|
6361
6604
|
const activeTurnId = transaction.canonical.queue.active_turn_id;
|
|
6362
6605
|
const anotherActiveTurn = Object.values(transaction.canonical.turns).some(turn =>
|
|
6363
6606
|
ACTIVE_TURN_STATUSES.has(turn.status as TurnStatus),
|
|
@@ -6413,9 +6656,13 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
6413
6656
|
sdk_idempotency_key: idempotencyKey,
|
|
6414
6657
|
phase: "claimed",
|
|
6415
6658
|
coordinator_turn_id: reserved.turn_id,
|
|
6659
|
+
...(retainDelegateResponse ? { delegate_response_pending: true } : {}),
|
|
6416
6660
|
created_at: now,
|
|
6417
6661
|
updated_at: now,
|
|
6418
6662
|
};
|
|
6663
|
+
// This transaction replaces pre-dispatch authority with the pinned prompt
|
|
6664
|
+
// reservation. Missing historical requests can never reuse that authority.
|
|
6665
|
+
if (initialAuthority) delete transaction.recovery.initial_delegate_request;
|
|
6419
6666
|
});
|
|
6420
6667
|
return keyDigest;
|
|
6421
6668
|
}
|
|
@@ -7399,6 +7646,9 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
7399
7646
|
|
|
7400
7647
|
async function callTool(name: string, args: Record<string, unknown> = {}): Promise<Record<string, unknown>> {
|
|
7401
7648
|
try {
|
|
7649
|
+
// Run startup maintenance before any tool-specific idempotency/session
|
|
7650
|
+
// lock is acquired. Nested state initialization must never invert those locks.
|
|
7651
|
+
await ensureQuestionStateReady();
|
|
7402
7652
|
if (name === "gjc_coordinator_list_sessions") {
|
|
7403
7653
|
// listSessions() enumerates the broker across allowed roots, but every other
|
|
7404
7654
|
// coordinator tool resolves a session through its durable projection. Without
|
|
@@ -8196,18 +8446,54 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
8196
8446
|
...(delegateWorktree.name ? { worktree: delegateWorktree.name } : {}),
|
|
8197
8447
|
allow_mutation: true,
|
|
8198
8448
|
};
|
|
8449
|
+
const operation =
|
|
8450
|
+
args.force === true ? "turn.abort_and_prompt" : args.queue === true ? "turn.follow_up" : "turn.prompt";
|
|
8451
|
+
const requestKey = createHash("sha256").update(`${idempotencyKey}\0${operation}`).digest("hex");
|
|
8199
8452
|
let creationRemoteStarted = false;
|
|
8453
|
+
let delegateEffectStarted = false;
|
|
8454
|
+
let delegateResponseComplete = false;
|
|
8200
8455
|
return await withToolIdempotency(
|
|
8201
8456
|
name,
|
|
8202
8457
|
idempotencyKey,
|
|
8203
8458
|
canonicalArgs,
|
|
8204
|
-
async
|
|
8205
|
-
|
|
8459
|
+
async recovering => {
|
|
8460
|
+
// An older in-progress request can have archived authority. Missing
|
|
8461
|
+
// live state is not proof that this key never dispatched a prompt.
|
|
8462
|
+
delegateEffectStarted = recovering;
|
|
8463
|
+
const delegate = async (): Promise<Record<string, unknown>> => {
|
|
8464
|
+
// The outer key lock validates identity before entering here. Admission
|
|
8465
|
+
// is not a final response: an interrupted await must still be observed.
|
|
8466
|
+
const outer = await readCoordinatorIdempotencyFile(idempotencyFile(idempotencyKey));
|
|
8467
|
+
if (outer.kind !== "record") throw new Error("terminal_uncertain");
|
|
8468
|
+
const checkpoint = asRecord(outer.value.admission);
|
|
8469
|
+
if (checkpoint) {
|
|
8470
|
+
delegateEffectStarted = true;
|
|
8471
|
+
const response = asRecord(checkpoint.response);
|
|
8472
|
+
if (
|
|
8473
|
+
checkpoint.kind !== "delegate_admission" ||
|
|
8474
|
+
typeof checkpoint.session_id !== "string" ||
|
|
8475
|
+
!SAFE_EXTERNAL_ID_PATTERN.test(checkpoint.session_id) ||
|
|
8476
|
+
typeof checkpoint.turn_id !== "string" ||
|
|
8477
|
+
!TURN_ID_PATTERN.test(checkpoint.turn_id) ||
|
|
8478
|
+
!response ||
|
|
8479
|
+
response.ok !== true ||
|
|
8480
|
+
response.tool_name !== name ||
|
|
8481
|
+
response.workflow !== delegateWorkflow ||
|
|
8482
|
+
response.session_id !== checkpoint.session_id ||
|
|
8483
|
+
response.turn_id !== checkpoint.turn_id
|
|
8484
|
+
)
|
|
8485
|
+
throw new Error("terminal_uncertain");
|
|
8486
|
+
return response;
|
|
8487
|
+
}
|
|
8206
8488
|
let sessionId: string;
|
|
8207
8489
|
let session: Record<string, unknown>;
|
|
8208
8490
|
let creationKey: string | null = null;
|
|
8491
|
+
let initialDelegateRequest: CoordinatorSessionTransactionV1["recovery"]["initial_delegate_request"];
|
|
8209
8492
|
if (reusedSessionId) {
|
|
8210
8493
|
sessionId = reusedSessionId;
|
|
8494
|
+
const prior = await readSessionTransaction(questionPaths, sessionId);
|
|
8495
|
+
const prompt = prior?.requests.prompts[requestKey];
|
|
8496
|
+
delegateEffectStarted ||= Boolean(prompt && prompt.phase !== "claimed");
|
|
8211
8497
|
const existing = asRecord(await readJsonFile(sessionFile(sessionId)));
|
|
8212
8498
|
if (!existing)
|
|
8213
8499
|
return {
|
|
@@ -8264,10 +8550,20 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
8264
8550
|
});
|
|
8265
8551
|
} else {
|
|
8266
8552
|
const creation = await claimProductionCreation(name, idempotencyKey, canonicalArgs);
|
|
8267
|
-
|
|
8553
|
+
creationRemoteStarted = creation.request.phase !== "claimed";
|
|
8554
|
+
if (creation.request.phase === "completed" && creation.request.safe_response) {
|
|
8555
|
+
delegateEffectStarted = true;
|
|
8268
8556
|
return creation.request.safe_response;
|
|
8557
|
+
}
|
|
8269
8558
|
creationKey = creation.keyDigest;
|
|
8559
|
+
// claimProductionCreation has validated the caller key and all
|
|
8560
|
+
// canonical arguments, including the prompt mode and workflow.
|
|
8561
|
+
initialDelegateRequest = {
|
|
8562
|
+
key_digest: creation.keyDigest,
|
|
8563
|
+
request_digest: creation.request.request_digest,
|
|
8564
|
+
};
|
|
8270
8565
|
if (creation.request.canonical_create_intent) {
|
|
8566
|
+
delegateEffectStarted = true;
|
|
8271
8567
|
session = sessionFromCreationSnapshot(creation.request.canonical_create_intent.session);
|
|
8272
8568
|
await bindCreationRequest(
|
|
8273
8569
|
questionPaths,
|
|
@@ -8353,63 +8649,134 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
8353
8649
|
await bindCreationRequest(questionPaths, creation.keyDigest, intent);
|
|
8354
8650
|
await commitCreationWal(questionPaths, creation.keyDigest, intent);
|
|
8355
8651
|
}
|
|
8652
|
+
await services.afterDelegateCreationWalCommitted?.(sessionId);
|
|
8356
8653
|
}
|
|
8357
|
-
|
|
8358
|
-
|
|
8359
|
-
|
|
8360
|
-
|
|
8361
|
-
|
|
8362
|
-
|
|
8363
|
-
|
|
8364
|
-
|
|
8365
|
-
|
|
8654
|
+
const admit = async (): Promise<Record<string, unknown>> => {
|
|
8655
|
+
await writeJsonFile(sessionFile(sessionId), session);
|
|
8656
|
+
const previousActiveTurn =
|
|
8657
|
+
(await readActiveTurn(namespaceDir, sessionId)) ??
|
|
8658
|
+
(await readCanonicalActiveTurn(sessionId));
|
|
8659
|
+
const priorTransaction = await readSessionTransaction(questionPaths, sessionId);
|
|
8660
|
+
const priorPrompt = priorTransaction?.requests.prompts[requestKey];
|
|
8661
|
+
if (
|
|
8662
|
+
recovering &&
|
|
8663
|
+
!priorPrompt &&
|
|
8664
|
+
!hasInitialDelegatePromptAuthority(priorTransaction, initialDelegateRequest)
|
|
8665
|
+
)
|
|
8666
|
+
throw new Error("terminal_uncertain");
|
|
8667
|
+
if (priorPrompt && ["remote_started", "accepted", "completed"].includes(priorPrompt.phase))
|
|
8668
|
+
delegateEffectStarted = true;
|
|
8669
|
+
const reserved = priorPrompt?.coordinator_turn_id
|
|
8670
|
+
? priorTransaction?.canonical.turns[priorPrompt.coordinator_turn_id]
|
|
8671
|
+
: null;
|
|
8672
|
+
if (priorPrompt && !reserved) throw new Error("terminal_uncertain");
|
|
8673
|
+
if (
|
|
8674
|
+
priorPrompt &&
|
|
8675
|
+
priorPrompt.phase !== "completed" &&
|
|
8676
|
+
operation !== "turn.follow_up" &&
|
|
8677
|
+
!reserved?.terminal_fence &&
|
|
8678
|
+
priorTransaction?.canonical.queue.active_turn_id !== reserved?.turn_id
|
|
8679
|
+
)
|
|
8680
|
+
throw new Error("terminal_uncertain");
|
|
8681
|
+
// Recover this key's reservation before rejecting a busy session.
|
|
8682
|
+
if (!priorPrompt && previousActiveTurn && args.queue !== true && args.force !== true) {
|
|
8683
|
+
return {
|
|
8684
|
+
ok: false,
|
|
8685
|
+
error: {
|
|
8686
|
+
code: "active_turn_exists",
|
|
8687
|
+
message: `Session ${sessionId} already has active turn ${previousActiveTurn.turn_id}.`,
|
|
8688
|
+
},
|
|
8689
|
+
turn_id: previousActiveTurn.turn_id,
|
|
8690
|
+
};
|
|
8691
|
+
}
|
|
8692
|
+
const promptKey = await claimCanonicalPrompt(
|
|
8693
|
+
sessionId,
|
|
8694
|
+
taggedPrompt,
|
|
8695
|
+
operation,
|
|
8696
|
+
idempotencyKey,
|
|
8697
|
+
true,
|
|
8698
|
+
recovering,
|
|
8699
|
+
initialDelegateRequest,
|
|
8700
|
+
);
|
|
8701
|
+
if (reserved?.terminal_fence && !(await promptReceipt(sessionId, promptKey))) {
|
|
8702
|
+
delegateEffectStarted = true;
|
|
8703
|
+
throw new Error("terminal_uncertain");
|
|
8704
|
+
}
|
|
8705
|
+
let acknowledgement: RuntimePromptAcknowledgement;
|
|
8706
|
+
try {
|
|
8707
|
+
acknowledgement = await dispatchOrRecoverPrompt(
|
|
8708
|
+
session,
|
|
8709
|
+
sessionId,
|
|
8710
|
+
operation,
|
|
8711
|
+
taggedPrompt,
|
|
8712
|
+
idempotencyKey,
|
|
8713
|
+
promptKey,
|
|
8714
|
+
);
|
|
8715
|
+
} catch (error) {
|
|
8716
|
+
// Receipt-first failures on reused sessions must remain recoverable.
|
|
8717
|
+
delegateEffectStarted ||= !(error instanceof SdkClientError);
|
|
8718
|
+
delegateEffectStarted ||= (await promptReceipt(sessionId, promptKey)) !== null;
|
|
8719
|
+
throw error;
|
|
8720
|
+
}
|
|
8721
|
+
delegateEffectStarted = true;
|
|
8722
|
+
const turn = await recordAcceptedPrompt(
|
|
8723
|
+
sessionId,
|
|
8724
|
+
taggedPrompt,
|
|
8725
|
+
operation,
|
|
8726
|
+
reserved?.terminal_fence || previousActiveTurn?.turn_id === reserved?.turn_id
|
|
8727
|
+
? null
|
|
8728
|
+
: previousActiveTurn,
|
|
8729
|
+
acknowledgement,
|
|
8730
|
+
promptKey,
|
|
8731
|
+
);
|
|
8732
|
+
const codexHandoff = await autoBindDelegateCodexHandoff(
|
|
8733
|
+
namespaceDir,
|
|
8734
|
+
canonicalCwd,
|
|
8735
|
+
sessionId,
|
|
8736
|
+
turn.turn_id,
|
|
8737
|
+
delegateWorkflow,
|
|
8738
|
+
explicitHostWorkUnit,
|
|
8739
|
+
);
|
|
8740
|
+
await appendCoordinatorEvent(namespaceDir, {
|
|
8741
|
+
stableId: `delegation:${sessionId}:${turn.turn_id}`,
|
|
8742
|
+
kind: "delegation.started",
|
|
8743
|
+
sessionId,
|
|
8744
|
+
turnId: turn.turn_id,
|
|
8745
|
+
summary: `Delegated ${delegateWorkflow} via ${name} on session ${sessionId}`,
|
|
8746
|
+
metadata: {
|
|
8747
|
+
workflow: delegateWorkflow,
|
|
8748
|
+
tool_name: name,
|
|
8749
|
+
session_id: sessionId,
|
|
8750
|
+
turn_id: turn.turn_id,
|
|
8751
|
+
active_turn_id: turn.delivery.queued
|
|
8752
|
+
? (previousActiveTurn?.turn_id ?? null)
|
|
8753
|
+
: turn.turn_id,
|
|
8754
|
+
status: turn.status,
|
|
8755
|
+
queued: turn.delivery.queued,
|
|
8756
|
+
delivered: turn.delivery.delivered,
|
|
8757
|
+
delivery: turn.delivery,
|
|
8758
|
+
session: publicCoordinatorSession(session),
|
|
8759
|
+
session_state: publicCoordinatorSessionState(
|
|
8760
|
+
await readSessionState(namespaceDir, sessionId),
|
|
8761
|
+
),
|
|
8762
|
+
turn: boundedPublicValue(turn, { remaining: COORDINATOR_IDEMPOTENCY_RESPONSE_BYTE_CAP }),
|
|
8763
|
+
result: publicSdkAcknowledgement(acknowledgement),
|
|
8764
|
+
codex_handoff: codexHandoff,
|
|
8765
|
+
endpoint_incarnation: session.endpoint_incarnation,
|
|
8766
|
+
...(hasTask && hasPrompt ? { prompt_alias_ignored: true } : {}),
|
|
8366
8767
|
},
|
|
8367
|
-
|
|
8368
|
-
|
|
8369
|
-
|
|
8370
|
-
const operation =
|
|
8371
|
-
args.force === true
|
|
8372
|
-
? "turn.abort_and_prompt"
|
|
8373
|
-
: args.queue === true
|
|
8374
|
-
? "turn.follow_up"
|
|
8375
|
-
: "turn.prompt";
|
|
8376
|
-
const promptKey = await claimCanonicalPrompt(sessionId, taggedPrompt, operation, idempotencyKey);
|
|
8377
|
-
const acknowledgement = await dispatchOrRecoverPrompt(
|
|
8378
|
-
session,
|
|
8379
|
-
sessionId,
|
|
8380
|
-
operation,
|
|
8381
|
-
taggedPrompt,
|
|
8382
|
-
idempotencyKey,
|
|
8383
|
-
promptKey,
|
|
8384
|
-
);
|
|
8385
|
-
const turn = await recordAcceptedPrompt(
|
|
8386
|
-
sessionId,
|
|
8387
|
-
taggedPrompt,
|
|
8388
|
-
operation,
|
|
8389
|
-
previousActiveTurn,
|
|
8390
|
-
acknowledgement,
|
|
8391
|
-
promptKey,
|
|
8392
|
-
);
|
|
8393
|
-
const codexHandoff = await autoBindDelegateCodexHandoff(
|
|
8394
|
-
namespaceDir,
|
|
8395
|
-
canonicalCwd,
|
|
8396
|
-
sessionId,
|
|
8397
|
-
turn.turn_id,
|
|
8398
|
-
delegateWorkflow,
|
|
8399
|
-
explicitHostWorkUnit,
|
|
8400
|
-
);
|
|
8401
|
-
await appendCoordinatorEvent(namespaceDir, {
|
|
8402
|
-
stableId: `delegation:${sessionId}:${turn.turn_id}`,
|
|
8403
|
-
kind: "delegation.started",
|
|
8404
|
-
sessionId,
|
|
8405
|
-
turnId: turn.turn_id,
|
|
8406
|
-
summary: `Delegated ${delegateWorkflow} via ${name} on session ${sessionId}`,
|
|
8407
|
-
metadata: {
|
|
8768
|
+
});
|
|
8769
|
+
const response = {
|
|
8770
|
+
ok: true,
|
|
8408
8771
|
workflow: delegateWorkflow,
|
|
8409
8772
|
tool_name: name,
|
|
8410
8773
|
session_id: sessionId,
|
|
8411
8774
|
turn_id: turn.turn_id,
|
|
8412
|
-
active_turn_id: turn.delivery.queued
|
|
8775
|
+
active_turn_id: turn.delivery.queued
|
|
8776
|
+
? (previousActiveTurn?.turn_id ?? null)
|
|
8777
|
+
: TERMINAL_TURN_STATUSES.has(turn.status)
|
|
8778
|
+
? null
|
|
8779
|
+
: turn.turn_id,
|
|
8413
8780
|
status: turn.status,
|
|
8414
8781
|
queued: turn.delivery.queued,
|
|
8415
8782
|
delivered: turn.delivery.delivered,
|
|
@@ -8421,52 +8788,61 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
8421
8788
|
turn: boundedPublicValue(turn, { remaining: COORDINATOR_IDEMPOTENCY_RESPONSE_BYTE_CAP }),
|
|
8422
8789
|
result: publicSdkAcknowledgement(acknowledgement),
|
|
8423
8790
|
codex_handoff: codexHandoff,
|
|
8424
|
-
endpoint_incarnation: session.endpoint_incarnation,
|
|
8425
8791
|
...(hasTask && hasPrompt ? { prompt_alias_ignored: true } : {}),
|
|
8426
|
-
}
|
|
8427
|
-
|
|
8428
|
-
|
|
8429
|
-
|
|
8430
|
-
|
|
8431
|
-
|
|
8432
|
-
|
|
8433
|
-
|
|
8434
|
-
|
|
8435
|
-
|
|
8436
|
-
:
|
|
8437
|
-
|
|
8438
|
-
|
|
8439
|
-
|
|
8440
|
-
|
|
8441
|
-
|
|
8442
|
-
|
|
8443
|
-
|
|
8444
|
-
|
|
8445
|
-
|
|
8446
|
-
|
|
8447
|
-
|
|
8448
|
-
|
|
8792
|
+
};
|
|
8793
|
+
const admission: DelegateAdmissionCheckpoint = {
|
|
8794
|
+
kind: "delegate_admission",
|
|
8795
|
+
session_id: sessionId,
|
|
8796
|
+
turn_id: turn.turn_id,
|
|
8797
|
+
response: boundedToolResponse(name, response),
|
|
8798
|
+
};
|
|
8799
|
+
const delegateResponsePin: DelegateResponsePinV1 = {
|
|
8800
|
+
session_id: sessionId,
|
|
8801
|
+
coordinator_turn_id: turn.turn_id,
|
|
8802
|
+
prompt_key_digest: promptKey,
|
|
8803
|
+
runtime_command_id: acknowledgement.command_id,
|
|
8804
|
+
runtime_turn_id: acknowledgement.turn_id,
|
|
8805
|
+
};
|
|
8806
|
+
await writeCoordinatorIdempotencyFile(idempotencyFile(idempotencyKey), {
|
|
8807
|
+
...(outer.value as unknown as CoordinatorToolIdempotencyRecord),
|
|
8808
|
+
admission,
|
|
8809
|
+
delegate_response_pin: delegateResponsePin,
|
|
8810
|
+
});
|
|
8811
|
+
// A completed creation must never outlive the outer recovery identity.
|
|
8812
|
+
if (creationKey) {
|
|
8813
|
+
await advanceCreationReceipt(questionPaths, creationKey, "projected", response);
|
|
8814
|
+
await advanceCreationReceipt(questionPaths, creationKey, "completed", response);
|
|
8815
|
+
await services.afterDelegateCreationCompleted?.();
|
|
8816
|
+
}
|
|
8817
|
+
await services.afterDelegateAdmission?.(sessionId);
|
|
8818
|
+
return admission.response;
|
|
8449
8819
|
};
|
|
8450
|
-
|
|
8451
|
-
|
|
8452
|
-
|
|
8453
|
-
|
|
8454
|
-
|
|
8820
|
+
return reusedSessionId ? await admit() : await withSessionTransition(sessionId, admit);
|
|
8821
|
+
};
|
|
8822
|
+
const response = reusedSessionId
|
|
8823
|
+
? await withSessionTransition(reusedSessionId, delegate)
|
|
8824
|
+
: await delegate();
|
|
8825
|
+
// Observation owns the same key flight, not SESSION: force/queue may
|
|
8826
|
+
// now dispatch while the full response awaits its durable commit.
|
|
8827
|
+
const complete =
|
|
8828
|
+
response.ok === true && args.await_completion === true
|
|
8455
8829
|
? {
|
|
8456
8830
|
...response,
|
|
8457
8831
|
completion: await awaitTurnPayload(
|
|
8458
|
-
|
|
8459
|
-
|
|
8832
|
+
response.turn_id,
|
|
8833
|
+
response.session_id,
|
|
8460
8834
|
args.timeout_ms,
|
|
8461
8835
|
args.poll_interval_ms,
|
|
8462
8836
|
),
|
|
8463
8837
|
}
|
|
8464
8838
|
: response;
|
|
8465
|
-
|
|
8466
|
-
return
|
|
8839
|
+
delegateResponseComplete = response.ok === true;
|
|
8840
|
+
return complete;
|
|
8467
8841
|
},
|
|
8468
8842
|
true,
|
|
8469
|
-
response =>
|
|
8843
|
+
response =>
|
|
8844
|
+
(!delegateResponseComplete && (creationRemoteStarted || delegateEffectStarted)) ||
|
|
8845
|
+
isRouterRequestAmbiguous(response),
|
|
8470
8846
|
);
|
|
8471
8847
|
}
|
|
8472
8848
|
if (name === "gjc_coordinator_stop_session") {
|