@gajae-code/coding-agent 0.15.2 → 0.15.3
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 +37 -1
- package/dist/types/config/autorouting-tier-map.d.ts +0 -9
- package/dist/types/config/file-lock.d.ts +7 -1
- package/dist/types/config/keybindings.d.ts +13 -3
- package/dist/types/config/model-registry.d.ts +5 -0
- package/dist/types/config/model-resolver.d.ts +1 -0
- package/dist/types/config/settings-schema.d.ts +13 -1
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/coordinator-mcp/question-state.d.ts +42 -2
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +12 -0
- package/dist/types/main.d.ts +1 -1
- package/dist/types/modes/action-registry.d.ts +1 -0
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +1 -1
- package/dist/types/modes/components/welcome.d.ts +2 -0
- package/dist/types/modes/controllers/input-controller.d.ts +16 -1
- package/dist/types/modes/controllers/runtime-mcp-command-controller.d.ts +42 -0
- package/dist/types/modes/interactive-mode.d.ts +7 -0
- package/dist/types/modes/shared/oauth-url-copy.d.ts +9 -0
- package/dist/types/modes/types.d.ts +3 -0
- package/dist/types/modes/ui-language.d.ts +2 -1
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +1 -0
- package/dist/types/sdk/broker/session-index.d.ts +18 -0
- package/dist/types/sdk/bus/index.d.ts +4 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +2 -0
- package/dist/types/sdk/cli/session-cli.d.ts +1 -1
- package/dist/types/sdk/lifecycle/service.d.ts +32 -5
- package/dist/types/sdk/session.d.ts +5 -0
- package/dist/types/session/agent-session.d.ts +4 -0
- package/dist/types/session/fallback-chain-controller.d.ts +8 -0
- package/dist/types/slash-commands/helpers/aside.d.ts +54 -0
- package/dist/types/tools/ask.d.ts +8 -0
- package/dist/types/utils/clipboard.d.ts +9 -1
- package/package.json +7 -7
- package/scripts/generate-sdk-adapter-parity-manifest.ts +5 -2
- package/scripts/generate-sdk-operation-inventory.ts +3 -0
- package/src/commands/sdk.ts +2 -2
- package/src/config/autorouting-tier-map.ts +0 -9
- package/src/config/file-lock.ts +15 -1
- package/src/config/keybindings.ts +13 -3
- package/src/config/model-registry.ts +132 -21
- package/src/config/model-resolver.ts +2 -1
- package/src/config/models-config-schema.ts +2 -2
- package/src/config/settings-schema.ts +13 -1
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/question-state.ts +235 -4
- package/src/coordinator-mcp/server.ts +402 -2
- package/src/dap/client.ts +19 -8
- package/src/gjc-runtime/deep-interview-stage.ts +20 -1
- package/src/gjc-runtime/deep-interview-state.ts +3 -1
- package/src/gjc-runtime/session-state-lock.ts +11 -2
- package/src/internal-urls/docs-index.generated.ts +8 -7
- package/src/lsp/client.ts +39 -4
- package/src/main.ts +42 -11
- package/src/modes/action-registry.ts +7 -0
- package/src/modes/components/iterm-pet-transport.ts +4 -2
- package/src/modes/components/runtime-mcp-add-wizard.ts +38 -1
- package/src/modes/components/settings-selector.ts +1 -0
- package/src/modes/components/tool-status-header.ts +149 -41
- package/src/modes/components/welcome.ts +8 -0
- package/src/modes/controllers/goal-mode-controller.ts +7 -2
- package/src/modes/controllers/input-controller.ts +141 -7
- package/src/modes/controllers/plan-mode-controller.ts +12 -4
- package/src/modes/controllers/runtime-mcp-command-controller.ts +164 -20
- package/src/modes/controllers/selector-controller.ts +11 -2
- package/src/modes/interactive-mode.ts +88 -1
- package/src/modes/shared/oauth-url-copy.ts +33 -0
- package/src/modes/types.ts +3 -0
- package/src/modes/ui-language.ts +44 -3
- package/src/runtime-mcp/oauth-flow.ts +19 -8
- package/src/runtime-mcp/transports/stdio.ts +27 -13
- package/src/sdk/acp/adapter.ts +29 -2
- package/src/sdk/broker/broker.ts +26 -6
- package/src/sdk/broker/lifecycle-ledger.ts +28 -1
- package/src/sdk/broker/lifecycle.ts +664 -5
- package/src/sdk/broker/session-index.ts +246 -0
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/bus/index.ts +135 -38
- package/src/sdk/bus/natives-augment.d.ts +20 -1
- package/src/sdk/bus/telegram-daemon-contract.ts +4 -1
- package/src/sdk/bus/telegram-daemon.ts +3 -0
- package/src/sdk/cli/session-cli.ts +34 -7
- package/src/sdk/lifecycle/service.ts +166 -11
- package/src/sdk/mcp/server.ts +2 -1
- package/src/sdk/protocol/operation-inventory.generated.json +40 -0
- package/src/sdk/protocol/operation-registry.ts +2 -1
- package/src/sdk/session.ts +91 -7
- package/src/session/agent-session.ts +403 -56
- package/src/session/fallback-chain-controller.ts +17 -0
- package/src/setup/frictionless-onboarding.ts +4 -4
- package/src/slash-commands/aside-command.test.ts +366 -0
- package/src/slash-commands/builtin-registry.ts +19 -0
- package/src/slash-commands/helpers/aside.ts +299 -0
- package/src/tools/ask.ts +8 -0
- package/src/utils/clipboard.ts +37 -28
|
@@ -103,6 +103,7 @@ import {
|
|
|
103
103
|
advanceDeletion,
|
|
104
104
|
advanceDeliveryDiscoveryCursor,
|
|
105
105
|
advanceSchedulerCursor,
|
|
106
|
+
assertCreationRetirementIdentity,
|
|
106
107
|
bindCreationRequest,
|
|
107
108
|
type CanonicalCreateIntentV1,
|
|
108
109
|
type CanonicalReportSnapshotV1,
|
|
@@ -110,6 +111,8 @@ import {
|
|
|
110
111
|
COORDINATOR_REPORT_ID_PATTERN,
|
|
111
112
|
COORDINATOR_SESSION_ID_PATTERN,
|
|
112
113
|
type CoordinatorSessionTransactionV1,
|
|
114
|
+
type CreationRetirementBrokerProofV1,
|
|
115
|
+
type CreationRetirementProofV1,
|
|
113
116
|
claimCreationRequest,
|
|
114
117
|
claimPublicDelivery,
|
|
115
118
|
commitCreationWal,
|
|
@@ -129,10 +132,13 @@ import {
|
|
|
129
132
|
readSchedulerRoster,
|
|
130
133
|
readSessionTransaction,
|
|
131
134
|
reconcileCreationRemoteVerifier,
|
|
135
|
+
recordCreationRetirementBrokerProof,
|
|
136
|
+
recordCreationRetirementIntent,
|
|
132
137
|
recoverExpiredPublicDelivery,
|
|
133
138
|
releasePublicDeliveryClaim,
|
|
134
139
|
removeSessionTransaction,
|
|
135
140
|
repairProjections,
|
|
141
|
+
replaceCreationRetirementIntent,
|
|
136
142
|
rotateClaimedCreationVerifier,
|
|
137
143
|
startCreationRemote,
|
|
138
144
|
withAdmittedSessionTransaction,
|
|
@@ -594,6 +600,46 @@ function toolSchema(name: CoordinatorToolName): {
|
|
|
594
600
|
},
|
|
595
601
|
};
|
|
596
602
|
}
|
|
603
|
+
if (name === "gjc_coordinator_retire_start_session") {
|
|
604
|
+
return {
|
|
605
|
+
name,
|
|
606
|
+
description:
|
|
607
|
+
"Retire a stranded start_session idempotency intent only after the exact indexed terminal-uncertain session identity has been proven exited and its endpoint is absent. This never signals or deletes a live process.",
|
|
608
|
+
inputSchema: {
|
|
609
|
+
type: "object",
|
|
610
|
+
properties: {
|
|
611
|
+
cwd,
|
|
612
|
+
session_id: sessionId,
|
|
613
|
+
state_root: { type: "string", description: "Exact indexed SDK state root." },
|
|
614
|
+
endpoint_generation: { type: "number" },
|
|
615
|
+
endpoint_mtime_ms: { type: "number" },
|
|
616
|
+
process_incarnation: { type: "string" },
|
|
617
|
+
host_incarnation: { type: "string" },
|
|
618
|
+
lifecycle_request_id: { type: "string" },
|
|
619
|
+
remote_create_key: { type: "string" },
|
|
620
|
+
creation_idempotency_key: { type: "string" },
|
|
621
|
+
request_digest: { type: "string", description: "SHA-256 of the original canonical start request." },
|
|
622
|
+
idempotency_key: idempotencyKey,
|
|
623
|
+
allow_mutation: allowMutation,
|
|
624
|
+
},
|
|
625
|
+
required: [
|
|
626
|
+
"cwd",
|
|
627
|
+
"session_id",
|
|
628
|
+
"state_root",
|
|
629
|
+
"endpoint_generation",
|
|
630
|
+
"endpoint_mtime_ms",
|
|
631
|
+
"process_incarnation",
|
|
632
|
+
"host_incarnation",
|
|
633
|
+
"lifecycle_request_id",
|
|
634
|
+
"remote_create_key",
|
|
635
|
+
"creation_idempotency_key",
|
|
636
|
+
"request_digest",
|
|
637
|
+
"idempotency_key",
|
|
638
|
+
"allow_mutation",
|
|
639
|
+
],
|
|
640
|
+
},
|
|
641
|
+
};
|
|
642
|
+
}
|
|
597
643
|
if (name === "gjc_coordinator_activate_session") {
|
|
598
644
|
return {
|
|
599
645
|
name,
|
|
@@ -1152,8 +1198,11 @@ const PUBLIC_ERROR_MESSAGES: Record<string, string> = {
|
|
|
1152
1198
|
codex_authenticated_handoff_unavailable_windows:
|
|
1153
1199
|
"Authenticated Codex token-file handoff is unavailable on native Windows.",
|
|
1154
1200
|
endpoint_stale: "Coordinator session endpoint is stale.",
|
|
1201
|
+
retirement_proof_stale: "Coordinator retirement proof is stale before effect start.",
|
|
1155
1202
|
ambiguous: "Coordinator request outcome is ambiguous.",
|
|
1156
1203
|
terminal_uncertain: "Coordinator state is uncertain.",
|
|
1204
|
+
retired: "The coordinator intent was retired after exact identity proof.",
|
|
1205
|
+
retire_not_allowed: "The coordinator intent is not eligible for retirement.",
|
|
1157
1206
|
idempotency_conflict: "Idempotency key conflicts with a different request.",
|
|
1158
1207
|
idempotency_in_progress: "A previous coordinator mutation is still in progress.",
|
|
1159
1208
|
protocol_error: "Coordinator protocol response is invalid.",
|
|
@@ -4979,6 +5028,12 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
4979
5028
|
return key;
|
|
4980
5029
|
}
|
|
4981
5030
|
|
|
5031
|
+
function requiredString(value: unknown, field: string): string {
|
|
5032
|
+
if (typeof value !== "string" || value.length === 0)
|
|
5033
|
+
throw new SdkClientError("invalid_request", `${field} is required.`);
|
|
5034
|
+
return value;
|
|
5035
|
+
}
|
|
5036
|
+
|
|
4982
5037
|
/**
|
|
4983
5038
|
* Path for the lock owner file that serializes idempotency mutations.
|
|
4984
5039
|
*
|
|
@@ -4997,6 +5052,15 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
4997
5052
|
const keyDigest = createHash("sha256").update(idempotencyKey).digest("hex");
|
|
4998
5053
|
return path.join(namespaceDir, "idempotency", `${keyDigest}.json`);
|
|
4999
5054
|
}
|
|
5055
|
+
async function withOrderedSessionStateLocks<T>(
|
|
5056
|
+
lockFiles: readonly string[],
|
|
5057
|
+
operation: () => Promise<T>,
|
|
5058
|
+
): Promise<T> {
|
|
5059
|
+
const ordered = [...new Set(lockFiles)].sort();
|
|
5060
|
+
const acquire = async (index: number): Promise<T> =>
|
|
5061
|
+
index === ordered.length ? operation() : withSessionStateLock(ordered[index]!, () => acquire(index + 1));
|
|
5062
|
+
return acquire(0);
|
|
5063
|
+
}
|
|
5000
5064
|
|
|
5001
5065
|
/**
|
|
5002
5066
|
* Run one mutation under its idempotency key, then seal its response as the
|
|
@@ -5018,6 +5082,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
5018
5082
|
operation: () => Promise<Record<string, unknown>>,
|
|
5019
5083
|
recoverInProgress = false,
|
|
5020
5084
|
isNonterminal: (response: Record<string, unknown>) => boolean = isRouterRequestAmbiguous,
|
|
5085
|
+
lockAlreadyHeld = false,
|
|
5021
5086
|
): Promise<Record<string, unknown>> {
|
|
5022
5087
|
const keyDigest = createHash("sha256").update(idempotencyKey).digest("hex");
|
|
5023
5088
|
const requestDigest = createHash("sha256")
|
|
@@ -5025,7 +5090,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
5025
5090
|
.digest("hex");
|
|
5026
5091
|
const file = idempotencyFile(idempotencyKey);
|
|
5027
5092
|
const lockFile = idempotencyLockFile(idempotencyKey);
|
|
5028
|
-
|
|
5093
|
+
const execute = async (): Promise<Record<string, unknown>> => {
|
|
5029
5094
|
const existingFile = await readCoordinatorIdempotencyFile(file);
|
|
5030
5095
|
if (existingFile.kind === "corrupt")
|
|
5031
5096
|
return {
|
|
@@ -5121,7 +5186,8 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
5121
5186
|
completed_at: new Date().toISOString(),
|
|
5122
5187
|
});
|
|
5123
5188
|
return response;
|
|
5124
|
-
}
|
|
5189
|
+
};
|
|
5190
|
+
return lockAlreadyHeld ? execute() : withSessionStateLock(lockFile, execute);
|
|
5125
5191
|
}
|
|
5126
5192
|
|
|
5127
5193
|
async function brokerSession(
|
|
@@ -5264,6 +5330,61 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
5264
5330
|
return result;
|
|
5265
5331
|
}
|
|
5266
5332
|
|
|
5333
|
+
function strictBrokerRetirementProof(
|
|
5334
|
+
value: unknown,
|
|
5335
|
+
expected: CreationRetirementProofV1,
|
|
5336
|
+
): { broker: CreationRetirementBrokerProofV1; public: Record<string, unknown> } {
|
|
5337
|
+
const response = asRecord(value);
|
|
5338
|
+
if (response?.ok === false) brokerResult(response);
|
|
5339
|
+
const result = response?.ok === true ? asRecord(response.result) : null;
|
|
5340
|
+
if (!result)
|
|
5341
|
+
throw new SdkClientError("protocol_error", "SDK broker returned a malformed retirement acknowledgement.");
|
|
5342
|
+
const broker = {
|
|
5343
|
+
session_id: typeof result.sessionId === "string" ? result.sessionId : "",
|
|
5344
|
+
retired: result.retired,
|
|
5345
|
+
ledger_state: result.ledgerState,
|
|
5346
|
+
index_type: result.indexType,
|
|
5347
|
+
state_root: typeof result.stateRoot === "string" ? result.stateRoot : "",
|
|
5348
|
+
endpoint_generation: typeof result.endpointGeneration === "number" ? result.endpointGeneration : Number.NaN,
|
|
5349
|
+
endpoint_mtime_ms: typeof result.endpointMtimeMs === "number" ? result.endpointMtimeMs : Number.NaN,
|
|
5350
|
+
process_incarnation: typeof result.processIncarnation === "string" ? result.processIncarnation : "",
|
|
5351
|
+
host_incarnation: typeof result.hostIncarnation === "string" ? result.hostIncarnation : "",
|
|
5352
|
+
lifecycle_request_id: typeof result.lifecycleRequestId === "string" ? result.lifecycleRequestId : "",
|
|
5353
|
+
remote_create_key: typeof result.remoteCreateKey === "string" ? result.remoteCreateKey : "",
|
|
5354
|
+
} as unknown as CreationRetirementBrokerProofV1;
|
|
5355
|
+
if (
|
|
5356
|
+
broker.session_id !== expected.session_id ||
|
|
5357
|
+
broker.retired !== true ||
|
|
5358
|
+
broker.ledger_state !== "terminal_error" ||
|
|
5359
|
+
broker.index_type !== "session_closed" ||
|
|
5360
|
+
broker.state_root !== expected.state_root ||
|
|
5361
|
+
broker.endpoint_generation !== expected.endpoint_generation ||
|
|
5362
|
+
broker.endpoint_mtime_ms !== expected.endpoint_mtime_ms ||
|
|
5363
|
+
broker.process_incarnation !== expected.process_incarnation ||
|
|
5364
|
+
broker.host_incarnation !== expected.host_incarnation ||
|
|
5365
|
+
broker.lifecycle_request_id !== expected.lifecycle_request_id ||
|
|
5366
|
+
broker.remote_create_key !== expected.remote_create_key
|
|
5367
|
+
)
|
|
5368
|
+
throw new SdkClientError("protocol_error", "SDK broker returned an unbound retirement proof.");
|
|
5369
|
+
return {
|
|
5370
|
+
broker,
|
|
5371
|
+
public: publicRetirementProof(broker),
|
|
5372
|
+
};
|
|
5373
|
+
}
|
|
5374
|
+
|
|
5375
|
+
function coordinatorRetiredResponse(sessionId: string, lifecycle: Record<string, unknown>): Record<string, unknown> {
|
|
5376
|
+
return { ok: true, session_id: sessionId, retired: true, lifecycle };
|
|
5377
|
+
}
|
|
5378
|
+
|
|
5379
|
+
function publicRetirementProof(proof: CreationRetirementBrokerProofV1): Record<string, unknown> {
|
|
5380
|
+
return {
|
|
5381
|
+
sessionId: proof.session_id,
|
|
5382
|
+
retired: true,
|
|
5383
|
+
ledgerState: proof.ledger_state,
|
|
5384
|
+
indexType: proof.index_type,
|
|
5385
|
+
};
|
|
5386
|
+
}
|
|
5387
|
+
|
|
5267
5388
|
function strictBrokerSessionClose(value: unknown, expectedSessionId: string): Record<string, unknown> {
|
|
5268
5389
|
const result = strictBrokerResult(value, "session.close");
|
|
5269
5390
|
const sessionId = optionalString(result.sessionId ?? result.session_id);
|
|
@@ -8572,6 +8693,285 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
8572
8693
|
isUnobservedCompensation(response) || creationRemoteStarted || isRouterRequestAmbiguous(response),
|
|
8573
8694
|
);
|
|
8574
8695
|
}
|
|
8696
|
+
if (name === "gjc_coordinator_retire_start_session") {
|
|
8697
|
+
requireCoordinatorMutation(config, "sessions", args);
|
|
8698
|
+
const retirementKey = requiredIdempotencyKey(args);
|
|
8699
|
+
const creationKey = requiredString(args.creation_idempotency_key, "creation_idempotency_key");
|
|
8700
|
+
const requestDigest = requiredString(args.request_digest, "request_digest");
|
|
8701
|
+
if (!/^[a-f0-9]{64}$/.test(requestDigest))
|
|
8702
|
+
return {
|
|
8703
|
+
ok: false,
|
|
8704
|
+
error: { code: "invalid_input", message: "request_digest must be a SHA-256 hash." },
|
|
8705
|
+
};
|
|
8706
|
+
if (typeof args.session_id !== "string" || !COORDINATOR_SESSION_ID_PATTERN.test(args.session_id))
|
|
8707
|
+
return {
|
|
8708
|
+
ok: false,
|
|
8709
|
+
error: { code: "invalid_input", message: "session_id must be a canonical safe identifier." },
|
|
8710
|
+
};
|
|
8711
|
+
const sessionId = safeExternalId("session", args.session_id);
|
|
8712
|
+
const cwd = await canonicalBrokerWorkspace(await assertCoordinatorWorkdir(config, args.cwd));
|
|
8713
|
+
const proofString = (value: unknown, maxLength: number): string | null =>
|
|
8714
|
+
typeof value === "string" &&
|
|
8715
|
+
value.length > 0 &&
|
|
8716
|
+
value.length <= maxLength &&
|
|
8717
|
+
value.trim() === value &&
|
|
8718
|
+
![...value].some(character => character <= "\u001f" || character === "\u007f")
|
|
8719
|
+
? value
|
|
8720
|
+
: null;
|
|
8721
|
+
const stateRootInput = proofString(args.state_root, 4096);
|
|
8722
|
+
const stateRoot = stateRootInput === null ? null : path.resolve(stateRootInput);
|
|
8723
|
+
const remoteCreateKey = proofString(args.remote_create_key, 256);
|
|
8724
|
+
const lifecycleRequestId = proofString(args.lifecycle_request_id, 128);
|
|
8725
|
+
const processIncarnation = proofString(args.process_incarnation, 256);
|
|
8726
|
+
const hostIncarnation = proofString(args.host_incarnation, 256);
|
|
8727
|
+
const endpointGeneration = args.endpoint_generation;
|
|
8728
|
+
const endpointMtimeMs = args.endpoint_mtime_ms;
|
|
8729
|
+
if (
|
|
8730
|
+
stateRoot === null ||
|
|
8731
|
+
remoteCreateKey === null ||
|
|
8732
|
+
lifecycleRequestId === null ||
|
|
8733
|
+
!/^[A-Za-z0-9._-]+$/u.test(lifecycleRequestId) ||
|
|
8734
|
+
processIncarnation === null ||
|
|
8735
|
+
hostIncarnation === null ||
|
|
8736
|
+
!path.isAbsolute(stateRoot) ||
|
|
8737
|
+
typeof endpointGeneration !== "number" ||
|
|
8738
|
+
!Number.isSafeInteger(endpointGeneration) ||
|
|
8739
|
+
endpointGeneration <= 0 ||
|
|
8740
|
+
typeof endpointMtimeMs !== "number" ||
|
|
8741
|
+
!Number.isFinite(endpointMtimeMs) ||
|
|
8742
|
+
endpointMtimeMs <= 0 ||
|
|
8743
|
+
path.resolve(stateRoot) !== path.join(cwd, ".gjc", "state")
|
|
8744
|
+
)
|
|
8745
|
+
return {
|
|
8746
|
+
ok: false,
|
|
8747
|
+
error: { code: "invalid_input", message: "Retirement proof fields are invalid or out of scope." },
|
|
8748
|
+
};
|
|
8749
|
+
const canonicalArgs = {
|
|
8750
|
+
cwd,
|
|
8751
|
+
session_id: sessionId,
|
|
8752
|
+
creation_idempotency_key: creationKey,
|
|
8753
|
+
request_digest: requestDigest,
|
|
8754
|
+
allow_mutation: true,
|
|
8755
|
+
};
|
|
8756
|
+
const proof: CreationRetirementProofV1 = {
|
|
8757
|
+
session_id: sessionId,
|
|
8758
|
+
cwd,
|
|
8759
|
+
state_root: stateRoot,
|
|
8760
|
+
endpoint_generation: endpointGeneration,
|
|
8761
|
+
endpoint_mtime_ms: endpointMtimeMs,
|
|
8762
|
+
process_incarnation: processIncarnation,
|
|
8763
|
+
host_incarnation: hostIncarnation,
|
|
8764
|
+
lifecycle_request_id: lifecycleRequestId,
|
|
8765
|
+
remote_create_key: remoteCreateKey,
|
|
8766
|
+
};
|
|
8767
|
+
const creationKeyDigest = creationDigests("gjc_coordinator_start_session", creationKey, {}).keyDigest;
|
|
8768
|
+
const brokerRequestKey = `coordinator-retire:${creationKeyDigest}:${createHash("sha256")
|
|
8769
|
+
.update(JSON.stringify(proof))
|
|
8770
|
+
.digest("hex")}`;
|
|
8771
|
+
const retirementKeyDigest = createHash("sha256").update(retirementKey).digest("hex");
|
|
8772
|
+
const isRetirementRetryable = (response: Record<string, unknown>): boolean => {
|
|
8773
|
+
const code = asRecord(response.error)?.code;
|
|
8774
|
+
return (
|
|
8775
|
+
code === "protocol_error" ||
|
|
8776
|
+
code === "terminal_uncertain" ||
|
|
8777
|
+
code === "state_corrupt" ||
|
|
8778
|
+
code === "unavailable" ||
|
|
8779
|
+
code === "broker_unavailable" ||
|
|
8780
|
+
code === "broker_request_unavailable" ||
|
|
8781
|
+
code === "broker_transport_unavailable" ||
|
|
8782
|
+
code === "endpoint_stale" ||
|
|
8783
|
+
code === "retirement_proof_stale" ||
|
|
8784
|
+
code === "not_found" ||
|
|
8785
|
+
code === "live_session"
|
|
8786
|
+
);
|
|
8787
|
+
};
|
|
8788
|
+
return await withOrderedSessionStateLocks(
|
|
8789
|
+
[idempotencyLockFile(creationKey), idempotencyLockFile(retirementKey)],
|
|
8790
|
+
async () =>
|
|
8791
|
+
await withToolIdempotency(
|
|
8792
|
+
name,
|
|
8793
|
+
retirementKey,
|
|
8794
|
+
canonicalArgs,
|
|
8795
|
+
async () => {
|
|
8796
|
+
const originalFile = idempotencyFile(creationKey);
|
|
8797
|
+
const originalFileState = await readCoordinatorIdempotencyFile(originalFile);
|
|
8798
|
+
if (originalFileState.kind !== "record")
|
|
8799
|
+
return {
|
|
8800
|
+
ok: false,
|
|
8801
|
+
error: {
|
|
8802
|
+
code: "not_found",
|
|
8803
|
+
message: "The stranded coordinator start intent was not found.",
|
|
8804
|
+
},
|
|
8805
|
+
};
|
|
8806
|
+
const original = originalFileState.value;
|
|
8807
|
+
if (
|
|
8808
|
+
original.schema_version !== 1 ||
|
|
8809
|
+
original.tool !== "gjc_coordinator_start_session" ||
|
|
8810
|
+
original.key_digest !== createHash("sha256").update(creationKey).digest("hex") ||
|
|
8811
|
+
original.request_digest !== requestDigest
|
|
8812
|
+
)
|
|
8813
|
+
return {
|
|
8814
|
+
ok: false,
|
|
8815
|
+
error: {
|
|
8816
|
+
code: "idempotency_conflict",
|
|
8817
|
+
message: "The start intent identity does not match.",
|
|
8818
|
+
},
|
|
8819
|
+
};
|
|
8820
|
+
const originalAlreadyRetired =
|
|
8821
|
+
original.state === "completed" &&
|
|
8822
|
+
asRecord(asRecord(original.response)?.error)?.code === "retired";
|
|
8823
|
+
if (original.state !== "in_progress" && !originalAlreadyRetired)
|
|
8824
|
+
return {
|
|
8825
|
+
ok: false,
|
|
8826
|
+
error: {
|
|
8827
|
+
code: "retire_not_allowed",
|
|
8828
|
+
message: "The coordinator start intent is not stranded in progress.",
|
|
8829
|
+
},
|
|
8830
|
+
};
|
|
8831
|
+
const creation = await assertCreationRetirementIdentity(
|
|
8832
|
+
questionPaths,
|
|
8833
|
+
creationKeyDigest,
|
|
8834
|
+
proof,
|
|
8835
|
+
);
|
|
8836
|
+
if (originalAlreadyRetired && creation.retirement_intent?.phase !== "broker_retired")
|
|
8837
|
+
throw new SdkClientError(
|
|
8838
|
+
"state_corrupt",
|
|
8839
|
+
"A completed retired start intent lacks its durable broker retirement proof.",
|
|
8840
|
+
);
|
|
8841
|
+
if (
|
|
8842
|
+
originalAlreadyRetired &&
|
|
8843
|
+
creation.retirement_intent?.retirement_key_digest !== retirementKeyDigest
|
|
8844
|
+
)
|
|
8845
|
+
return {
|
|
8846
|
+
ok: false,
|
|
8847
|
+
error: {
|
|
8848
|
+
code: "retire_not_allowed",
|
|
8849
|
+
message: "The completed retired start intent is bound to a different retirement key.",
|
|
8850
|
+
},
|
|
8851
|
+
};
|
|
8852
|
+
if (
|
|
8853
|
+
creation.retirement_intent &&
|
|
8854
|
+
creation.retirement_intent.retirement_key_digest !== retirementKeyDigest
|
|
8855
|
+
)
|
|
8856
|
+
return {
|
|
8857
|
+
ok: false,
|
|
8858
|
+
error: {
|
|
8859
|
+
code: "retire_not_allowed",
|
|
8860
|
+
message:
|
|
8861
|
+
"The coordinator start intent was already retired by another retirement request.",
|
|
8862
|
+
},
|
|
8863
|
+
};
|
|
8864
|
+
let lifecycle: Record<string, unknown>;
|
|
8865
|
+
if (creation.retirement_intent?.phase === "broker_retired") {
|
|
8866
|
+
const staged = creation.retirement_intent.broker_proof;
|
|
8867
|
+
if (!staged)
|
|
8868
|
+
throw new SdkClientError(
|
|
8869
|
+
"terminal_uncertain",
|
|
8870
|
+
"Retirement intent is staged without a bounded broker proof.",
|
|
8871
|
+
);
|
|
8872
|
+
if (
|
|
8873
|
+
staged.session_id !== proof.session_id ||
|
|
8874
|
+
staged.state_root !== proof.state_root ||
|
|
8875
|
+
staged.endpoint_generation !== proof.endpoint_generation ||
|
|
8876
|
+
staged.endpoint_mtime_ms !== proof.endpoint_mtime_ms ||
|
|
8877
|
+
staged.process_incarnation !== proof.process_incarnation ||
|
|
8878
|
+
staged.host_incarnation !== proof.host_incarnation ||
|
|
8879
|
+
staged.lifecycle_request_id !== proof.lifecycle_request_id ||
|
|
8880
|
+
staged.remote_create_key !== proof.remote_create_key ||
|
|
8881
|
+
staged.retired !== true ||
|
|
8882
|
+
staged.ledger_state !== "terminal_error" ||
|
|
8883
|
+
staged.index_type !== "session_closed"
|
|
8884
|
+
)
|
|
8885
|
+
throw new SdkClientError(
|
|
8886
|
+
"state_corrupt",
|
|
8887
|
+
"Retirement intent proof does not match its creation identity.",
|
|
8888
|
+
);
|
|
8889
|
+
lifecycle = publicRetirementProof(staged);
|
|
8890
|
+
} else {
|
|
8891
|
+
await recordCreationRetirementIntent(
|
|
8892
|
+
questionPaths,
|
|
8893
|
+
creationKeyDigest,
|
|
8894
|
+
proof,
|
|
8895
|
+
retirementKeyDigest,
|
|
8896
|
+
);
|
|
8897
|
+
const acknowledgement = await brokerSession(
|
|
8898
|
+
cwd,
|
|
8899
|
+
"session.reconcile_uncertain",
|
|
8900
|
+
{
|
|
8901
|
+
sessionId,
|
|
8902
|
+
cwd,
|
|
8903
|
+
stateRoot,
|
|
8904
|
+
endpointGeneration,
|
|
8905
|
+
endpointMtimeMs,
|
|
8906
|
+
processIncarnation,
|
|
8907
|
+
hostIncarnation,
|
|
8908
|
+
lifecycleRequestId,
|
|
8909
|
+
remoteCreateKey,
|
|
8910
|
+
},
|
|
8911
|
+
brokerRequestKey,
|
|
8912
|
+
);
|
|
8913
|
+
const brokerResponse = asRecord(acknowledgement);
|
|
8914
|
+
if (brokerResponse?.ok === false) {
|
|
8915
|
+
const brokerError = asRecord(brokerResponse.error);
|
|
8916
|
+
if (brokerError?.code === "retirement_proof_stale" && brokerError.cleanup === undefined)
|
|
8917
|
+
await replaceCreationRetirementIntent(
|
|
8918
|
+
questionPaths,
|
|
8919
|
+
creationKeyDigest,
|
|
8920
|
+
proof,
|
|
8921
|
+
retirementKeyDigest,
|
|
8922
|
+
);
|
|
8923
|
+
}
|
|
8924
|
+
const parsed = strictBrokerRetirementProof(acknowledgement, proof);
|
|
8925
|
+
await recordCreationRetirementBrokerProof(
|
|
8926
|
+
questionPaths,
|
|
8927
|
+
creationKeyDigest,
|
|
8928
|
+
proof,
|
|
8929
|
+
parsed.broker,
|
|
8930
|
+
);
|
|
8931
|
+
lifecycle = parsed.public;
|
|
8932
|
+
}
|
|
8933
|
+
const retiredResponse = {
|
|
8934
|
+
ok: false,
|
|
8935
|
+
error: {
|
|
8936
|
+
code: "retired",
|
|
8937
|
+
message:
|
|
8938
|
+
"The stranded coordinator start intent was retired after exact session identity proof.",
|
|
8939
|
+
},
|
|
8940
|
+
};
|
|
8941
|
+
await advanceCreationReceipt(
|
|
8942
|
+
questionPaths,
|
|
8943
|
+
creationKeyDigest,
|
|
8944
|
+
"retired",
|
|
8945
|
+
retiredResponse,
|
|
8946
|
+
proof,
|
|
8947
|
+
);
|
|
8948
|
+
const latestOriginal = await readCoordinatorIdempotencyFile(originalFile);
|
|
8949
|
+
if (latestOriginal.kind !== "record")
|
|
8950
|
+
throw new SdkClientError("state_corrupt", "Start intent disappeared during retirement.");
|
|
8951
|
+
if (latestOriginal.value.state === "completed") {
|
|
8952
|
+
if (asRecord(asRecord(latestOriginal.value.response)?.error)?.code !== "retired")
|
|
8953
|
+
throw new SdkClientError(
|
|
8954
|
+
"idempotency_conflict",
|
|
8955
|
+
"Start intent changed during retirement.",
|
|
8956
|
+
);
|
|
8957
|
+
} else if (latestOriginal.value.state !== "in_progress") {
|
|
8958
|
+
throw new SdkClientError("idempotency_conflict", "Start intent changed during retirement.");
|
|
8959
|
+
} else {
|
|
8960
|
+
await writeCoordinatorIdempotencyFile(originalFile, {
|
|
8961
|
+
...(latestOriginal.value as unknown as CoordinatorToolIdempotencyRecord),
|
|
8962
|
+
state: "completed",
|
|
8963
|
+
response: retiredResponse,
|
|
8964
|
+
completed_at: new Date().toISOString(),
|
|
8965
|
+
});
|
|
8966
|
+
}
|
|
8967
|
+
return coordinatorRetiredResponse(sessionId, lifecycle);
|
|
8968
|
+
},
|
|
8969
|
+
true,
|
|
8970
|
+
isRetirementRetryable,
|
|
8971
|
+
true,
|
|
8972
|
+
),
|
|
8973
|
+
);
|
|
8974
|
+
}
|
|
8575
8975
|
if (name === "gjc_coordinator_activate_session") {
|
|
8576
8976
|
requireCoordinatorMutation(config, "sessions", args);
|
|
8577
8977
|
const idempotencyKey = requiredIdempotencyKey(args);
|
package/src/dap/client.ts
CHANGED
|
@@ -62,8 +62,13 @@ function parseMessage(
|
|
|
62
62
|
|
|
63
63
|
async function writeMessage(sink: DapWriteSink, message: DapRequestMessage | DapResponseMessage): Promise<void> {
|
|
64
64
|
const content = JSON.stringify(message);
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
// DapWriteSink.write() returns `number | Promise<number>`: under
|
|
66
|
+
// backpressure it returns a promise whose rejection (e.g. EPIPE when the
|
|
67
|
+
// adapter dies mid-write) would otherwise be discarded here and surface as
|
|
68
|
+
// an unhandled rejection. Await both writes so failures reach the callers
|
|
69
|
+
// that already handle flush() errors.
|
|
70
|
+
await sink.write(`Content-Length: ${Buffer.byteLength(content, "utf-8")}\r\n\r\n`);
|
|
71
|
+
await sink.write(content);
|
|
67
72
|
await sink.flush();
|
|
68
73
|
}
|
|
69
74
|
|
|
@@ -99,6 +104,7 @@ export class DapClient {
|
|
|
99
104
|
#isReading = false;
|
|
100
105
|
#disposed = false;
|
|
101
106
|
#lastActivity = Date.now();
|
|
107
|
+
#writeQueue = Promise.resolve();
|
|
102
108
|
#capabilities?: DapCapabilities;
|
|
103
109
|
#eventHandlers = new Map<string, Set<DapEventHandler>>();
|
|
104
110
|
#anyEventHandlers = new Set<DapEventHandler>();
|
|
@@ -328,6 +334,7 @@ export class DapClient {
|
|
|
328
334
|
throw signal.reason instanceof Error ? signal.reason : new ToolAbortError();
|
|
329
335
|
}
|
|
330
336
|
const { promise, resolve, reject } = Promise.withResolvers<TBody>();
|
|
337
|
+
void promise.catch(() => {});
|
|
331
338
|
let timeout: NodeJS.Timeout | undefined;
|
|
332
339
|
const cleanup = () => {
|
|
333
340
|
unsubscribe();
|
|
@@ -411,13 +418,11 @@ export class DapClient {
|
|
|
411
418
|
},
|
|
412
419
|
});
|
|
413
420
|
this.#lastActivity = Date.now();
|
|
414
|
-
|
|
415
|
-
await writeMessage(this.#writeSink, request);
|
|
416
|
-
} catch (error) {
|
|
421
|
+
void this.#queueWrite(request).catch(error => {
|
|
417
422
|
this.#pendingRequests.delete(requestSeq);
|
|
418
423
|
cleanup();
|
|
419
|
-
|
|
420
|
-
}
|
|
424
|
+
reject(error);
|
|
425
|
+
});
|
|
421
426
|
return promise;
|
|
422
427
|
}
|
|
423
428
|
|
|
@@ -431,7 +436,13 @@ export class DapClient {
|
|
|
431
436
|
...(message ? { message } : {}),
|
|
432
437
|
...(body !== undefined ? { body } : {}),
|
|
433
438
|
};
|
|
434
|
-
await
|
|
439
|
+
await this.#queueWrite(response);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
#queueWrite(message: DapRequestMessage | DapResponseMessage): Promise<void> {
|
|
443
|
+
const write = this.#writeQueue.catch(() => {}).then(() => writeMessage(this.#writeSink, message));
|
|
444
|
+
this.#writeQueue = write.catch(() => {});
|
|
445
|
+
return write;
|
|
435
446
|
}
|
|
436
447
|
|
|
437
448
|
async dispose(): Promise<void> {
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
assertDeepInterviewEnvelopeInputLimits,
|
|
9
9
|
assertDeepInterviewIntentManifest,
|
|
10
10
|
assertDeepInterviewStructuredResponseWithinLimit,
|
|
11
|
+
ENVELOPE_RESERVED_STATE_KEYS,
|
|
11
12
|
mergeDeepInterviewEnvelope,
|
|
12
13
|
normalizeDeepInterviewEnvelope,
|
|
13
14
|
} from "./deep-interview-state";
|
|
@@ -185,7 +186,19 @@ const RUNTIME_OWNED_ENVELOPE_KEYS = [
|
|
|
185
186
|
*/
|
|
186
187
|
const RECORDER_OWNED_STATE_KEYS = ["intent_contract", "intent_review"] as const;
|
|
187
188
|
|
|
188
|
-
/**
|
|
189
|
+
/**
|
|
190
|
+
* Remove sanitizer-owned keys from a staged payload and classify the
|
|
191
|
+
* normalization-owned ones; returns every ignored key name.
|
|
192
|
+
*
|
|
193
|
+
* Nested `state.*` occurrences of envelope-reserved keys are named but not
|
|
194
|
+
* removed here: `normalizeDeepInterviewEnvelope` is still the only remover, so
|
|
195
|
+
* the payload handed to validation stays byte-identical to what the caller sent.
|
|
196
|
+
* Naming them routes an already-certain discard through the existing
|
|
197
|
+
* `ignored_runtime_owned_keys` channel, which previously reported nothing for
|
|
198
|
+
* the nested form -- `{"state":{"current_phase":"cancelled"}}` returned an
|
|
199
|
+
* unqualified `ok:true` for an instruction that never took effect, while the
|
|
200
|
+
* same key at the top level was reported.
|
|
201
|
+
*/
|
|
189
202
|
function sanitizeStagedPayload(payload: Record<string, unknown>): {
|
|
190
203
|
payload: Record<string, unknown>;
|
|
191
204
|
ignoredKeys: string[];
|
|
@@ -206,6 +219,12 @@ function sanitizeStagedPayload(payload: Record<string, unknown>): {
|
|
|
206
219
|
ignoredKeys.push(`state.${key}`);
|
|
207
220
|
}
|
|
208
221
|
}
|
|
222
|
+
// Report only -- do not delete. Removing these here would strip content
|
|
223
|
+
// before `assertDeepInterviewStructuredResponseWithinLimit`, so an oversized
|
|
224
|
+
// discarded value would stop failing `DI_STAGE_INPUT_INVALID`.
|
|
225
|
+
for (const key of ENVELOPE_RESERVED_STATE_KEYS) {
|
|
226
|
+
if (key in state) ignoredKeys.push(`state.${key}`);
|
|
227
|
+
}
|
|
209
228
|
next.state = state;
|
|
210
229
|
}
|
|
211
230
|
return { payload: next, ignoredKeys };
|
|
@@ -197,7 +197,7 @@ const HOISTED_STATE_FIELDS = [
|
|
|
197
197
|
* removes, permanently corrupting the shape. They are stripped from `state` on
|
|
198
198
|
* normalize so the canonical envelope self-heals on the next write.
|
|
199
199
|
*/
|
|
200
|
-
const ENVELOPE_RESERVED_STATE_KEYS = [
|
|
200
|
+
export const ENVELOPE_RESERVED_STATE_KEYS = [
|
|
201
201
|
"state",
|
|
202
202
|
"receipt",
|
|
203
203
|
"skill",
|
|
@@ -206,6 +206,8 @@ const ENVELOPE_RESERVED_STATE_KEYS = [
|
|
|
206
206
|
"active",
|
|
207
207
|
"current_phase",
|
|
208
208
|
"state_revision",
|
|
209
|
+
"source_state_revision",
|
|
210
|
+
"last_applied_draft_id",
|
|
209
211
|
"session_id",
|
|
210
212
|
] as const;
|
|
211
213
|
|
|
@@ -662,12 +662,20 @@ async function openOwnerIdentity(handle: fs.FileHandle): Promise<OpenOwnerIdenti
|
|
|
662
662
|
* the compare-and-delete cannot object — the capture and the authorization would be the
|
|
663
663
|
* same foreign object.
|
|
664
664
|
*
|
|
665
|
-
* So the created descriptor is still open here, and it stays open: it is what makes
|
|
666
|
-
* comparison sound instead of merely likely, because the created inode cannot be recycled
|
|
665
|
+
* So on POSIX the created descriptor is still open here, and it stays open: it is what makes
|
|
666
|
+
* the comparison sound instead of merely likely, because the created inode cannot be recycled
|
|
667
667
|
* underneath a successor while this process holds it. Only when a fresh no-follow capture
|
|
668
668
|
* of the pathname proves to be that same still-open object is the existing exact unlink
|
|
669
669
|
* allowed to run — and it then closes the remaining capture-to-unlink race on the bytes.
|
|
670
670
|
*
|
|
671
|
+
* Windows inverts that order. Opening the object for DELETE while this descriptor pins it
|
|
672
|
+
* fails (the create descriptor grants no share-delete), so the descriptor cannot outlive the
|
|
673
|
+
* proof there. It has already vouched for the object identity before it closes; afterwards
|
|
674
|
+
* the compare-and-delete itself re-proves the FULL recorded identity — dev/ino, link count,
|
|
675
|
+
* size, mtime, and payload hash — against whatever the pathname names at delete time. A
|
|
676
|
+
* successor that takes the pathname in the close-to-delete window fails that compare and is
|
|
677
|
+
* left alone, which keeps the retraction fail-closed without the descriptor.
|
|
678
|
+
*
|
|
671
679
|
* Anything short of that proof — a descriptor that no longer answers, a capture that
|
|
672
680
|
* refuses or reports a type swap, a different object at the path — leaves the pathname
|
|
673
681
|
* exactly as it is for the normal stale protocol.
|
|
@@ -680,6 +688,7 @@ async function retractFailedOwnerRecord(
|
|
|
680
688
|
if (!created) return;
|
|
681
689
|
const stillOpen = await openOwnerIdentity(handle);
|
|
682
690
|
if (!stillOpen || stillOpen.dev !== created.dev || stillOpen.ino !== created.ino) return;
|
|
691
|
+
if (ownerAccessStrategy() === "windows-validated") await handle.close().catch(() => undefined);
|
|
683
692
|
const current = await captureRegularLockOwner(file).catch(() => null);
|
|
684
693
|
if (!current || current.dev !== created.dev || current.ino !== created.ino) return;
|
|
685
694
|
exactUnlinkOwnerRecord(file, current);
|