@gajae-code/coding-agent 0.15.2 → 0.15.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/CHANGELOG.md +43 -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 +65 -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/extensibility/extensions/types.d.ts +2 -0
  11. package/dist/types/gjc-runtime/deep-interview-state.d.ts +12 -0
  12. package/dist/types/gjc-runtime/workflow-placeholder.d.ts +6 -0
  13. package/dist/types/main.d.ts +1 -1
  14. package/dist/types/modes/action-registry.d.ts +1 -0
  15. package/dist/types/modes/components/hook-selector.d.ts +1 -0
  16. package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +1 -1
  17. package/dist/types/modes/components/welcome.d.ts +2 -0
  18. package/dist/types/modes/controllers/input-controller.d.ts +16 -1
  19. package/dist/types/modes/controllers/runtime-mcp-command-controller.d.ts +42 -0
  20. package/dist/types/modes/interactive-mode.d.ts +7 -0
  21. package/dist/types/modes/shared/oauth-url-copy.d.ts +9 -0
  22. package/dist/types/modes/types.d.ts +3 -0
  23. package/dist/types/modes/ui-language.d.ts +2 -1
  24. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +1 -0
  25. package/dist/types/sdk/broker/session-index.d.ts +18 -0
  26. package/dist/types/sdk/bus/index.d.ts +4 -1
  27. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -1
  28. package/dist/types/sdk/bus/telegram-daemon.d.ts +2 -0
  29. package/dist/types/sdk/cli/session-cli.d.ts +1 -1
  30. package/dist/types/sdk/lifecycle/service.d.ts +32 -5
  31. package/dist/types/sdk/session.d.ts +5 -0
  32. package/dist/types/session/agent-session.d.ts +4 -0
  33. package/dist/types/session/fallback-chain-controller.d.ts +8 -0
  34. package/dist/types/slash-commands/helpers/aside.d.ts +54 -0
  35. package/dist/types/tools/ask.d.ts +9 -0
  36. package/dist/types/utils/clipboard.d.ts +9 -1
  37. package/package.json +7 -7
  38. package/scripts/generate-sdk-adapter-parity-manifest.ts +5 -2
  39. package/scripts/generate-sdk-operation-inventory.ts +3 -0
  40. package/src/commands/sdk.ts +2 -2
  41. package/src/config/autorouting-tier-map.ts +0 -9
  42. package/src/config/file-lock.ts +15 -1
  43. package/src/config/keybindings.ts +13 -3
  44. package/src/config/model-registry.ts +132 -21
  45. package/src/config/model-resolver.ts +2 -1
  46. package/src/config/models-config-schema.ts +2 -2
  47. package/src/config/settings-schema.ts +66 -1
  48. package/src/config/settings.ts +1 -1
  49. package/src/coordinator/contract.ts +1 -0
  50. package/src/coordinator-mcp/question-state.ts +235 -4
  51. package/src/coordinator-mcp/server.ts +402 -2
  52. package/src/dap/client.ts +19 -8
  53. package/src/deep-interview/render-middleware.ts +6 -2
  54. package/src/extensibility/extensions/types.ts +2 -0
  55. package/src/gjc-runtime/deep-interview-stage.ts +20 -1
  56. package/src/gjc-runtime/deep-interview-state.ts +3 -1
  57. package/src/gjc-runtime/ralplan-runtime.ts +6 -1
  58. package/src/gjc-runtime/session-state-lock.ts +11 -2
  59. package/src/gjc-runtime/ultragoal-runtime.ts +5 -0
  60. package/src/gjc-runtime/workflow-placeholder.ts +33 -0
  61. package/src/internal-urls/docs-index.generated.ts +11 -9
  62. package/src/lsp/client.ts +39 -4
  63. package/src/main.ts +42 -11
  64. package/src/modes/action-registry.ts +7 -0
  65. package/src/modes/components/hook-selector.ts +2 -1
  66. package/src/modes/components/iterm-pet-transport.ts +4 -2
  67. package/src/modes/components/runtime-mcp-add-wizard.ts +38 -1
  68. package/src/modes/components/settings-selector.ts +1 -0
  69. package/src/modes/components/tool-status-header.ts +149 -41
  70. package/src/modes/components/welcome.ts +8 -0
  71. package/src/modes/controllers/extension-ui-controller.ts +1 -0
  72. package/src/modes/controllers/goal-mode-controller.ts +7 -2
  73. package/src/modes/controllers/input-controller.ts +141 -7
  74. package/src/modes/controllers/plan-mode-controller.ts +12 -4
  75. package/src/modes/controllers/runtime-mcp-command-controller.ts +164 -20
  76. package/src/modes/controllers/selector-controller.ts +11 -2
  77. package/src/modes/interactive-mode.ts +88 -1
  78. package/src/modes/shared/oauth-url-copy.ts +33 -0
  79. package/src/modes/types.ts +3 -0
  80. package/src/modes/ui-language.ts +44 -3
  81. package/src/runtime-mcp/oauth-flow.ts +19 -8
  82. package/src/runtime-mcp/transports/stdio.ts +27 -13
  83. package/src/sdk/acp/adapter.ts +29 -2
  84. package/src/sdk/broker/broker.ts +26 -6
  85. package/src/sdk/broker/lifecycle-ledger.ts +28 -1
  86. package/src/sdk/broker/lifecycle.ts +664 -5
  87. package/src/sdk/broker/session-index.ts +246 -0
  88. package/src/sdk/broker/transport.ts +1 -0
  89. package/src/sdk/bus/index.ts +135 -38
  90. package/src/sdk/bus/natives-augment.d.ts +20 -1
  91. package/src/sdk/bus/telegram-daemon-contract.ts +4 -1
  92. package/src/sdk/bus/telegram-daemon.ts +3 -0
  93. package/src/sdk/cli/session-cli.ts +34 -7
  94. package/src/sdk/lifecycle/service.ts +166 -11
  95. package/src/sdk/mcp/server.ts +2 -1
  96. package/src/sdk/protocol/operation-inventory.generated.json +40 -0
  97. package/src/sdk/protocol/operation-registry.ts +2 -1
  98. package/src/sdk/session.ts +91 -7
  99. package/src/session/agent-session.ts +473 -66
  100. package/src/session/fallback-chain-controller.ts +17 -0
  101. package/src/setup/frictionless-onboarding.ts +4 -4
  102. package/src/slash-commands/aside-command.test.ts +366 -0
  103. package/src/slash-commands/builtin-registry.ts +19 -0
  104. package/src/slash-commands/helpers/aside.ts +299 -0
  105. package/src/tools/ask-contract.ts +54 -3
  106. package/src/tools/ask.ts +54 -6
  107. 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> {
@@ -10,6 +10,7 @@ import {
10
10
  type ViewportAnchorSource,
11
11
  type ViewportAnchorSourceRenderer,
12
12
  } from "@gajae-code/tui";
13
+ import { isLegacyDeepInterviewPlaceholder, isWorkflowPlaceholderText } from "../gjc-runtime/workflow-placeholder";
13
14
  import { getMarkdownTheme, type Theme } from "../modes/theme/theme";
14
15
 
15
16
  interface RoundQuestionModel {
@@ -132,7 +133,7 @@ function parseRoundQuestion(text: string): RoundQuestionModel | null {
132
133
  .slice(headerIndex + 1)
133
134
  .join("\n")
134
135
  .trim();
135
- if (!body) return null;
136
+ if (isWorkflowPlaceholderText(body)) return null;
136
137
 
137
138
  const componentMatch =
138
139
  /^Round\s+(\d+)\s+\|\s+Component:\s*(.*?)\s+\|\s+Targeting:\s*(.*?)\s+\|\s+Why now:\s*(.*?)\s+\|\s+Ambiguity:\s*(.+?)%?\s*$/i.exec(
@@ -204,11 +205,13 @@ function parseTopologyQuestion(text: string): TopologyQuestionModel | null {
204
205
  if (inQuestion) questionLines.push(trimmed);
205
206
  else contextLines.push(trimmed);
206
207
  }
208
+ const question = questionLines.join("\n");
209
+ if (isWorkflowPlaceholderText(question)) return null;
207
210
  return {
208
211
  kind: "topology-question",
209
212
  context: contextLines.join("\n") || undefined,
210
213
  components,
211
- question: questionLines.join("\n"),
214
+ question,
212
215
  };
213
216
  }
214
217
 
@@ -399,6 +402,7 @@ export function renderDeepInterviewAskQuestion(question: string, uiTheme: Theme)
399
402
  export function isDeepInterviewAskQuestion(question: string): boolean {
400
403
  if (parseTopologyQuestion(question) ?? parseRoundQuestion(question)) return true;
401
404
  const normalized = normalizeText(question);
405
+ if (isLegacyDeepInterviewPlaceholder(normalized)) return false;
402
406
  return /(?:^|\n)\s*Round\s+\d+\s*\|.*?\bAmbiguity\b/i.test(normalized);
403
407
  }
404
408
 
@@ -155,6 +155,8 @@ export interface ExtensionUIDialogOptions {
155
155
  customInput?: {
156
156
  optionLabel: string;
157
157
  onSubmit: (text: string) => void;
158
+ /** Empty/whitespace-only submissions may be rejected by the selector. */
159
+ allowEmpty?: boolean;
158
160
  };
159
161
  /**
160
162
  * Inline free-text input for a non-answer clarification action. It is
@@ -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
 
@@ -747,7 +747,12 @@ export async function resolveRalplanTargetRoot(
747
747
  if (!repository) {
748
748
  throw new RalplanCommandError(2, `ralplan --worktree-root is not inside a git repository: ${canonical}`);
749
749
  }
750
- const verified = Bun.spawn(["git", "-C", canonical, "rev-parse", "--verify", "HEAD^{commit}"], {
750
+ // Do not pass the caret-containing `HEAD^{commit}` revision here. On Windows,
751
+ // Bun may dispatch git through a `.cmd` shim, where `^` is interpreted by the
752
+ // command shell before git receives the argument. HEAD is sufficient after
753
+ // resolving a repository root: an unborn or non-commit HEAD still fails the
754
+ // same validation, while the argv remains shell-safe on every platform.
755
+ const verified = Bun.spawn(["git", "-C", canonical, "rev-parse", "--verify", "HEAD"], {
751
756
  stdout: "pipe",
752
757
  stderr: "pipe",
753
758
  });