@gajae-code/coding-agent 0.12.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/CHANGELOG.md +82 -1
  2. package/dist/types/async/job-manager.d.ts +17 -3
  3. package/dist/types/config/settings-schema.d.ts +54 -0
  4. package/dist/types/exa/mcp-client.d.ts +10 -1
  5. package/dist/types/exec/bash-executor.d.ts +45 -12
  6. package/dist/types/extensibility/extensions/types.d.ts +16 -2
  7. package/dist/types/extensibility/gjc-plugins/index.d.ts +12 -2
  8. package/dist/types/extensibility/gjc-plugins/installer.d.ts +63 -7
  9. package/dist/types/extensibility/gjc-plugins/lifecycle-reconciliation.d.ts +55 -0
  10. package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +64 -0
  11. package/dist/types/extensibility/gjc-plugins/runtime-quarantine.d.ts +67 -0
  12. package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +3 -1
  13. package/dist/types/extensibility/gjc-plugins/types.d.ts +124 -0
  14. package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
  15. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +18 -0
  16. package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
  17. package/dist/types/gjc-runtime/team-runtime.d.ts +25 -0
  18. package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +43 -0
  19. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +10 -0
  20. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +31 -80
  21. package/dist/types/modes/acp/acp-agent.d.ts +2 -19
  22. package/dist/types/modes/components/gjc-bundle-settings.d.ts +34 -0
  23. package/dist/types/modes/components/notifications-settings-editor.d.ts +1 -0
  24. package/dist/types/modes/components/session-observer-overlay.d.ts +18 -0
  25. package/dist/types/modes/components/settings-selector.d.ts +9 -0
  26. package/dist/types/modes/components/status-line/gh.d.ts +5 -0
  27. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +18 -1
  28. package/dist/types/reminders/star-reminder.d.ts +3 -13
  29. package/dist/types/runtime-mcp/manager.d.ts +1 -0
  30. package/dist/types/sdk/acp/adapter.d.ts +3 -1
  31. package/dist/types/sdk/broker/transport.d.ts +2 -0
  32. package/dist/types/sdk/bus/attachment-registry.d.ts +1 -0
  33. package/dist/types/sdk/bus/chat-daemon-control.d.ts +4 -1
  34. package/dist/types/sdk/bus/config.d.ts +6 -2
  35. package/dist/types/sdk/bus/index.d.ts +29 -0
  36. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +8 -1
  37. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
  38. package/dist/types/sdk/bus/reconciliation-store.d.ts +7 -3
  39. package/dist/types/sdk/bus/rich-draft.d.ts +1 -0
  40. package/dist/types/sdk/bus/rich-render.d.ts +2 -0
  41. package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +165 -0
  42. package/dist/types/sdk/bus/telegram-cli.d.ts +12 -0
  43. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +11 -4
  44. package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -1
  45. package/dist/types/sdk/bus/telegram-reference.d.ts +11 -1
  46. package/dist/types/sdk/bus/threaded-render.d.ts +3 -2
  47. package/dist/types/sdk/bus/topic-registry.d.ts +11 -1
  48. package/dist/types/sdk/host/query/revision-store.d.ts +2 -0
  49. package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
  50. package/dist/types/sdk/prompt-status.d.ts +23 -3
  51. package/dist/types/sdk/session.d.ts +6 -0
  52. package/dist/types/session/agent-session.d.ts +38 -2
  53. package/dist/types/session/artifacts.d.ts +8 -0
  54. package/dist/types/session/blob-store.d.ts +57 -4
  55. package/dist/types/session/internal/managed-session-storage.d.ts +8 -1
  56. package/dist/types/session/session-manager.d.ts +23 -6
  57. package/dist/types/session/session-storage.d.ts +4 -0
  58. package/dist/types/session/streaming-output.d.ts +36 -7
  59. package/dist/types/setup/credential-import.d.ts +12 -0
  60. package/dist/types/skill-state/active-state.d.ts +12 -0
  61. package/dist/types/skill-state/workflow-hud.d.ts +6 -0
  62. package/dist/types/task/executor.d.ts +3 -2
  63. package/dist/types/task/index.d.ts +3 -1
  64. package/dist/types/task/provider-retry-status.d.ts +8 -0
  65. package/dist/types/task/receipt.d.ts +7 -1
  66. package/dist/types/task/types.d.ts +23 -0
  67. package/dist/types/tools/ask-answer-registry.d.ts +2 -2
  68. package/dist/types/tools/bash-interactive.d.ts +4 -1
  69. package/dist/types/tools/bash.d.ts +3 -1
  70. package/dist/types/tools/image-gen.d.ts +4 -0
  71. package/dist/types/tools/index.d.ts +4 -0
  72. package/dist/types/tools/output-meta.d.ts +23 -2
  73. package/dist/types/tools/path-utils.d.ts +1 -0
  74. package/dist/types/tools/read.d.ts +0 -6
  75. package/dist/types/tools/review.d.ts +5 -7
  76. package/dist/types/tools/subagent.d.ts +2 -0
  77. package/dist/types/utils/gh.d.ts +13 -0
  78. package/dist/types/utils/image-resize.d.ts +1 -0
  79. package/dist/types/web/search/providers/kimi.d.ts +2 -0
  80. package/dist/types/web/search/providers/searxng.d.ts +7 -0
  81. package/dist/types/web/search/providers/xai.d.ts +2 -0
  82. package/package.json +10 -8
  83. package/scripts/acp-conformance-agent.ts +218 -0
  84. package/scripts/capture-gjc-bundle-settings.ts +306 -0
  85. package/scripts/generate-sdk-operation-inventory.ts +3 -0
  86. package/scripts/resident-memory-bench.ts +1341 -0
  87. package/scripts/run-acp-conformance.ts +285 -0
  88. package/src/async/job-manager.ts +78 -20
  89. package/src/cli/fast-help.ts +1 -1
  90. package/src/cli/plugin-cli.ts +176 -18
  91. package/src/commands/team.ts +19 -1
  92. package/src/config/file-lock.ts +11 -2
  93. package/src/config/settings-schema.ts +51 -0
  94. package/src/coordinator-mcp/question-gate-codec.ts +1 -1
  95. package/src/coordinator-mcp/server.ts +5 -12
  96. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -0
  97. package/src/defaults/gjc/skills/ralplan/SKILL.md +19 -16
  98. package/src/defaults/gjc/skills/team/SKILL.md +2 -0
  99. package/src/defaults/gjc/skills/ultragoal/SKILL.md +37 -24
  100. package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +12 -0
  101. package/src/defaults/gjc-defaults.ts +4 -4
  102. package/src/exa/mcp-client.ts +12 -3
  103. package/src/exec/bash-executor.ts +144 -27
  104. package/src/extensibility/extensions/runner.ts +15 -0
  105. package/src/extensibility/extensions/types.ts +18 -1
  106. package/src/extensibility/gjc-plugins/constrained-hooks.ts +8 -2
  107. package/src/extensibility/gjc-plugins/index.ts +19 -2
  108. package/src/extensibility/gjc-plugins/installer.ts +189 -50
  109. package/src/extensibility/gjc-plugins/lifecycle-reconciliation.ts +199 -0
  110. package/src/extensibility/gjc-plugins/lifecycle.ts +626 -0
  111. package/src/extensibility/gjc-plugins/observability.ts +3 -3
  112. package/src/extensibility/gjc-plugins/prompt-appendix.ts +5 -1
  113. package/src/extensibility/gjc-plugins/runtime-adapters.ts +12 -2
  114. package/src/extensibility/gjc-plugins/runtime-quarantine.ts +121 -0
  115. package/src/extensibility/gjc-plugins/schema.ts +84 -12
  116. package/src/extensibility/gjc-plugins/session-validation.ts +9 -3
  117. package/src/extensibility/gjc-plugins/types.ts +141 -0
  118. package/src/extensibility/gjc-plugins/validation.ts +9 -0
  119. package/src/gjc-runtime/launch-tmux.ts +25 -6
  120. package/src/gjc-runtime/managed-owner-admission.ts +1 -1
  121. package/src/gjc-runtime/ralplan-runtime.ts +411 -17
  122. package/src/gjc-runtime/team-launch.ts +8 -0
  123. package/src/gjc-runtime/team-runtime.ts +140 -32
  124. package/src/gjc-runtime/team-worker-heartbeat.ts +149 -0
  125. package/src/gjc-runtime/tmux-owner-isolation.ts +10 -0
  126. package/src/gjc-runtime/tmux-sessions.ts +58 -13
  127. package/src/gjc-runtime/ultragoal-runtime.ts +401 -611
  128. package/src/gjc-runtime/workflow-manifest.generated.json +8 -99
  129. package/src/gjc-runtime/workflow-manifest.ts +4 -68
  130. package/src/internal-urls/docs-index.generated.ts +15 -15
  131. package/src/modes/DESIGN.md +51 -0
  132. package/src/modes/acp/acp-agent.ts +298 -186
  133. package/src/modes/components/gjc-bundle-settings.ts +508 -0
  134. package/src/modes/components/notifications-settings-editor.ts +12 -1
  135. package/src/modes/components/oauth-selector.ts +68 -30
  136. package/src/modes/components/session-observer-overlay.ts +305 -86
  137. package/src/modes/components/settings-selector.ts +56 -5
  138. package/src/modes/components/status-line/gh.ts +16 -0
  139. package/src/modes/components/tool-status-header.ts +3 -14
  140. package/src/modes/components/transcript-viewer-overlay.ts +99 -24
  141. package/src/modes/controllers/command-controller.ts +4 -0
  142. package/src/modes/controllers/extension-ui-controller.ts +7 -0
  143. package/src/modes/controllers/selector-controller.ts +45 -0
  144. package/src/modes/runtime-init.ts +3 -0
  145. package/src/prompts/agent-fragments/ralplan-persistence.md +4 -3
  146. package/src/prompts/system/eager-todo.md +8 -4
  147. package/src/prompts/system/system-prompt.md +11 -4
  148. package/src/prompts/tools/bash.md +5 -5
  149. package/src/prompts/tools/search-tool-bm25.md +5 -0
  150. package/src/prompts/tools/task.md +1 -1
  151. package/src/reminders/star-reminder.ts +4 -42
  152. package/src/runtime-mcp/manager.ts +19 -4
  153. package/src/sdk/acp/adapter.ts +22 -6
  154. package/src/sdk/broker/transport.ts +35 -3
  155. package/src/sdk/bus/attachment-registry.ts +5 -1
  156. package/src/sdk/bus/chat-daemon-cli.ts +4 -39
  157. package/src/sdk/bus/chat-daemon-control.ts +6 -3
  158. package/src/sdk/bus/config.ts +18 -2
  159. package/src/sdk/bus/index.ts +661 -126
  160. package/src/sdk/bus/kind-aware-reconciliation.ts +148 -73
  161. package/src/sdk/bus/lifecycle-commands.ts +4 -1
  162. package/src/sdk/bus/lifecycle-control-runtime.ts +403 -88
  163. package/src/sdk/bus/reconciliation-store.ts +95 -16
  164. package/src/sdk/bus/rich-draft.ts +1 -1
  165. package/src/sdk/bus/rich-render.ts +2 -2
  166. package/src/sdk/bus/telegram-adoption-intent.ts +574 -0
  167. package/src/sdk/bus/telegram-cli.ts +33 -8
  168. package/src/sdk/bus/telegram-daemon-cli.ts +1 -0
  169. package/src/sdk/bus/telegram-daemon-contract.ts +11 -4
  170. package/src/sdk/bus/telegram-daemon.ts +916 -23
  171. package/src/sdk/bus/telegram-reference.ts +40 -3
  172. package/src/sdk/bus/threaded-render.ts +48 -3
  173. package/src/sdk/bus/topic-registry.ts +20 -0
  174. package/src/sdk/host/query/revision-store.ts +36 -2
  175. package/src/sdk/host/reverse-leases.ts +44 -7
  176. package/src/sdk/prompt-status.ts +16 -3
  177. package/src/sdk/protocol/operation-inventory.generated.json +22 -0
  178. package/src/sdk/session.ts +88 -9
  179. package/src/session/agent-session.ts +822 -172
  180. package/src/session/artifacts.ts +22 -0
  181. package/src/session/auth-broker-config.ts +10 -3
  182. package/src/session/blob-store.ts +915 -24
  183. package/src/session/internal/managed-session-scope.ts +44 -6
  184. package/src/session/internal/managed-session-storage.ts +48 -6
  185. package/src/session/session-manager.ts +1614 -435
  186. package/src/session/session-storage.ts +9 -1
  187. package/src/session/streaming-output.ts +238 -68
  188. package/src/setup/credential-import.ts +75 -13
  189. package/src/skill-state/active-state.ts +32 -0
  190. package/src/skill-state/workflow-hud.ts +17 -0
  191. package/src/skill-state/workflow-mutation-guard.ts +496 -2
  192. package/src/task/executor.ts +122 -39
  193. package/src/task/index.ts +23 -14
  194. package/src/task/provider-retry-status.ts +16 -0
  195. package/src/task/receipt.ts +23 -7
  196. package/src/task/render.ts +6 -2
  197. package/src/task/types.ts +108 -0
  198. package/src/tools/ask-answer-registry.ts +12 -6
  199. package/src/tools/ask.ts +86 -55
  200. package/src/tools/ast-edit.ts +1 -0
  201. package/src/tools/ast-grep.ts +1 -0
  202. package/src/tools/bash-interactive.ts +8 -3
  203. package/src/tools/bash.ts +503 -60
  204. package/src/tools/find.ts +1 -0
  205. package/src/tools/image-gen.ts +29 -5
  206. package/src/tools/index.ts +4 -0
  207. package/src/tools/output-meta.ts +108 -14
  208. package/src/tools/path-utils.ts +3 -2
  209. package/src/tools/read.ts +18 -0
  210. package/src/tools/review.ts +5 -7
  211. package/src/tools/search.ts +1 -0
  212. package/src/tools/subagent-render.ts +6 -2
  213. package/src/tools/subagent.ts +6 -0
  214. package/src/tools/telegram-send.ts +6 -3
  215. package/src/utils/gh.ts +39 -0
  216. package/src/utils/image-resize.ts +3 -0
  217. package/src/web/search/providers/anthropic.ts +4 -4
  218. package/src/web/search/providers/kimi.ts +15 -5
  219. package/src/web/search/providers/searxng.ts +47 -4
  220. package/src/web/search/providers/xai.ts +7 -2
  221. package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +0 -23
  222. package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +0 -30
  223. package/src/gjc-runtime/ultragoal-pipeline.ts +0 -306
