@gajae-code/coding-agent 0.9.6 → 0.10.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 (226) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dist/types/cli/classify-install-target.d.ts +0 -10
  3. package/dist/types/cli/daemon-cli.d.ts +2 -0
  4. package/dist/types/cli/initial-message.d.ts +1 -1
  5. package/dist/types/cli/malloc-env-guard.d.ts +65 -0
  6. package/dist/types/cli/malloc-env-guard.test.d.ts +1 -0
  7. package/dist/types/cli/notify-cli.d.ts +3 -1
  8. package/dist/types/cli/session-picker.d.ts +4 -3
  9. package/dist/types/cli/update-cli.d.ts +22 -3
  10. package/dist/types/cli.d.ts +5 -0
  11. package/dist/types/commands/daemon.d.ts +6 -2
  12. package/dist/types/commands/launch.d.ts +1 -0
  13. package/dist/types/commands/notify.d.ts +6 -0
  14. package/dist/types/commit/analysis/conventional.d.ts +1 -1
  15. package/dist/types/commit/analysis/summary.d.ts +1 -1
  16. package/dist/types/commit/changelog/generate.d.ts +1 -1
  17. package/dist/types/commit/changelog/index.d.ts +2 -2
  18. package/dist/types/commit/map-reduce/map-phase.d.ts +1 -1
  19. package/dist/types/commit/map-reduce/reduce-phase.d.ts +1 -1
  20. package/dist/types/config/keybindings.d.ts +5 -0
  21. package/dist/types/config/model-profile-activation.d.ts +1 -0
  22. package/dist/types/config/settings-schema.d.ts +47 -7
  23. package/dist/types/config/settings.d.ts +2 -0
  24. package/dist/types/coordinator/contract.d.ts +1 -1
  25. package/dist/types/coordinator-mcp/model-preset.d.ts +45 -0
  26. package/dist/types/coordinator-mcp/policy.d.ts +3 -0
  27. package/dist/types/coordinator-mcp/server.d.ts +79 -6
  28. package/dist/types/coordinator-mcp/session-reaper.d.ts +53 -0
  29. package/dist/types/daemon/control-types.d.ts +10 -0
  30. package/dist/types/daemon/operator-contract.d.ts +47 -0
  31. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.d.ts +4 -0
  32. package/dist/types/discovery/helpers.d.ts +1 -1
  33. package/dist/types/discovery/substitute-plugin-root.d.ts +0 -4
  34. package/dist/types/extensibility/extensions/types.d.ts +3 -1
  35. package/dist/types/gjc-runtime/launch-tmux.d.ts +26 -7
  36. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +34 -3
  37. package/dist/types/gjc-runtime/tmux-common.d.ts +6 -0
  38. package/dist/types/gjc-runtime/tmux-owner-isolation-cli.d.ts +5 -0
  39. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +309 -0
  40. package/dist/types/gjc-runtime/tmux-sessions.d.ts +49 -17
  41. package/dist/types/internal-urls/local-protocol.d.ts +13 -9
  42. package/dist/types/main.d.ts +66 -2
  43. package/dist/types/modes/acp/acp-agent.d.ts +19 -2
  44. package/dist/types/modes/acp/acp-mode.d.ts +12 -0
  45. package/dist/types/modes/components/assistant-message.d.ts +1 -1
  46. package/dist/types/modes/components/custom-editor.d.ts +2 -0
  47. package/dist/types/modes/components/gajae-pet-widget.d.ts +59 -0
  48. package/dist/types/modes/components/hook-selector.d.ts +5 -0
  49. package/dist/types/modes/components/irc-sidebar.d.ts +32 -0
  50. package/dist/types/modes/components/pet-selector.d.ts +11 -0
  51. package/dist/types/modes/components/session-selector.d.ts +22 -9
  52. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  53. package/dist/types/modes/components/status-line/types.d.ts +1 -1
  54. package/dist/types/modes/components/status-line.d.ts +0 -10
  55. package/dist/types/modes/components/tool-execution.d.ts +1 -0
  56. package/dist/types/modes/components/user-message.d.ts +2 -1
  57. package/dist/types/modes/controllers/event-controller.d.ts +4 -0
  58. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  59. package/dist/types/modes/interactive-mode.d.ts +24 -2
  60. package/dist/types/modes/irc-observation-ledger.d.ts +22 -0
  61. package/dist/types/modes/print-mode.d.ts +3 -4
  62. package/dist/types/modes/rpc/rpc-client.d.ts +7 -1
  63. package/dist/types/modes/rpc/rpc-mode.d.ts +2 -1
  64. package/dist/types/modes/rpc/rpc-types.d.ts +17 -1
  65. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +8 -0
  66. package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +5 -0
  67. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +45 -0
  68. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +42 -2
  69. package/dist/types/modes/theme/defaults/index.d.ts +568 -568
  70. package/dist/types/modes/types.d.ts +28 -2
  71. package/dist/types/modes/utils/abort-message.d.ts +1 -1
  72. package/dist/types/modes/utils/context-usage.d.ts +4 -19
  73. package/dist/types/modes/utils/irc-message.d.ts +38 -0
  74. package/dist/types/modes/utils/ui-helpers.d.ts +13 -4
  75. package/dist/types/notifications/config.d.ts +34 -0
  76. package/dist/types/notifications/daemon-paths.d.ts +7 -0
  77. package/dist/types/notifications/index.d.ts +27 -0
  78. package/dist/types/notifications/lifecycle-control-runtime.d.ts +12 -4
  79. package/dist/types/notifications/notification-service.d.ts +144 -0
  80. package/dist/types/notifications/rate-limit-pool.d.ts +27 -9
  81. package/dist/types/notifications/telegram-daemon-control.d.ts +6 -0
  82. package/dist/types/notifications/telegram-daemon.d.ts +130 -3
  83. package/dist/types/notifications/telegram-reference.d.ts +19 -2
  84. package/dist/types/notifications/topic-registry.d.ts +24 -4
  85. package/dist/types/registry/agent-registry.d.ts +2 -0
  86. package/dist/types/rlm/index.d.ts +1 -1
  87. package/dist/types/sdk.d.ts +2 -0
  88. package/dist/types/secrets/obfuscator.d.ts +1 -1
  89. package/dist/types/session/agent-session.d.ts +40 -3
  90. package/dist/types/session/cache-economics.d.ts +79 -0
  91. package/dist/types/session/context-estimation.d.ts +31 -0
  92. package/dist/types/session/provider-safety-stop.d.ts +5 -0
  93. package/dist/types/session/session-manager.d.ts +124 -4
  94. package/dist/types/session/session-storage.d.ts +155 -13
  95. package/dist/types/setup/credential-auto-import.d.ts +2 -2
  96. package/dist/types/task/executor.d.ts +3 -0
  97. package/dist/types/task/receipt.d.ts +3 -1
  98. package/dist/types/task/types.d.ts +8 -0
  99. package/dist/types/tools/ask-answer-registry.d.ts +5 -0
  100. package/dist/types/tools/ask.d.ts +25 -1
  101. package/dist/types/tools/bash.d.ts +1 -1
  102. package/dist/types/tools/browser/tab-supervisor.d.ts +6 -0
  103. package/dist/types/tools/index.d.ts +58 -6
  104. package/dist/types/tools/telegram-send.d.ts +7 -0
  105. package/dist/types/utils/external-editor.d.ts +2 -0
  106. package/package.json +9 -9
  107. package/scripts/g004-tmux-smoke.ts +196 -36
  108. package/src/cli/daemon-cli.ts +19 -29
  109. package/src/cli/malloc-env-guard.test.ts +104 -0
  110. package/src/cli/malloc-env-guard.ts +149 -0
  111. package/src/cli/notify-cli.ts +78 -14
  112. package/src/cli/session-picker.ts +23 -40
  113. package/src/cli/update-cli.ts +56 -37
  114. package/src/cli.ts +51 -10
  115. package/src/commands/daemon.ts +19 -5
  116. package/src/commands/harness.ts +515 -47
  117. package/src/commands/launch.ts +11 -3
  118. package/src/commands/notify.ts +5 -1
  119. package/src/commands/session.ts +6 -1
  120. package/src/config/keybindings.ts +5 -0
  121. package/src/config/model-profile-activation.ts +1 -1
  122. package/src/config/model-profiles.ts +14 -14
  123. package/src/config/settings-schema.ts +41 -1
  124. package/src/config/settings.ts +96 -45
  125. package/src/coordinator/contract.ts +1 -0
  126. package/src/coordinator-mcp/model-preset.ts +100 -0
  127. package/src/coordinator-mcp/policy.ts +30 -0
  128. package/src/coordinator-mcp/server.ts +2881 -425
  129. package/src/coordinator-mcp/session-reaper.ts +129 -0
  130. package/src/daemon/control-types.ts +11 -0
  131. package/src/daemon/operator-contract.ts +121 -0
  132. package/src/deep-interview/render-middleware.ts +67 -2
  133. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +57 -17
  134. package/src/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.ts +15 -2
  135. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +5 -1
  136. package/src/extensibility/extensions/runner.ts +1 -1
  137. package/src/extensibility/extensions/types.ts +3 -1
  138. package/src/gjc-runtime/launch-tmux.ts +702 -135
  139. package/src/gjc-runtime/session-state-sidecar.ts +756 -135
  140. package/src/gjc-runtime/team-runtime.ts +6 -0
  141. package/src/gjc-runtime/tmux-common.ts +26 -7
  142. package/src/gjc-runtime/tmux-gc.ts +52 -9
  143. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +185 -0
  144. package/src/gjc-runtime/tmux-owner-isolation.ts +2060 -0
  145. package/src/gjc-runtime/tmux-sessions.ts +871 -68
  146. package/src/internal-urls/docs-index.generated.ts +12 -11
  147. package/src/internal-urls/local-protocol.ts +29 -10
  148. package/src/main.ts +301 -80
  149. package/src/modes/acp/acp-agent.ts +730 -100
  150. package/src/modes/acp/acp-mode.ts +29 -2
  151. package/src/modes/components/assistant-message.ts +32 -3
  152. package/src/modes/components/bash-execution.ts +43 -13
  153. package/src/modes/components/custom-editor.ts +5 -0
  154. package/src/modes/components/footer.ts +3 -3
  155. package/src/modes/components/gajae-pet-widget.ts +360 -0
  156. package/src/modes/components/hook-selector.ts +12 -0
  157. package/src/modes/components/irc-sidebar.ts +206 -0
  158. package/src/modes/components/pet-selector.ts +55 -0
  159. package/src/modes/components/session-selector.ts +222 -221
  160. package/src/modes/components/settings-selector.ts +10 -0
  161. package/src/modes/components/status-line/segments.ts +13 -6
  162. package/src/modes/components/status-line/types.ts +1 -1
  163. package/src/modes/components/status-line.ts +4 -171
  164. package/src/modes/components/tool-execution.ts +47 -6
  165. package/src/modes/components/user-message.ts +26 -9
  166. package/src/modes/controllers/command-controller.ts +28 -2
  167. package/src/modes/controllers/event-controller.ts +159 -36
  168. package/src/modes/controllers/extension-ui-controller.ts +37 -18
  169. package/src/modes/controllers/input-controller.ts +8 -2
  170. package/src/modes/controllers/selector-controller.ts +62 -15
  171. package/src/modes/interactive-mode.ts +199 -7
  172. package/src/modes/irc-observation-ledger.ts +127 -0
  173. package/src/modes/print-mode.ts +228 -92
  174. package/src/modes/rpc/rpc-client.ts +58 -1
  175. package/src/modes/rpc/rpc-mode.ts +286 -85
  176. package/src/modes/rpc/rpc-types.ts +15 -1
  177. package/src/modes/shared/agent-wire/command-dispatch.ts +33 -1
  178. package/src/modes/shared/agent-wire/command-validation.ts +10 -0
  179. package/src/modes/shared/agent-wire/deep-interview-gate.ts +45 -2
  180. package/src/modes/shared/agent-wire/scopes.ts +1 -0
  181. package/src/modes/shared/agent-wire/unattended-session.ts +288 -1
  182. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +130 -20
  183. package/src/modes/types.ts +34 -2
  184. package/src/modes/utils/context-usage.ts +56 -92
  185. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  186. package/src/modes/utils/irc-message.ts +138 -0
  187. package/src/modes/utils/ui-helpers.ts +216 -50
  188. package/src/notifications/config.ts +44 -0
  189. package/src/notifications/daemon-paths.ts +8 -0
  190. package/src/notifications/index.ts +535 -75
  191. package/src/notifications/lifecycle-control-runtime.ts +930 -77
  192. package/src/notifications/notification-service.ts +710 -0
  193. package/src/notifications/rate-limit-pool.ts +46 -12
  194. package/src/notifications/telegram-daemon-cli.ts +2 -0
  195. package/src/notifications/telegram-daemon-control.ts +27 -2
  196. package/src/notifications/telegram-daemon.ts +635 -94
  197. package/src/notifications/telegram-reference.ts +73 -13
  198. package/src/notifications/topic-registry.ts +86 -9
  199. package/src/prompts/system/irc-peer-roster.md +1 -0
  200. package/src/prompts/system/subagent-system-prompt.md +1 -0
  201. package/src/prompts/system/system-prompt.md +1 -0
  202. package/src/prompts/tools/irc.md +1 -1
  203. package/src/registry/agent-registry.ts +3 -0
  204. package/src/sdk.ts +52 -1
  205. package/src/secrets/obfuscator.ts +4 -2
  206. package/src/session/agent-session.ts +670 -217
  207. package/src/session/cache-economics.ts +275 -0
  208. package/src/session/context-estimation.ts +91 -0
  209. package/src/session/provider-safety-stop.ts +9 -0
  210. package/src/session/session-dump-format.ts +6 -0
  211. package/src/session/session-manager.ts +809 -39
  212. package/src/session/session-storage.ts +582 -38
  213. package/src/slash-commands/builtin-registry.ts +153 -9
  214. package/src/slash-commands/helpers/context-report.ts +26 -4
  215. package/src/task/executor.ts +12 -3
  216. package/src/task/index.ts +8 -0
  217. package/src/task/receipt.ts +7 -2
  218. package/src/task/types.ts +26 -0
  219. package/src/tools/ask-answer-registry.ts +25 -0
  220. package/src/tools/ask.ts +355 -33
  221. package/src/tools/browser/tab-supervisor.ts +63 -6
  222. package/src/tools/browser.ts +8 -3
  223. package/src/tools/index.ts +81 -6
  224. package/src/tools/sqlite-reader.ts +8 -4
  225. package/src/tui/output-block.ts +18 -6
  226. package/src/utils/external-editor.ts +6 -1
