@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.
Files changed (95) hide show
  1. package/CHANGELOG.md +37 -1
  2. package/dist/types/config/autorouting-tier-map.d.ts +0 -9
  3. package/dist/types/config/file-lock.d.ts +7 -1
  4. package/dist/types/config/keybindings.d.ts +13 -3
  5. package/dist/types/config/model-registry.d.ts +5 -0
  6. package/dist/types/config/model-resolver.d.ts +1 -0
  7. package/dist/types/config/settings-schema.d.ts +13 -1
  8. package/dist/types/coordinator/contract.d.ts +1 -1
  9. package/dist/types/coordinator-mcp/question-state.d.ts +42 -2
  10. package/dist/types/gjc-runtime/deep-interview-state.d.ts +12 -0
  11. package/dist/types/main.d.ts +1 -1
  12. package/dist/types/modes/action-registry.d.ts +1 -0
  13. package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +1 -1
  14. package/dist/types/modes/components/welcome.d.ts +2 -0
  15. package/dist/types/modes/controllers/input-controller.d.ts +16 -1
  16. package/dist/types/modes/controllers/runtime-mcp-command-controller.d.ts +42 -0
  17. package/dist/types/modes/interactive-mode.d.ts +7 -0
  18. package/dist/types/modes/shared/oauth-url-copy.d.ts +9 -0
  19. package/dist/types/modes/types.d.ts +3 -0
  20. package/dist/types/modes/ui-language.d.ts +2 -1
  21. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +1 -0
  22. package/dist/types/sdk/broker/session-index.d.ts +18 -0
  23. package/dist/types/sdk/bus/index.d.ts +4 -1
  24. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -1
  25. package/dist/types/sdk/bus/telegram-daemon.d.ts +2 -0
  26. package/dist/types/sdk/cli/session-cli.d.ts +1 -1
  27. package/dist/types/sdk/lifecycle/service.d.ts +32 -5
  28. package/dist/types/sdk/session.d.ts +5 -0
  29. package/dist/types/session/agent-session.d.ts +4 -0
  30. package/dist/types/session/fallback-chain-controller.d.ts +8 -0
  31. package/dist/types/slash-commands/helpers/aside.d.ts +54 -0
  32. package/dist/types/tools/ask.d.ts +8 -0
  33. package/dist/types/utils/clipboard.d.ts +9 -1
  34. package/package.json +7 -7
  35. package/scripts/generate-sdk-adapter-parity-manifest.ts +5 -2
  36. package/scripts/generate-sdk-operation-inventory.ts +3 -0
  37. package/src/commands/sdk.ts +2 -2
  38. package/src/config/autorouting-tier-map.ts +0 -9
  39. package/src/config/file-lock.ts +15 -1
  40. package/src/config/keybindings.ts +13 -3
  41. package/src/config/model-registry.ts +132 -21
  42. package/src/config/model-resolver.ts +2 -1
  43. package/src/config/models-config-schema.ts +2 -2
  44. package/src/config/settings-schema.ts +13 -1
  45. package/src/coordinator/contract.ts +1 -0
  46. package/src/coordinator-mcp/question-state.ts +235 -4
  47. package/src/coordinator-mcp/server.ts +402 -2
  48. package/src/dap/client.ts +19 -8
  49. package/src/gjc-runtime/deep-interview-stage.ts +20 -1
  50. package/src/gjc-runtime/deep-interview-state.ts +3 -1
  51. package/src/gjc-runtime/session-state-lock.ts +11 -2
  52. package/src/internal-urls/docs-index.generated.ts +8 -7
  53. package/src/lsp/client.ts +39 -4
  54. package/src/main.ts +42 -11
  55. package/src/modes/action-registry.ts +7 -0
  56. package/src/modes/components/iterm-pet-transport.ts +4 -2
  57. package/src/modes/components/runtime-mcp-add-wizard.ts +38 -1
  58. package/src/modes/components/settings-selector.ts +1 -0
  59. package/src/modes/components/tool-status-header.ts +149 -41
  60. package/src/modes/components/welcome.ts +8 -0
  61. package/src/modes/controllers/goal-mode-controller.ts +7 -2
  62. package/src/modes/controllers/input-controller.ts +141 -7
  63. package/src/modes/controllers/plan-mode-controller.ts +12 -4
  64. package/src/modes/controllers/runtime-mcp-command-controller.ts +164 -20
  65. package/src/modes/controllers/selector-controller.ts +11 -2
  66. package/src/modes/interactive-mode.ts +88 -1
  67. package/src/modes/shared/oauth-url-copy.ts +33 -0
  68. package/src/modes/types.ts +3 -0
  69. package/src/modes/ui-language.ts +44 -3
  70. package/src/runtime-mcp/oauth-flow.ts +19 -8
  71. package/src/runtime-mcp/transports/stdio.ts +27 -13
  72. package/src/sdk/acp/adapter.ts +29 -2
  73. package/src/sdk/broker/broker.ts +26 -6
  74. package/src/sdk/broker/lifecycle-ledger.ts +28 -1
  75. package/src/sdk/broker/lifecycle.ts +664 -5
  76. package/src/sdk/broker/session-index.ts +246 -0
  77. package/src/sdk/broker/transport.ts +1 -0
  78. package/src/sdk/bus/index.ts +135 -38
  79. package/src/sdk/bus/natives-augment.d.ts +20 -1
  80. package/src/sdk/bus/telegram-daemon-contract.ts +4 -1
  81. package/src/sdk/bus/telegram-daemon.ts +3 -0
  82. package/src/sdk/cli/session-cli.ts +34 -7
  83. package/src/sdk/lifecycle/service.ts +166 -11
  84. package/src/sdk/mcp/server.ts +2 -1
  85. package/src/sdk/protocol/operation-inventory.generated.json +40 -0
  86. package/src/sdk/protocol/operation-registry.ts +2 -1
  87. package/src/sdk/session.ts +91 -7
  88. package/src/session/agent-session.ts +403 -56
  89. package/src/session/fallback-chain-controller.ts +17 -0
  90. package/src/setup/frictionless-onboarding.ts +4 -4
  91. package/src/slash-commands/aside-command.test.ts +366 -0
  92. package/src/slash-commands/builtin-registry.ts +19 -0
  93. package/src/slash-commands/helpers/aside.ts +299 -0
  94. package/src/tools/ask.ts +8 -0
  95. 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
- return await withSessionStateLock(lockFile, async () => {
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
- sink.write(`Content-Length: ${Buffer.byteLength(content, "utf-8")}\r\n\r\n`);
66
- sink.write(content);
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
- try {
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
- throw error;
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 writeMessage(this.#writeSink, response);
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
- /** Strip runtime-owned keys from a staged payload; returns the ignored key names. */
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 the
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);