@@ -209,7 +209,12 @@ export function buildActionMarkdown(action: {
209
209
  if (action.kind === "idle") {
210
210
  return action.summary ? `🟢 Agent idle\n${action.summary}` : "🟢 Agent idle";
211
211
  }
212
- const heading = `❓ **${action.question ?? "Question"}**`;
212
+ const question = (action.question ?? "Question")
213
+ .split(/\r\n|\r|\n/)
214
+ .map(line => line.trimEnd())
215
+ .map(line => (line ? `**${line}**` : ""))
216
+ .join(" \n");
217
+ const heading = `❓ ${question}`;
213
218
  const options = action.options ?? [];
214
219
  const displayOptions = withRecommendedOptionLabel(options, action.recommendedIndex);
215
220
  if (options.length === 0) return `${heading}\n\n(reply with text)`;
@@ -217,19 +222,43 @@ export function buildActionMarkdown(action: {
217
222
  return `${heading}\n\n${list}`;
218
223
  }
219
224
 
225
+ export type TelegramNotificationSound = "all" | "important" | "none";
226
+
227
+ export type TelegramDeliveryLane = "ask" | "idle" | "live" | "finalized";
228
+
229
+ /**
230
+ * Resolve Telegram's optional silent-delivery flag. `finalChunk` silences
231
+ * non-final actionable chunks under the `important` policy; `undefined`
232
+ * intentionally omits the field so Telegram uses its normal audible delivery
233
+ * behavior.
234
+ */
235
+ export function telegramDisableNotification(
236
+ sound: TelegramNotificationSound | undefined,
237
+ lane: TelegramDeliveryLane,
238
+ finalChunk = true,
239
+ ): true | undefined {
240
+ if (sound === "none") return true;
241
+ if (sound !== "important") return undefined;
242
+ return (lane === "ask" || lane === "idle") && finalChunk ? undefined : true;
243
+ }
244
+
220
245
  /** Send Telegram HTML text chunks sequentially so long messages preserve order. */
221
246
  export async function sendTelegramHtmlChunks(
222
247
  send: TelegramSend,
223
248
  chatId: string,
224
249
  text: string,
225
250
  inlineKeyboard?: InlineButton[][],
251
+ sound?: TelegramNotificationSound,
252
+ lane: TelegramDeliveryLane = "ask",
226
253
  ): Promise<void> {
227
254
  const chunks = splitTelegramHtml(text);
228
255
  for (let i = 0; i < chunks.length; i++) {
256
+ const disableNotification = telegramDisableNotification(sound, lane, i === chunks.length - 1);
229
257
  await send("sendMessage", {
230
258
  chat_id: chatId,
231
259
  text: chunks[i]!,
232
260
  parse_mode: TELEGRAM_PARSE_MODE,
261
+ ...(disableNotification === true ? { disable_notification: true } : {}),
233
262
  ...(i === chunks.length - 1 && inlineKeyboard ? { reply_markup: { inline_keyboard: inlineKeyboard } } : {}),
234
263
  });
235
264
  }
@@ -348,6 +377,7 @@ export interface TelegramReferenceOptions {
348
377
  endpointFile: string;
349
378
  apiBase?: string;
350
379
  fetchImpl?: typeof fetch;
380
+ sound?: TelegramNotificationSound;
351
381
  }
352
382
 
353
383
  /**
@@ -395,7 +425,14 @@ export async function runTelegramReferenceClient(opts: TelegramReferenceOptions)
395
425
  controls: msg.controls,
396
426
  summary: msg.summary,
397
427
  });
398
- await sendTelegramHtmlChunks(send, opts.chatId, rendered.text, rendered.inline_keyboard);
428
+ await sendTelegramHtmlChunks(
429
+ send,
430
+ opts.chatId,
431
+ rendered.text,
432
+ rendered.inline_keyboard,
433
+ opts.sound,
434
+ msg.kind ?? "ask",
435
+ );
399
436
  } else if (msg.type === "action_unavailable") {
400
437
  const requiredCapabilities = Array.isArray(msg.requiredCapabilities)
401
438
  ? msg.requiredCapabilities
@@ -416,7 +453,7 @@ export async function runTelegramReferenceClient(opts: TelegramReferenceOptions)
416
453
  // session's forum topic; this reference shows the minimal handling.
417
454
  const threaded = renderThreadedFrame(msg as never);
418
455
  if (threaded?.text) {
419
- await sendTelegramHtmlChunks(send, opts.chatId, threaded.text);
456
+ await sendTelegramHtmlChunks(send, opts.chatId, threaded.text, undefined, opts.sound, threaded.lane);
420
457
  }
421
458
  }
422
459
  };
@@ -18,13 +18,13 @@ export interface ThreadedSend {
18
18
  method: "sendMessage" | "sendPhoto" | "sendDocument";
19
19
  /** Rate-limit lane for prioritisation/fairness. */
20
20
  lane: RateLimitLane;
21
- /** Message text (sendMessage) or photo caption (sendPhoto). */
21
+ /** Message text (sendMessage) or media caption (sendPhoto/sendDocument). */
22
22
  text?: string;
23
23
  /** Base64 image bytes for sendPhoto. */
24
24
  photoBase64?: string;
25
25
  /** Base64 file bytes for sendDocument. */
26
26
  documentBase64?: string;
27
- /** Image MIME type for sendPhoto. */
27
+ /** Uploaded media MIME type. */
28
28
  mime?: string;
29
29
  /** Suggested document filename. */
30
30
  fileName?: string;
@@ -100,6 +100,40 @@ function str(v: unknown): string | undefined {
100
100
  return typeof v === "string" && v.length > 0 ? v : undefined;
101
101
  }
102
102
 
103
+ function baseMimeType(mime: string): string {
104
+ return mime.split(";", 1)[0]!.trim().toLowerCase();
105
+ }
106
+
107
+ export function supportsTelegramPhotoUpload(mime: string | undefined): boolean {
108
+ if (mime === undefined) return true;
109
+ const normalized = baseMimeType(mime);
110
+ return normalized === "image/jpeg" || normalized === "image/png";
111
+ }
112
+
113
+ function imageDocumentFileName(mime: string | undefined): string {
114
+ switch (mime === undefined ? "" : baseMimeType(mime)) {
115
+ case "image/webp":
116
+ return "image.webp";
117
+ case "image/gif":
118
+ return "image.gif";
119
+ case "image/svg+xml":
120
+ return "image.svg";
121
+ case "image/avif":
122
+ return "image.avif";
123
+ case "image/bmp":
124
+ case "image/x-ms-bmp":
125
+ return "image.bmp";
126
+ case "image/tiff":
127
+ return "image.tiff";
128
+ case "image/heic":
129
+ return "image.heic";
130
+ case "image/heif":
131
+ return "image.heif";
132
+ default:
133
+ return "image";
134
+ }
135
+ }
136
+
103
137
  function isDotOnlyText(value: string): boolean {
104
138
  return /^[.\s]+$/.test(value);
105
139
  }
@@ -244,11 +278,22 @@ export function renderThreadedFrame(frame: ThreadedFrame): ThreadedSend | undefi
244
278
  const data = str(frame.data);
245
279
  if (!data) return undefined;
246
280
  const caption = str(frame.caption);
281
+ const mime = str(frame.mime);
282
+ if (!supportsTelegramPhotoUpload(mime)) {
283
+ return {
284
+ method: "sendDocument",
285
+ lane: "finalized",
286
+ documentBase64: data,
287
+ mime,
288
+ fileName: imageDocumentFileName(mime),
289
+ text: finalizeTelegramHtml(caption === undefined ? undefined : escapeHtml(caption)),
290
+ };
291
+ }
247
292
  return {
248
293
  method: "sendPhoto",
249
294
  lane: "finalized",
250
295
  photoBase64: data,
251
- mime: str(frame.mime),
296
+ mime,
252
297
  text: finalizeTelegramHtml(caption === undefined ? undefined : escapeHtml(caption)),
253
298
  };
254
299
  }
@@ -43,6 +43,8 @@ export interface TopicRecord {
43
43
  creationLeaseEpoch?: number;
44
44
  /** An uncertain delete fences future creation and inbound routing. */
45
45
  authorityState?: "active" | "delete_pending";
46
+ /** Provenance for topics created by the user and adopted by GJC. Missing means daemon-created. */
47
+ topicOrigin?: "user_created";
46
48
  /** Telegram chat and endpoint authority last proven to use this topic. */
47
49
  chatId?: string;
48
50
  /** Canonical endpoint tuple (URL + token) that currently holds the lease. */
@@ -234,6 +236,7 @@ export class TopicRegistry {
234
236
  ...(raw.authorityState === "delete_pending" || fenceSupersedesRecord
235
237
  ? { authorityState: "delete_pending" as const }
236
238
  : {}),
239
+ ...(raw.topicOrigin === "user_created" ? { topicOrigin: "user_created" as const } : {}),
237
240
  ...(isValidBindingString(raw.chatId) ? { chatId: raw.chatId } : {}),
238
241
  ...(isValidBindingString(raw.endpointKey) ? { endpointKey: raw.endpointKey } : {}),
239
242
  ...(isValidBindingString(raw.endpointDigest) ? { endpointDigest: raw.endpointDigest } : {}),
@@ -307,6 +310,21 @@ export class TopicRegistry {
307
310
  const record = this.topics.get(sessionId);
308
311
  return record?.authorityState !== "delete_pending" && this.byTopic.get(record?.topicId ?? "") === sessionId;
309
312
  }
313
+ /**
314
+ * Pure read-only availability check for user-topic adoption. Rejects invalid
315
+ * ids and any id already committed, delete-pending/fenced, staged, or
316
+ * ambiguous, without mutating maps, epochs, or persistence. Used inside the
317
+ * {@link getOrCreateTopic} create callback to fail-closed before adopting a
318
+ * user-created topic id.
319
+ */
320
+ isTopicIdAvailable(topicId: string): boolean {
321
+ if (!isValidTopicId(topicId)) return false;
322
+ if (this.#ambiguousTopicIds.has(topicId)) return false;
323
+ if (this.byTopic.has(topicId)) return false;
324
+ for (const record of this.topics.values()) if (record.topicId === topicId) return false;
325
+ for (const record of this.staged.values()) if (record.topicId === topicId) return false;
326
+ return true;
327
+ }
310
328
 
311
329
  /**
312
330
  * Resolve endpoint authority for identity-less replay. An endpoint may bootstrap
@@ -455,6 +473,7 @@ export class TopicRegistry {
455
473
  binding?: TopicEndpointBinding,
456
474
  commit?: () => Promise<void>,
457
475
  transientClaimant?: object,
476
+ topicOrigin?: TopicRecord["topicOrigin"],
458
477
  ): Promise<TopicRecord> {
459
478
  const existing = this.topics.get(sessionId);
460
479
  if (existing?.authorityState === "delete_pending") throw new Error("topic authority is deletion-fenced");
@@ -491,6 +510,7 @@ export class TopicRegistry {
491
510
  }
492
511
  : {}),
493
512
  ...(revoked ? { authorityState: "delete_pending" as const } : {}),
513
+ ...(topicOrigin === "user_created" ? { topicOrigin: "user_created" as const } : {}),
494
514
  };
495
515
  if (revoked) {
496
516
  this.topics.set(sessionId, record);
@@ -2,6 +2,32 @@ import { createHash } from "node:crypto";
2
2
  import { chmod, mkdir, mkdtemp, open, readFile, rename, rm, unlink } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
+ import { postmortem } from "@gajae-code/utils";
6
+
7
+ /**
8
+ * Spill directories this process created under the system temp dir.
9
+ *
10
+ * `close()` removes a store's own directory, but an abnormal exit never reaches
11
+ * it, and these hold real spilled snapshot data rather than a marker. The two
12
+ * sibling temp-artifact caches (`shell-snapshot`, `python-runner-artifact`)
13
+ * already register a postmortem sweep; this does the same.
14
+ *
15
+ * Only `mkdtemp` directories are tracked. A caller-supplied `storageDir` is
16
+ * durable session storage and must never be swept here.
17
+ */
18
+ const processSpillDirectories = new Set<string>();
19
+
20
+ /** Test seam: spill directories currently tracked for postmortem cleanup. */
21
+ export function trackedSpillDirectoriesForTest(): string[] {
22
+ return [...processSpillDirectories];
23
+ }
24
+
25
+ postmortem.register("sdk-revision-store:spill", async () => {
26
+ for (const dir of [...processSpillDirectories]) {
27
+ processSpillDirectories.delete(dir);
28
+ await rm(dir, { recursive: true, force: true }).catch(() => undefined);
29
+ }
30
+ });
5
31
 
6
32
  export const MAX_REVISIONS_PER_RESOURCE = 8;
7
33
  export const MAX_PINNED_REVISIONS = 128;
@@ -512,7 +538,10 @@ export class RevisionStore {
512
538
  this.#memoryBytes = 0;
513
539
  this.#chunkRefs.clear();
514
540
  this.#manifestRefs.clear();
515
- if (this.#directory) await rm(this.#directory, { recursive: true, force: true });
541
+ if (this.#directory) {
542
+ processSpillDirectories.delete(this.#directory);
543
+ await rm(this.#directory, { recursive: true, force: true });
544
+ }
516
545
  })();
517
546
  return this.#closePromise;
518
547
  }
@@ -1040,7 +1069,12 @@ export class RevisionStore {
1040
1069
  }
1041
1070
 
1042
1071
  async #spillDirectory(): Promise<string> {
1043
- if (!this.#directory) this.#directory = await mkdtemp(join(tmpdir(), "gjc-sdk-snapshots-"));
1072
+ if (!this.#directory) {
1073
+ this.#directory = await mkdtemp(join(tmpdir(), "gjc-sdk-snapshots-"));
1074
+ // Only temp spill directories are swept on abnormal exit; a caller-supplied
1075
+ // storageDir is durable and never enters this set.
1076
+ processSpillDirectories.add(this.#directory);
1077
+ }
1044
1078
  await mkdir(join(this.#directory, "objects"), { recursive: true, mode: 0o700 });
1045
1079
  await mkdir(join(this.#directory, "manifests"), { recursive: true, mode: 0o700 });
1046
1080
  await chmod(this.#directory, 0o700);
@@ -41,6 +41,8 @@ interface Outstanding {
41
41
  leaseId: string;
42
42
  resolve: (value: unknown) => void;
43
43
  reject: (reason: Error) => void;
44
+ signal?: AbortSignal;
45
+ onAbort?: () => void;
44
46
  }
45
47
 
46
48
  export interface ReverseLeaseOptions {
@@ -188,7 +190,7 @@ export class ReverseLeaseRuntime {
188
190
  this.#cancelForConnection(connectionId, "provider_disconnected");
189
191
  }
190
192
 
191
- request(capability: string, method: string, payload: unknown): Promise<unknown> {
193
+ request(capability: string, method: string, payload: unknown, signal?: AbortSignal): Promise<unknown> {
192
194
  if (this.#disposing) throw new Error("reverse runtime is disposing");
193
195
  this.#assertPayload(payload);
194
196
  const lease = this.#liveLease(capability);
@@ -198,16 +200,42 @@ export class ReverseLeaseRuntime {
198
200
  throw new ReverseLeaseError("lease_unavailable");
199
201
  throw new ReverseLeaseError("provider_required");
200
202
  }
203
+ if (signal?.aborted)
204
+ return Promise.reject(Object.assign(new Error("request_cancelled"), { name: "request_cancelled" }));
201
205
  if (this.#outstanding.size >= MAX_REVERSE_OUTSTANDING) throw new ReverseLeaseError("too_many_outstanding");
202
206
  const id = randomUUID();
203
207
  return new Promise((resolve, reject) => {
204
- this.#outstanding.set(id, {
208
+ const outstanding: Outstanding = {
205
209
  connectionId: lease.connectionId,
206
210
  capability,
207
211
  leaseId: lease.leaseId,
208
212
  resolve,
209
213
  reject,
210
- });
214
+ ...(signal ? { signal } : {}),
215
+ };
216
+ this.#outstanding.set(id, outstanding);
217
+ if (signal) {
218
+ outstanding.onAbort = () => {
219
+ if (this.#takeOutstanding(id) !== outstanding) return;
220
+ try {
221
+ const cancellation = this.#sendFrame(lease.connectionId, {
222
+ type: "reverse_cancel",
223
+ id,
224
+ connectionId: lease.connectionId,
225
+ leaseId: lease.leaseId,
226
+ });
227
+ void Promise.resolve(cancellation).catch(() => {});
228
+ } catch {
229
+ // Cancellation is best effort; the caller is already settled locally.
230
+ }
231
+ reject(Object.assign(new Error("request_cancelled"), { name: "request_cancelled" }));
232
+ };
233
+ signal.addEventListener("abort", outstanding.onAbort, { once: true });
234
+ if (signal.aborted) {
235
+ outstanding.onAbort();
236
+ return;
237
+ }
238
+ }
211
239
  let delivery: void | Promise<void>;
212
240
  try {
213
241
  delivery = this.#sendFrame(lease.connectionId, {
@@ -219,12 +247,12 @@ export class ReverseLeaseRuntime {
219
247
  payload: { method, payload },
220
248
  });
221
249
  } catch (error) {
222
- this.#outstanding.delete(id);
250
+ this.#takeOutstanding(id);
223
251
  reject(error instanceof Error ? error : new Error(String(error)));
224
252
  return;
225
253
  }
226
254
  Promise.resolve(delivery).catch(error => {
227
- this.#outstanding.delete(id);
255
+ if (this.#takeOutstanding(id) !== outstanding) return;
228
256
  reject(error instanceof Error ? error : new Error(String(error)));
229
257
  });
230
258
  });
@@ -242,7 +270,7 @@ export class ReverseLeaseRuntime {
242
270
  if (!request) throw new ReverseLeaseError("unknown_request");
243
271
  if (request.connectionId !== connectionId || request.leaseId !== leaseId)
244
272
  throw new ReverseLeaseError("not_lease_owner");
245
- this.#outstanding.delete(id);
273
+ this.#takeOutstanding(id);
246
274
  if (error) {
247
275
  const rejection = new Error(error.message);
248
276
  rejection.name = error.code;
@@ -270,6 +298,8 @@ export class ReverseLeaseRuntime {
270
298
  this.#installedCapabilities.clear();
271
299
  this.#leases.clear();
272
300
  this.#idempotency.clear();
301
+ for (const request of outstanding.map(([, request]) => request))
302
+ if (request.signal && request.onAbort) request.signal.removeEventListener("abort", request.onAbort);
273
303
  for (const [id, request] of outstanding) {
274
304
  request.reject(new Error("request_cancelled"));
275
305
  try {
@@ -304,11 +334,18 @@ export class ReverseLeaseRuntime {
304
334
  #cancelForConnection(connectionId: string, reason: "provider_disconnected" | "lease_released"): void {
305
335
  for (const [id, request] of this.#outstanding)
306
336
  if (request.connectionId === connectionId) {
307
- this.#outstanding.delete(id);
337
+ this.#takeOutstanding(id);
308
338
  request.reject(new Error("request_cancelled"));
309
339
  this.#onCancel?.(id, reason);
310
340
  }
311
341
  }
342
+ #takeOutstanding(id: string): Outstanding | undefined {
343
+ const request = this.#outstanding.get(id);
344
+ if (!request) return undefined;
345
+ this.#outstanding.delete(id);
346
+ if (request.signal && request.onAbort) request.signal.removeEventListener("abort", request.onAbort);
347
+ return request;
348
+ }
312
349
  #installDefinitionsFor(capability: string, definitions: unknown): void {
313
350
  this.#installDefinitions?.(capability, definitions);
314
351
  this.#installedCapabilities.add(capability);
@@ -17,8 +17,10 @@
17
17
  * prior outcome unknown — callers MUST NOT reuse a clientRef as a retry
18
18
  * mechanism and MUST treat `unknown` as uncertainty, not proof of
19
19
  * non-execution.
20
- * - The durability floor is the live session process: after restart or
21
- * retained-record eviction, a lookup honestly reports `unknown`.
20
+ * - Prompt records survive process restart: an active prompt is finalized from its
21
+ * durable pending outcome, or as `prompt_failed` when it has none. Only skill
22
+ * records settle as `process_restart`, and a lookup reports `unknown` only after
23
+ * retained-record TTL/capacity eviction.
22
24
  * - Q26 tracks prompts accepted through the SDK control surface (which always
23
25
  * carries a requesting connection); submissions without a delivery owner are
24
26
  * outside the reconciliation contract and hold no reservation.
@@ -38,8 +40,17 @@ export interface TurnPromptInput {
38
40
  clientRef?: string;
39
41
  }
40
42
 
41
- /** Terminal outcome is preserved exactly; active records never age into terminal. */
43
+ /**
44
+ * Terminal outcome is preserved exactly; active records never age into terminal. A
45
+ * prompt that is active at process restart is finalized from its durable pending
46
+ * outcome (or `prompt_failed` when it has none), so it never reports as unknown.
47
+ */
42
48
  export type PromptReconciliationStatus = "accepted" | "in_flight" | "terminal_ok" | "failed";
49
+ export type SdkPromptStopReason = "end_turn" | "max_tokens" | "max_turn_requests" | "refusal" | "cancelled";
50
+ export type SdkPromptFailureCode = "prompt_failed" | "prompt_deadline_exceeded";
51
+ export type SdkPromptTerminalOutcome =
52
+ | { kind: "stopped"; reason: SdkPromptStopReason; provenance: "agent" | "client_cancel" }
53
+ | { kind: "failed"; code: SdkPromptFailureCode; message: string; provenance: "agent_failed" | "deadline" };
43
54
 
44
55
  /** Exactly one selector per lookup. */
45
56
  export type TurnPromptStatusSelector = { clientRef: string } | { commandId: string; turnId: string };
@@ -67,6 +78,7 @@ export interface TurnPromptReconciliationTerminalOk extends TurnPromptReconcilia
67
78
  startedAt?: number;
68
79
  /** Epoch milliseconds of the terminal transition. */
69
80
  terminalAt: number;
81
+ outcome?: SdkPromptTerminalOutcome;
70
82
  }
71
83
 
72
84
  export interface TurnPromptReconciliationFailed extends TurnPromptReconciliationIdentity {
@@ -75,6 +87,7 @@ export interface TurnPromptReconciliationFailed extends TurnPromptReconciliation
75
87
  terminalAt: number;
76
88
  /** Bounded, sanitized failure detail (code safe-token ≤64, message ≤512). */
77
89
  error: { code: string; message: string };
90
+ outcome?: SdkPromptTerminalOutcome;
78
91
  }
79
92
 
80
93
  export interface TurnPromptReconciliationUnknown {
@@ -3656,6 +3656,17 @@
3656
3656
  "packages/coding-agent/test/sdk-operation-inventory.test.ts"
3657
3657
  ]
3658
3658
  },
3659
+ {
3660
+ "sourceId": "agent_session:abortPromptAndWait",
3661
+ "sourceFile": "packages/coding-agent/src/session/agent-session.ts",
3662
+ "sourceKind": "agent_session",
3663
+ "decision": "exclude",
3664
+ "rationale": "internal SDK prompt-terminalization resource fence over a host-captured run handle, not an independent public SDK control seam",
3665
+ "exclusionMetadata": {
3666
+ "adapterMappings": "not_applicable",
3667
+ "testIds": "not_applicable"
3668
+ }
3669
+ },
3659
3670
  {
3660
3671
  "sourceId": "agent_session:cancelAndSubmit",
3661
3672
  "sourceFile": "packages/coding-agent/src/session/agent-session.ts",
@@ -3852,6 +3863,17 @@
3852
3863
  "testIds": "not_applicable"
3853
3864
  }
3854
3865
  },
3866
+ {
3867
+ "sourceId": "agent_session:resolveConfiguredDefaultModel",
3868
+ "sourceFile": "packages/coding-agent/src/session/agent-session.ts",
3869
+ "sourceKind": "agent_session",
3870
+ "decision": "exclude",
3871
+ "rationale": "internal accessor/plumbing, not a user-facing control seam",
3872
+ "exclusionMetadata": {
3873
+ "adapterMappings": "not_applicable",
3874
+ "testIds": "not_applicable"
3875
+ }
3876
+ },
3855
3877
  {
3856
3878
  "sourceId": "agent_session:recordResumeDefaultModel",
3857
3879
  "sourceFile": "packages/coding-agent/src/session/agent-session.ts",