@@ -25,12 +25,30 @@ import {
25
25
  } from "./html-format";
26
26
  import { renderThreadedFrame } from "./threaded-render";
27
27
 
28
+ /** `ask_controls_v1` is a protocol version 3 wire token. Keep this local because
29
+ * `telegram-daemon.ts` imports this reference client. */
30
+ const REFERENCE_CLIENT_HELLO = {
31
+ type: "hello",
32
+ protocolVersion: 3,
33
+ capabilities: ["ask_controls_v1"],
34
+ } as const;
35
+
28
36
  /** One inline-keyboard button. */
29
37
  export interface InlineButton {
30
38
  text: string;
31
39
  callback_data: string;
32
40
  }
33
41
 
42
+ /** Typed action controls are protocol data, never inferred from option labels. */
43
+ export interface TelegramActionControl {
44
+ id: "navigation_forward";
45
+ kind: "navigation";
46
+ label: "Next" | "Done";
47
+ enabled: boolean;
48
+ }
49
+
50
+ export type TelegramCallbackAnswer = number | string | { controlId: TelegramActionControl["id"] };
51
+
34
52
  /** A rendered Telegram message for an `action_needed`. */
35
53
  export interface RenderedMessage {
36
54
  text: string;
@@ -51,10 +69,22 @@ export function decodeCallbackData(data: string): { id: string; index: number }
51
69
  return { index: Number(m[1]), id: m[2]! };
52
70
  }
53
71
 
72
+ /** Encode a typed control independently from option labels. */
73
+ export function encodeControlCallbackData(actionId: string, controlId: TelegramActionControl["id"]): string {
74
+ const encoded = `c:${controlId === "navigation_forward" ? "n" : controlId}:${actionId}`;
75
+ if (Buffer.byteLength(encoded, "utf8") > 64) throw new Error("control callback data exceeded Telegram limit");
76
+ return encoded;
77
+ }
78
+
79
+ export function decodeControlCallbackData(data: string): { id: string; controlId: TelegramActionControl["id"] } | null {
80
+ const match = /^c:n:(.+)$/.exec(data);
81
+ return match ? { controlId: "navigation_forward", id: match[1]! } : null;
82
+ }
83
+
54
84
  export interface CallbackRoute {
55
85
  sessionId: string;
56
86
  actionId: string;
57
- answer: number | string;
87
+ answer: TelegramCallbackAnswer;
58
88
  }
59
89
 
60
90
  export interface SerializedAliasTable {
@@ -78,7 +108,11 @@ function isCallbackRoute(value: unknown): value is CallbackRoute {
78
108
  return (
79
109
  typeof route.sessionId === "string" &&
80
110
  typeof route.actionId === "string" &&
81
- (typeof route.answer === "string" || typeof route.answer === "number")
111
+ (typeof route.answer === "string" ||
112
+ typeof route.answer === "number" ||
113
+ (typeof route.answer === "object" &&
114
+ route.answer !== null &&
115
+ (route.answer as { controlId?: unknown }).controlId === "navigation_forward"))
82
116
  );
83
117
  }
84
118
 
@@ -129,6 +163,7 @@ export function buildActionMessage(action: {
129
163
  id: string;
130
164
  question?: string;
131
165
  options?: string[];
166
+ controls?: readonly TelegramActionControl[];
132
167
  summary?: string;
133
168
  }): RenderedMessage {
134
169
  if (action.kind === "idle") {
@@ -137,9 +172,15 @@ export function buildActionMessage(action: {
137
172
  }
138
173
  const text = `❓ ${bold(action.question ?? "Question")}`;
139
174
  const options = action.options ?? [];
140
- if (options.length === 0) return { text: `${text}\n\n(reply with text)` };
141
- const body = `${text}\n\n${numberedOptionList(options)}`;
142
- const inline_keyboard = buildCompactChoiceGrid(options, i => encodeCallbackData(action.id, i));
175
+ const controls = (action.controls ?? []).filter(control => control.enabled);
176
+ if (options.length === 0 && controls.length === 0) return { text: `${text}\n\n(reply with text)` };
177
+ const body = options.length ? `${text}\n\n${numberedOptionList(options)}` : text;
178
+ const inline_keyboard = [
179
+ ...(options.length ? buildCompactChoiceGrid(options, i => encodeCallbackData(action.id, i)) : []),
180
+ ...controls.map(control => [
181
+ { text: control.label, callback_data: encodeControlCallbackData(action.id, control.id) },
182
+ ]),
183
+ ];
143
184
  return { text: body, inline_keyboard };
144
185
  }
145
186
 
@@ -182,7 +223,7 @@ export async function sendTelegramHtmlChunks(
182
223
  export interface ReplyFrame {
183
224
  type: "reply";
184
225
  id: string;
185
- answer: number | string;
226
+ answer: TelegramCallbackAnswer;
186
227
  token: string;
187
228
  }
188
229
 
@@ -195,17 +236,15 @@ export function telegramUpdateToReply(
195
236
  token: string,
196
237
  latestPendingAskId: string | undefined,
197
238
  ): ReplyFrame | null {
198
- const u = update as {
199
- callback_query?: { data?: string };
200
- message?: { text?: string };
201
- };
239
+ const u = update as { callback_query?: { data?: string }; message?: { text?: string } };
202
240
  if (u.callback_query?.data) {
203
241
  const decoded = decodeCallbackData(u.callback_query.data);
204
242
  if (decoded) return { type: "reply", id: decoded.id, answer: decoded.index, token };
243
+ const control = decodeControlCallbackData(u.callback_query.data);
244
+ if (control) return { type: "reply", id: control.id, answer: { controlId: control.controlId }, token };
205
245
  }
206
- if (u.message?.text && latestPendingAskId) {
246
+ if (u.message?.text && latestPendingAskId)
207
247
  return { type: "reply", id: latestPendingAskId, answer: u.message.text, token };
208
- }
209
248
  return null;
210
249
  }
211
250
 
@@ -239,7 +278,10 @@ function updateChatId(update: TelegramUpdateShape): string | undefined {
239
278
  return id === undefined || id === null ? undefined : String(id);
240
279
  }
241
280
 
242
- function routeWithAnswer(route: CallbackRoute | Omit<CallbackRoute, "answer">, answer: number | string): CallbackRoute {
281
+ function routeWithAnswer(
282
+ route: CallbackRoute | Omit<CallbackRoute, "answer">,
283
+ answer: TelegramCallbackAnswer,
284
+ ): CallbackRoute {
243
285
  return { sessionId: route.sessionId, actionId: route.actionId, answer };
244
286
  }
245
287
 
@@ -320,8 +362,10 @@ export async function runTelegramReferenceClient(opts: TelegramReferenceOptions)
320
362
  id?: string;
321
363
  question?: string;
322
364
  options?: string[];
365
+ controls?: TelegramActionControl[];
323
366
  summary?: string;
324
367
  reason?: string;
368
+ requiredCapabilities?: unknown;
325
369
  };
326
370
  if (msg.type === "action_needed" && msg.id) {
327
371
  if (msg.kind === "ask") latestPendingAskId = msg.id;
@@ -330,9 +374,22 @@ export async function runTelegramReferenceClient(opts: TelegramReferenceOptions)
330
374
  id: msg.id,
331
375
  question: msg.question,
332
376
  options: msg.options,
377
+ controls: msg.controls,
333
378
  summary: msg.summary,
334
379
  });
335
380
  await sendTelegramHtmlChunks(send, opts.chatId, rendered.text, rendered.inline_keyboard);
381
+ } else if (msg.type === "action_unavailable") {
382
+ const requiredCapabilities = Array.isArray(msg.requiredCapabilities)
383
+ ? msg.requiredCapabilities
384
+ .filter((capability): capability is string => typeof capability === "string")
385
+ .slice(0, 4)
386
+ .map(capability => capability.slice(0, 64))
387
+ : [];
388
+ console.warn(
389
+ `Telegram reference client: server withheld a controlled ask because this client lacks requiredCapabilities=[${requiredCapabilities.join(", ") || "unspecified"}].`,
390
+ );
391
+ // Diagnostic only: never turn it into a Telegram prompt or option buttons.
392
+ return;
336
393
  } else if (msg.type === "action_resolved" && msg.id === latestPendingAskId) {
337
394
  latestPendingAskId = undefined;
338
395
  } else {
@@ -347,6 +404,9 @@ export async function runTelegramReferenceClient(opts: TelegramReferenceOptions)
347
404
  };
348
405
 
349
406
  let messageQueue = Promise.resolve();
407
+ ws.addEventListener("open", () => {
408
+ if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(REFERENCE_CLIENT_HELLO));
409
+ });
350
410
  ws.addEventListener("message", ev => {
351
411
  const data = String(ev.data);
352
412
  messageQueue = messageQueue.catch(() => undefined).then(() => handleServerMessage(data));
@@ -21,8 +21,16 @@ export interface TopicRecord {
21
21
  identitySent: boolean;
22
22
  /** Creation timestamp (ms epoch). */
23
23
  createdAt: number;
24
- /** Last applied topic title (for rename detection). */
24
+ /** Last applied or observed Telegram topic title. */
25
25
  name?: string;
26
+ /** Naming authority. Missing values are legacy daemon-owned records. */
27
+ nameOwner?: "user";
28
+ /** Whether a user-owned name still needs a best-effort Telegram re-assert. */
29
+ nameReconcilePending?: boolean;
30
+ /** Last accepted Telegram update id for a user-owned name. */
31
+ userNameUpdateId?: number;
32
+ /** Stable repo/branch identity used when topic names are user-owned or customized. */
33
+ identityKey?: string;
26
34
  }
27
35
 
28
36
  /** Serialisable shape persisted to disk. */
@@ -48,13 +56,31 @@ export class TopicRegistry {
48
56
  private readonly inflight = new Map<string, Promise<TopicRecord>>();
49
57
 
50
58
  constructor(state: TopicRegistryState = emptyTopicRegistryState()) {
51
- this.topics = new Map(Object.entries(state.topics ?? {}));
52
- for (const [sessionId, record] of this.topics) this.byTopic.set(record.topicId, sessionId);
59
+ this.topics = new Map();
60
+ this.load(state);
53
61
  }
54
62
 
55
- /** Merge a serialized state into this registry, preserving all persisted fields. */
63
+ /** Merge serialized state and normalize authority fields from older releases. */
56
64
  load(state: TopicRegistryState): void {
57
- for (const [sessionId, record] of Object.entries(state.topics ?? {})) {
65
+ for (const [sessionId, raw] of Object.entries(state.topics ?? {})) {
66
+ if (!raw || typeof raw.topicId !== "string") continue;
67
+ const hasValidUserAuthority =
68
+ raw.nameOwner === "user" &&
69
+ typeof raw.name === "string" &&
70
+ raw.name.trim().length > 0 &&
71
+ typeof raw.userNameUpdateId === "number" &&
72
+ Number.isSafeInteger(raw.userNameUpdateId) &&
73
+ raw.userNameUpdateId >= 0;
74
+ const record: TopicRecord = {
75
+ topicId: raw.topicId,
76
+ identitySent: raw.identitySent === true,
77
+ createdAt: typeof raw.createdAt === "number" ? raw.createdAt : 0,
78
+ ...(typeof raw.name === "string" ? { name: raw.name } : {}),
79
+ ...(hasValidUserAuthority ? { nameOwner: "user" as const } : {}),
80
+ ...(hasValidUserAuthority && raw.nameReconcilePending === true ? { nameReconcilePending: true } : {}),
81
+ ...(hasValidUserAuthority ? { userNameUpdateId: raw.userNameUpdateId } : {}),
82
+ ...(typeof raw.identityKey === "string" ? { identityKey: raw.identityKey } : {}),
83
+ };
58
84
  this.topics.set(sessionId, record);
59
85
  this.byTopic.set(record.topicId, sessionId);
60
86
  }
@@ -120,16 +146,67 @@ export class TopicRegistry {
120
146
  return record ? !record.identitySent : true;
121
147
  }
122
148
 
123
- /** Whether a known session topic's applied title differs from `name`. */
149
+ /** Remember stable repo/branch identity independently of the displayed name. */
150
+ markIdentityKey(sessionId: string, identityKey: string): boolean {
151
+ const record = this.topics.get(sessionId);
152
+ if (!record || record.identityKey === identityKey) return false;
153
+ record.identityKey = identityKey;
154
+ return true;
155
+ }
156
+
157
+ /** Whether daemon identity reconciliation should apply `name`. */
124
158
  needsRename(sessionId: string, name: string): boolean {
125
159
  const record = this.topics.get(sessionId);
126
- return record !== undefined && record.name !== name;
160
+ return record !== undefined && record.nameOwner !== "user" && record.name !== name;
161
+ }
162
+
163
+ /** The user-owned name that must be preserved, when one exists. */
164
+ userOwnedName(sessionId: string): string | undefined {
165
+ const record = this.topics.get(sessionId);
166
+ return record?.nameOwner === "user" ? record.name : undefined;
167
+ }
168
+
169
+ /** A user-owned name whose Telegram reconciliation is still pending. */
170
+ userNameToReconcile(sessionId: string): string | undefined {
171
+ const record = this.topics.get(sessionId);
172
+ return record?.nameOwner === "user" && record.nameReconcilePending ? record.name : undefined;
173
+ }
174
+
175
+ /** Record an explicit Telegram-side user rename, rejecting stale update ids. */
176
+ markUserName(sessionId: string, name: string, updateId: number): "updated" | "duplicate" | "stale" {
177
+ const record = this.topics.get(sessionId);
178
+ if (!record) return "stale";
179
+ if (record.userNameUpdateId !== undefined && updateId < record.userNameUpdateId) return "stale";
180
+ if (record.userNameUpdateId === updateId) return "duplicate";
181
+ record.name = name;
182
+ record.nameOwner = "user";
183
+ record.nameReconcilePending = true;
184
+ record.userNameUpdateId = updateId;
185
+ return "updated";
186
+ }
187
+
188
+ /** Mark the matching preserved user name as reconciled with Telegram. */
189
+ markUserNameReconciled(sessionId: string, name: string): boolean {
190
+ const record = this.topics.get(sessionId);
191
+ if (record?.nameOwner !== "user" || record.name !== name || !record.nameReconcilePending) return false;
192
+ record.nameReconcilePending = false;
193
+ return true;
194
+ }
195
+
196
+ /** Restore retryable reconciliation after a failed pending-clear persistence. */
197
+ markUserNamePending(sessionId: string, name: string): boolean {
198
+ const record = this.topics.get(sessionId);
199
+ if (record?.nameOwner !== "user" || record.name !== name || record.nameReconcilePending) return false;
200
+ record.nameReconcilePending = true;
201
+ return true;
127
202
  }
128
203
 
129
- /** Commit a successfully-applied Telegram topic title. */
204
+ /** Commit a successfully-applied daemon topic title. */
130
205
  markNameApplied(sessionId: string, name: string): void {
131
206
  const record = this.topics.get(sessionId);
132
- if (record) record.name = name;
207
+ if (!record || record.nameOwner === "user") return;
208
+ record.name = name;
209
+ record.nameReconcilePending = false;
133
210
  }
134
211
 
135
212
  /** Remove a session topic record after Telegram deletes the topic. */
@@ -0,0 +1 @@
1
+ <system-reminder>IRC peers: {{roster}}</system-reminder>
@@ -34,6 +34,7 @@ You can reach other live agents via the `irc` tool. Your id is `{{ircSelfId}}`.
34
34
  {{ircPeers}}
35
35
 
36
36
  Use `irc` only when you need a quick answer from a peer; do not use it for long-form content. Address peers by id or use `"all"` to broadcast.
37
+ Use `irc` to DM `0-Main` on unexpected state instead of guessing; coordinate before changing shared files.
37
38
  {{/if}}
38
39
  [/COOP]
39
40
 
@@ -213,6 +213,7 @@ Delegate by default for multi-file changes, refactors, new features, tests, and
213
213
  {{#has tools "subagent"}}- Use `{{toolRefs.subagent}}` to list, inspect, await with `timeout_ms`, or cancel detached task subagents.{{/has}}
214
214
  - If an await timeout elapses, the subagent is still running; this is not a failure. Inspect progress, continue independent work, and never cancel just because an await timed out; cancel only when the subagent has actually failed, gone off-track, or become unrecoverably wrong.
215
215
  {{#has tools "irc"}}- If live messaging is enabled, coordinate with running subagents through `{{toolRefs.irc}}`; cancellation is not a message channel.{{/has}}
216
+ {{#has tools "irc"}}- When a subagent's scope shifts or needs steering, check in through `{{toolRefs.irc}}` rather than guessing its state.{{/has}}
216
217
  {{#has tools "job"}}- `{{toolRefs.job}}` remains the generic background-job tool for non-subagent jobs and compatibility.{{/has}}
217
218
  </detached-subagents>
218
219
  {{/has}}
@@ -14,7 +14,7 @@ You SHOULD reach for `irc` proactively when continuing alone is wasteful or wron
14
14
  - **Unexpected state.** You hit something the original task did not describe — a missing file, a config that contradicts the assignment, an API behaving differently than you were told, a tool failing in a way that suggests the spec is wrong. DM `0-Main` (or the spawning agent) for guidance instead of guessing.
15
15
  - **Blocked by another agent.** A peer holds the file/branch/resource you need, has already started the change you are about to make, or owns a decision you depend on. DM that peer (or broadcast to discover who) before duplicating or stepping on work.
16
16
  - **Decision points outside your scope.** A genuine fork in the road that the assignment did not pre-decide (e.g. which of two viable APIs to use, whether to refactor adjacent code). Ask the requester rather than picking unilaterally.
17
- - **Coordination opportunities.** You realize a peer's in-flight work would benefit from yours, or vice-versa.
17
+ - **Coordination opportunities.** Before editing a shared file or relying on another agent's in-flight API, message the relevant peer; proactively share state that affects their work.
18
18
 
19
19
  Do **not** use `irc` for: routine progress updates, things you can verify with a tool call, or questions whose answer is already in your assignment / repo / docs.
20
20
  </conditions>
@@ -16,6 +16,7 @@ export type AgentKind = "main" | "sub";
16
16
  export interface AgentRef {
17
17
  id: string;
18
18
  displayName: string;
19
+ rosterLabel?: string;
19
20
  kind: AgentKind;
20
21
  parentId?: string;
21
22
  status: AgentStatus;
@@ -35,6 +36,7 @@ type RegistryListener = (event: RegistryEvent) => void;
35
36
  export interface RegisterInput {
36
37
  id: string;
37
38
  displayName: string;
39
+ rosterLabel?: string;
38
40
  kind: AgentKind;
39
41
  parentId?: string;
40
42
  session: AgentSession | null;
@@ -65,6 +67,7 @@ export class AgentRegistry {
65
67
  const ref: AgentRef = {
66
68
  id: input.id,
67
69
  displayName: input.displayName,
70
+ rosterLabel: input.rosterLabel,
68
71
  kind: input.kind,
69
72
  parentId: input.parentId,
70
73
  status: input.status ?? "running",
package/src/sdk.ts CHANGED
@@ -94,6 +94,7 @@ import { createNotificationsExtension } from "./notifications";
94
94
  import {
95
95
  getNotificationConfig,
96
96
  type NotificationConfig,
97
+ SPAWN_PROVENANCE_ENV,
97
98
  shouldRegisterNotificationsExtension,
98
99
  } from "./notifications/config";
99
100
  import asyncResultTemplate from "./prompts/tools/async-result.md" with { type: "text" };
@@ -242,6 +243,28 @@ function buildMcpNotificationBatchMessage(entries: McpNotificationEntry[]): Agen
242
243
  };
243
244
  }
244
245
 
246
+ function sanitizeRosterLabel(value: string): string {
247
+ const normalized = value
248
+ .replace(/[\p{Cc}]/gu, " ")
249
+ .replace(/\s+/g, " ")
250
+ .trim();
251
+ return normalized.length > 48 ? `${normalized.slice(0, 47)}…` : normalized;
252
+ }
253
+
254
+ function humanizeAgentTaskId(id: string): string {
255
+ return id
256
+ .replace(/([a-z\d])([A-Z])/g, "$1 $2")
257
+ .replace(/[-_]+/g, " ")
258
+ .trim();
259
+ }
260
+
261
+ function resolveAgentRosterLabel(label: string | undefined, agentId: string, displayName: string): string {
262
+ return (
263
+ sanitizeRosterLabel(label ?? "") ||
264
+ sanitizeRosterLabel(humanizeAgentTaskId(agentId)) ||
265
+ sanitizeRosterLabel(displayName)
266
+ );
267
+ }
245
268
  // Types
246
269
  export interface CreateAgentSessionOptions {
247
270
  /** Working directory for project-local discovery. Default: getProjectDir() */
@@ -334,6 +357,8 @@ export interface CreateAgentSessionOptions {
334
357
  agentId?: string;
335
358
  /** Display name for the agent in IRC. Default: "main" or "sub". */
336
359
  agentDisplayName?: string;
360
+ /** Compact task label for hidden IRC roster reminders. */
361
+ agentRosterLabel?: string;
337
362
  /** Optional restricted bash command prefixes for read-only role agents. */
338
363
  bashAllowedPrefixes?: string[];
339
364
  /** Restriction policy paired with bashAllowedPrefixes. */
@@ -1271,7 +1296,19 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1271
1296
  const resolvedAgentId = options.agentId ?? options.parentTaskPrefix ?? MAIN_AGENT_ID;
1272
1297
  const isSubSession = taskDepth > 0 || Boolean(options.parentTaskPrefix) || Boolean(options.currentAgentType);
1273
1298
  const resolvedAgentDisplayName = options.agentDisplayName ?? (isSubSession ? "sub" : "main");
1299
+ const resolvedAgentRosterLabel = resolveAgentRosterLabel(
1300
+ options.agentRosterLabel,
1301
+ resolvedAgentId,
1302
+ resolvedAgentDisplayName,
1303
+ );
1274
1304
  const evalKernelOwnerId = `agent-session:${Snowflake.next()}`;
1305
+ let disposeLocalProtocolOverride: (() => void) | undefined;
1306
+ let localProtocolOverrideReleased = false;
1307
+ const releaseLocalProtocolOverride = (): void => {
1308
+ if (localProtocolOverrideReleased) return;
1309
+ localProtocolOverrideReleased = true;
1310
+ disposeLocalProtocolOverride?.();
1311
+ };
1275
1312
 
1276
1313
  try {
1277
1314
  const getActiveModelString = (): string | undefined => {
@@ -1401,7 +1438,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1401
1438
  if (asyncJobManager) AsyncJobManager.setInstance(asyncJobManager);
1402
1439
  }
1403
1440
  if (options.localProtocolOptions) {
1404
- LocalProtocolHandler.setOverride(options.localProtocolOptions);
1441
+ disposeLocalProtocolOverride = LocalProtocolHandler.installOverride(options.localProtocolOptions);
1405
1442
  }
1406
1443
  toolSession.getArtifactsDir = getArtifactsDir;
1407
1444
  toolSession.agentOutputManager = new AgentOutputManager(
@@ -1579,6 +1616,15 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1579
1616
  } catch {
1580
1617
  notificationCfg = undefined;
1581
1618
  }
1619
+ // Consume the GJC spawn-provenance marker: read it once, then remove it
1620
+ // from this process's env so it is never re-inherited by children this
1621
+ // session later spawns (marker is per-spawn, not dynastic — each GJC child
1622
+ // spawn site sets it explicitly). Suppression under `sessionScope=primary`
1623
+ // keeps auto-spawned children (team workers, harness RPC owners) silent
1624
+ // while explicit `/session_create` opt-in (GJC_NOTIFICATIONS=1) still wins.
1625
+ const spawnProvenance = process.env[SPAWN_PROVENANCE_ENV];
1626
+ const spawnedByGjc = typeof spawnProvenance === "string" && spawnProvenance.trim().length > 0;
1627
+ delete process.env[SPAWN_PROVENANCE_ENV];
1582
1628
  if (
1583
1629
  shouldRegisterNotificationsExtension({
1584
1630
  env: process.env,
@@ -1586,6 +1632,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1586
1632
  taskDepth,
1587
1633
  parentTaskPrefix: options.parentTaskPrefix,
1588
1634
  currentAgentType: options.currentAgentType,
1635
+ spawnedByGjc,
1589
1636
  })
1590
1637
  ) {
1591
1638
  inlineExtensions.push(api => createNotificationsExtension(api, { settings }));
@@ -2038,6 +2085,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2038
2085
  agentRegistry.register({
2039
2086
  id: resolvedAgentId,
2040
2087
  displayName: resolvedAgentDisplayName,
2088
+ rosterLabel: resolvedAgentRosterLabel,
2041
2089
  kind: isSubSession ? "sub" : "main",
2042
2090
  parentId: options.parentTaskPrefix,
2043
2091
  session: null,
@@ -2371,6 +2419,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2371
2419
  } finally {
2372
2420
  agentRegistry.unregister(resolvedAgentId);
2373
2421
  unsubscribeCredentialDisabled?.();
2422
+ releaseLocalProtocolOverride();
2374
2423
  }
2375
2424
  };
2376
2425
  }
@@ -2535,6 +2584,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2535
2584
  logger.warn("Failed to clean up createAgentSession resources after startup error", {
2536
2585
  error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError),
2537
2586
  });
2587
+ } finally {
2588
+ releaseLocalProtocolOverride();
2538
2589
  }
2539
2590
  throw error;
2540
2591
  }
@@ -1,5 +1,5 @@
1
1
  import type { Message, TextContent } from "@gajae-code/ai";
2
- import type { SessionContext } from "../session/session-manager";
2
+ import { type SessionContext, transferSessionMessageIdentity } from "../session/session-manager";
3
3
  import { compileSecretRegex } from "./regex";
4
4
 
5
5
  // ═══════════════════════════════════════════════════════════════════════════
@@ -281,7 +281,9 @@ export function deobfuscateSessionContext(
281
281
  ): SessionContext {
282
282
  if (!obfuscator?.hasSecrets()) return sessionContext;
283
283
  const messages = obfuscator.deobfuscateObject(sessionContext.messages);
284
- return messages === sessionContext.messages ? sessionContext : { ...sessionContext, messages };
284
+ if (messages === sessionContext.messages) return sessionContext;
285
+ transferSessionMessageIdentity(sessionContext.messages, messages);
286
+ return { ...sessionContext, messages };
285
287
  }
286
288
 
287
289
  // ═══════════════════════════════════════════════════════════════════════════