@sayknow-cli/coding-agent 0.3.12 → 0.3.15

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 (248) hide show
  1. package/dist/types/autoresearch/tools/init-experiment.d.ts +1 -1
  2. package/dist/types/autoresearch/tools/log-experiment.d.ts +2 -2
  3. package/dist/types/cli/classify-install-target.d.ts +0 -10
  4. package/dist/types/cli/daemon-cli.d.ts +2 -0
  5. package/dist/types/cli/initial-message.d.ts +1 -1
  6. package/dist/types/cli/malloc-env-guard.d.ts +65 -0
  7. package/dist/types/cli/malloc-env-guard.test.d.ts +1 -0
  8. package/dist/types/cli/notify-cli.d.ts +3 -1
  9. package/dist/types/cli/session-picker.d.ts +4 -3
  10. package/dist/types/cli/update-cli.d.ts +22 -3
  11. package/dist/types/cli.d.ts +5 -0
  12. package/dist/types/commands/daemon.d.ts +6 -2
  13. package/dist/types/commands/launch.d.ts +1 -0
  14. package/dist/types/commands/notify.d.ts +6 -0
  15. package/dist/types/commands/web-search.d.ts +1 -1
  16. package/dist/types/commit/agentic/tools/propose-changelog.d.ts +4 -4
  17. package/dist/types/commit/agentic/tools/propose-commit.d.ts +9 -9
  18. package/dist/types/commit/agentic/tools/schemas.d.ts +9 -9
  19. package/dist/types/commit/agentic/tools/split-commit.d.ts +9 -9
  20. package/dist/types/commit/analysis/conventional.d.ts +1 -1
  21. package/dist/types/commit/analysis/summary.d.ts +1 -1
  22. package/dist/types/commit/changelog/generate.d.ts +3 -3
  23. package/dist/types/commit/changelog/index.d.ts +2 -2
  24. package/dist/types/commit/map-reduce/map-phase.d.ts +1 -1
  25. package/dist/types/commit/map-reduce/reduce-phase.d.ts +1 -1
  26. package/dist/types/commit/shared-llm.d.ts +9 -9
  27. package/dist/types/config/keybindings.d.ts +134 -129
  28. package/dist/types/config/model-profile-activation.d.ts +1 -0
  29. package/dist/types/config/model-registry.d.ts +35 -35
  30. package/dist/types/config/models-config-schema.d.ts +111 -111
  31. package/dist/types/config/settings-schema.d.ts +56 -16
  32. package/dist/types/config/settings.d.ts +2 -0
  33. package/dist/types/coordinator/contract.d.ts +1 -1
  34. package/dist/types/coordinator-mcp/model-preset.d.ts +45 -0
  35. package/dist/types/coordinator-mcp/policy.d.ts +3 -0
  36. package/dist/types/coordinator-mcp/server.d.ts +79 -6
  37. package/dist/types/coordinator-mcp/session-reaper.d.ts +53 -0
  38. package/dist/types/daemon/control-types.d.ts +10 -0
  39. package/dist/types/daemon/operator-contract.d.ts +47 -0
  40. package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/models/catalog.d.ts +4 -0
  41. package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/shared/errors.d.ts +12 -12
  42. package/dist/types/discovery/helpers.d.ts +1 -1
  43. package/dist/types/discovery/substitute-plugin-root.d.ts +0 -4
  44. package/dist/types/edit/modes/patch.d.ts +2 -2
  45. package/dist/types/extensibility/extensions/types.d.ts +3 -1
  46. package/dist/types/goals/tools/goal-tool.d.ts +6 -6
  47. package/dist/types/i18n/messages/en.d.ts +8 -8
  48. package/dist/types/internal-urls/local-protocol.d.ts +13 -9
  49. package/dist/types/lsp/index.d.ts +9 -9
  50. package/dist/types/lsp/types.d.ts +9 -9
  51. package/dist/types/main.d.ts +66 -2
  52. package/dist/types/modes/acp/acp-agent.d.ts +19 -2
  53. package/dist/types/modes/acp/acp-mode.d.ts +12 -0
  54. package/dist/types/modes/components/assistant-message.d.ts +1 -1
  55. package/dist/types/modes/components/custom-editor.d.ts +2 -0
  56. package/dist/types/modes/components/hook-selector.d.ts +5 -0
  57. package/dist/types/modes/components/irc-sidebar.d.ts +32 -0
  58. package/dist/types/modes/components/pet-selector.d.ts +11 -0
  59. package/dist/types/modes/components/sayknow-pet-widget.d.ts +59 -0
  60. package/dist/types/modes/components/session-selector.d.ts +22 -9
  61. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  62. package/dist/types/modes/components/status-line/types.d.ts +1 -1
  63. package/dist/types/modes/components/status-line.d.ts +0 -10
  64. package/dist/types/modes/components/tool-execution.d.ts +1 -0
  65. package/dist/types/modes/components/user-message.d.ts +2 -1
  66. package/dist/types/modes/controllers/event-controller.d.ts +4 -0
  67. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  68. package/dist/types/modes/interactive-mode.d.ts +24 -2
  69. package/dist/types/modes/irc-observation-ledger.d.ts +22 -0
  70. package/dist/types/modes/print-mode.d.ts +3 -4
  71. package/dist/types/modes/rpc/rpc-client.d.ts +7 -1
  72. package/dist/types/modes/rpc/rpc-mode.d.ts +2 -1
  73. package/dist/types/modes/rpc/rpc-types.d.ts +17 -1
  74. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +8 -0
  75. package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +5 -0
  76. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +45 -0
  77. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +42 -2
  78. package/dist/types/modes/theme/defaults/index.d.ts +568 -568
  79. package/dist/types/modes/types.d.ts +28 -2
  80. package/dist/types/modes/utils/abort-message.d.ts +1 -1
  81. package/dist/types/modes/utils/context-usage.d.ts +4 -19
  82. package/dist/types/modes/utils/irc-message.d.ts +38 -0
  83. package/dist/types/modes/utils/ui-helpers.d.ts +13 -4
  84. package/dist/types/notifications/config.d.ts +34 -0
  85. package/dist/types/notifications/daemon-paths.d.ts +7 -0
  86. package/dist/types/notifications/index.d.ts +27 -0
  87. package/dist/types/notifications/lifecycle-control-runtime.d.ts +12 -4
  88. package/dist/types/notifications/notification-service.d.ts +144 -0
  89. package/dist/types/notifications/rate-limit-pool.d.ts +27 -9
  90. package/dist/types/notifications/telegram-daemon-control.d.ts +6 -0
  91. package/dist/types/notifications/telegram-daemon.d.ts +130 -3
  92. package/dist/types/notifications/telegram-reference.d.ts +19 -2
  93. package/dist/types/notifications/topic-registry.d.ts +24 -4
  94. package/dist/types/registry/agent-registry.d.ts +2 -0
  95. package/dist/types/rlm/index.d.ts +1 -1
  96. package/dist/types/sdk.d.ts +2 -0
  97. package/dist/types/secrets/obfuscator.d.ts +1 -1
  98. package/dist/types/session/agent-session.d.ts +40 -3
  99. package/dist/types/session/cache-economics.d.ts +79 -0
  100. package/dist/types/session/context-estimation.d.ts +31 -0
  101. package/dist/types/session/provider-safety-stop.d.ts +5 -0
  102. package/dist/types/session/session-manager.d.ts +124 -4
  103. package/dist/types/session/session-storage.d.ts +155 -13
  104. package/dist/types/setup/credential-auto-import.d.ts +2 -2
  105. package/dist/types/skc-runtime/session-state-sidecar.d.ts +34 -3
  106. package/dist/types/skc-runtime/state-schema.d.ts +19 -19
  107. package/dist/types/skc-runtime/tmux-common.d.ts +6 -0
  108. package/dist/types/skc-runtime/tmux-owner-isolation-cli.d.ts +5 -0
  109. package/dist/types/skc-runtime/tmux-owner-isolation.d.ts +309 -0
  110. package/dist/types/skc-runtime/tmux-sessions.d.ts +49 -17
  111. package/dist/types/task/agents.d.ts +2 -2
  112. package/dist/types/task/executor.d.ts +3 -0
  113. package/dist/types/task/receipt.d.ts +3 -1
  114. package/dist/types/task/types.d.ts +35 -27
  115. package/dist/types/tools/ask-answer-registry.d.ts +5 -0
  116. package/dist/types/tools/bash.d.ts +1 -1
  117. package/dist/types/tools/browser/tab-supervisor.d.ts +6 -0
  118. package/dist/types/tools/browser.d.ts +28 -28
  119. package/dist/types/tools/computer.d.ts +20 -20
  120. package/dist/types/tools/cron.d.ts +2 -2
  121. package/dist/types/tools/debug.d.ts +40 -40
  122. package/dist/types/tools/gh.d.ts +10 -10
  123. package/dist/types/tools/image-gen.d.ts +2 -2
  124. package/dist/types/tools/index.d.ts +58 -6
  125. package/dist/types/tools/monitor.d.ts +6 -6
  126. package/dist/types/tools/skill-discovery.d.ts +4 -4
  127. package/dist/types/tools/subagent.d.ts +12 -12
  128. package/dist/types/tools/telegram-send.d.ts +7 -0
  129. package/dist/types/tools/todo-write.d.ts +8 -8
  130. package/dist/types/utils/external-editor.d.ts +2 -0
  131. package/dist/types/web/insane/bridge.d.ts +9 -9
  132. package/dist/types/web/search/index.d.ts +4 -4
  133. package/package.json +8 -8
  134. package/scripts/g004-tmux-smoke.ts +196 -36
  135. package/src/cli/daemon-cli.ts +19 -29
  136. package/src/cli/malloc-env-guard.test.ts +104 -0
  137. package/src/cli/malloc-env-guard.ts +149 -0
  138. package/src/cli/notify-cli.ts +78 -14
  139. package/src/cli/session-picker.ts +23 -40
  140. package/src/cli/update-cli.ts +56 -37
  141. package/src/cli.ts +51 -10
  142. package/src/commands/daemon.ts +19 -5
  143. package/src/commands/harness.ts +515 -47
  144. package/src/commands/launch.ts +11 -3
  145. package/src/commands/notify.ts +5 -1
  146. package/src/commands/session.ts +6 -1
  147. package/src/config/keybindings.ts +5 -0
  148. package/src/config/model-profile-activation.ts +1 -1
  149. package/src/config/model-profiles.ts +14 -14
  150. package/src/config/settings-schema.ts +41 -1
  151. package/src/config/settings.ts +96 -45
  152. package/src/coordinator/contract.ts +1 -0
  153. package/src/coordinator-mcp/model-preset.ts +100 -0
  154. package/src/coordinator-mcp/policy.ts +30 -0
  155. package/src/coordinator-mcp/server.ts +2958 -430
  156. package/src/coordinator-mcp/session-reaper.ts +129 -0
  157. package/src/daemon/control-types.ts +11 -0
  158. package/src/daemon/operator-contract.ts +121 -0
  159. package/src/defaults/skc/extensions/grok-cli-vendor/src/models/catalog.ts +57 -17
  160. package/src/defaults/skc/extensions/grok-cli-vendor/src/payload/sanitize.ts +15 -2
  161. package/src/defaults/skc/extensions/grok-cli-vendor/src/provider/register.ts +5 -1
  162. package/src/extensibility/extensions/runner.ts +1 -1
  163. package/src/extensibility/extensions/types.ts +3 -1
  164. package/src/internal-urls/docs-index.generated.ts +10 -9
  165. package/src/internal-urls/local-protocol.ts +29 -10
  166. package/src/main.ts +301 -80
  167. package/src/modes/acp/acp-agent.ts +730 -100
  168. package/src/modes/acp/acp-mode.ts +29 -2
  169. package/src/modes/components/assistant-message.ts +32 -3
  170. package/src/modes/components/bash-execution.ts +43 -13
  171. package/src/modes/components/custom-editor.ts +5 -0
  172. package/src/modes/components/footer.ts +3 -3
  173. package/src/modes/components/hook-selector.ts +12 -0
  174. package/src/modes/components/irc-sidebar.ts +206 -0
  175. package/src/modes/components/pet-selector.ts +55 -0
  176. package/src/modes/components/sayknow-pet-widget.ts +360 -0
  177. package/src/modes/components/session-selector.ts +222 -221
  178. package/src/modes/components/settings-selector.ts +10 -0
  179. package/src/modes/components/status-line/segments.ts +14 -7
  180. package/src/modes/components/status-line/types.ts +1 -1
  181. package/src/modes/components/status-line.ts +4 -171
  182. package/src/modes/components/tool-execution.ts +47 -6
  183. package/src/modes/components/user-message.ts +26 -9
  184. package/src/modes/controllers/command-controller.ts +28 -2
  185. package/src/modes/controllers/event-controller.ts +159 -36
  186. package/src/modes/controllers/extension-ui-controller.ts +37 -18
  187. package/src/modes/controllers/input-controller.ts +8 -2
  188. package/src/modes/controllers/selector-controller.ts +62 -15
  189. package/src/modes/interactive-mode.ts +199 -7
  190. package/src/modes/irc-observation-ledger.ts +127 -0
  191. package/src/modes/print-mode.ts +228 -92
  192. package/src/modes/rpc/rpc-client.ts +58 -1
  193. package/src/modes/rpc/rpc-mode.ts +286 -85
  194. package/src/modes/rpc/rpc-types.ts +15 -1
  195. package/src/modes/shared/agent-wire/command-dispatch.ts +33 -1
  196. package/src/modes/shared/agent-wire/command-validation.ts +10 -0
  197. package/src/modes/shared/agent-wire/deep-interview-gate.ts +45 -2
  198. package/src/modes/shared/agent-wire/scopes.ts +1 -0
  199. package/src/modes/shared/agent-wire/unattended-session.ts +288 -1
  200. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +130 -20
  201. package/src/modes/types.ts +34 -2
  202. package/src/modes/utils/context-usage.ts +56 -92
  203. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  204. package/src/modes/utils/irc-message.ts +138 -0
  205. package/src/modes/utils/ui-helpers.ts +216 -50
  206. package/src/notifications/config.ts +44 -0
  207. package/src/notifications/daemon-paths.ts +8 -0
  208. package/src/notifications/index.ts +535 -75
  209. package/src/notifications/lifecycle-control-runtime.ts +930 -77
  210. package/src/notifications/notification-service.ts +710 -0
  211. package/src/notifications/rate-limit-pool.ts +46 -12
  212. package/src/notifications/telegram-daemon-control.ts +27 -2
  213. package/src/notifications/telegram-daemon.ts +635 -94
  214. package/src/notifications/telegram-reference.ts +73 -13
  215. package/src/notifications/topic-registry.ts +86 -9
  216. package/src/prompts/system/irc-peer-roster.md +1 -0
  217. package/src/prompts/system/subagent-system-prompt.md +1 -0
  218. package/src/prompts/system/system-prompt.md +1 -0
  219. package/src/prompts/tools/irc.md +1 -1
  220. package/src/registry/agent-registry.ts +3 -0
  221. package/src/sdk.ts +52 -1
  222. package/src/secrets/obfuscator.ts +4 -2
  223. package/src/session/agent-session.ts +670 -217
  224. package/src/session/cache-economics.ts +275 -0
  225. package/src/session/context-estimation.ts +91 -0
  226. package/src/session/provider-safety-stop.ts +9 -0
  227. package/src/session/session-dump-format.ts +6 -0
  228. package/src/session/session-manager.ts +809 -39
  229. package/src/session/session-storage.ts +582 -38
  230. package/src/skc-runtime/session-state-sidecar.ts +756 -135
  231. package/src/skc-runtime/tmux-common.ts +26 -7
  232. package/src/skc-runtime/tmux-gc.ts +52 -9
  233. package/src/skc-runtime/tmux-owner-isolation-cli.ts +185 -0
  234. package/src/skc-runtime/tmux-owner-isolation.ts +2060 -0
  235. package/src/skc-runtime/tmux-sessions.ts +871 -68
  236. package/src/slash-commands/builtin-registry.ts +153 -9
  237. package/src/slash-commands/helpers/context-report.ts +26 -4
  238. package/src/task/executor.ts +12 -3
  239. package/src/task/index.ts +8 -0
  240. package/src/task/receipt.ts +7 -2
  241. package/src/task/types.ts +26 -0
  242. package/src/tools/ask-answer-registry.ts +25 -0
  243. package/src/tools/browser/tab-supervisor.ts +63 -6
  244. package/src/tools/browser.ts +8 -3
  245. package/src/tools/index.ts +81 -6
  246. package/src/tools/sqlite-reader.ts +8 -4
  247. package/src/tui/output-block.ts +18 -6
  248. 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
 
@@ -210,6 +210,7 @@ Delegate by default for multi-file changes, refactors, new features, tests, and
210
210
  {{#has tools "subagent"}}- Use `{{toolRefs.subagent}}` to list, inspect, await with `timeout_ms`, or cancel detached task subagents.{{/has}}
211
211
  - 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.
212
212
  {{#has tools "irc"}}- If live messaging is enabled, coordinate with running subagents through `{{toolRefs.irc}}`; cancellation is not a message channel.{{/has}}
213
+ {{#has tools "irc"}}- When a subagent's scope shifts or needs steering, check in through `{{toolRefs.irc}}` rather than guessing its state.{{/has}}
213
214
  {{#has tools "job"}}- `{{toolRefs.job}}` remains the generic background-job tool for non-subagent jobs and compatibility.{{/has}}
214
215
  </detached-subagents>
215
216
  {{/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
@@ -96,6 +96,7 @@ import { createNotificationsExtension } from "./notifications";
96
96
  import {
97
97
  getNotificationConfig,
98
98
  type NotificationConfig,
99
+ SPAWN_PROVENANCE_ENV,
99
100
  shouldRegisterNotificationsExtension,
100
101
  } from "./notifications/config";
101
102
  import asyncResultTemplate from "./prompts/tools/async-result.md" with { type: "text" };
@@ -244,6 +245,28 @@ function buildMcpNotificationBatchMessage(entries: McpNotificationEntry[]): Agen
244
245
  };
245
246
  }
246
247
 
248
+ function sanitizeRosterLabel(value: string): string {
249
+ const normalized = value
250
+ .replace(/[\p{Cc}]/gu, " ")
251
+ .replace(/\s+/g, " ")
252
+ .trim();
253
+ return normalized.length > 48 ? `${normalized.slice(0, 47)}…` : normalized;
254
+ }
255
+
256
+ function humanizeAgentTaskId(id: string): string {
257
+ return id
258
+ .replace(/([a-z\d])([A-Z])/g, "$1 $2")
259
+ .replace(/[-_]+/g, " ")
260
+ .trim();
261
+ }
262
+
263
+ function resolveAgentRosterLabel(label: string | undefined, agentId: string, displayName: string): string {
264
+ return (
265
+ sanitizeRosterLabel(label ?? "") ||
266
+ sanitizeRosterLabel(humanizeAgentTaskId(agentId)) ||
267
+ sanitizeRosterLabel(displayName)
268
+ );
269
+ }
247
270
  // Types
248
271
  export interface CreateAgentSessionOptions {
249
272
  /** Working directory for project-local discovery. Default: getProjectDir() */
@@ -336,6 +359,8 @@ export interface CreateAgentSessionOptions {
336
359
  agentId?: string;
337
360
  /** Display name for the agent in IRC. Default: "main" or "sub". */
338
361
  agentDisplayName?: string;
362
+ /** Compact task label for hidden IRC roster reminders. */
363
+ agentRosterLabel?: string;
339
364
  /** Optional restricted bash command prefixes for read-only role agents. */
340
365
  bashAllowedPrefixes?: string[];
341
366
  /** Restriction policy paired with bashAllowedPrefixes. */
@@ -1273,7 +1298,19 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1273
1298
  const resolvedAgentId = options.agentId ?? options.parentTaskPrefix ?? MAIN_AGENT_ID;
1274
1299
  const isSubSession = taskDepth > 0 || Boolean(options.parentTaskPrefix) || Boolean(options.currentAgentType);
1275
1300
  const resolvedAgentDisplayName = options.agentDisplayName ?? (isSubSession ? "sub" : "main");
1301
+ const resolvedAgentRosterLabel = resolveAgentRosterLabel(
1302
+ options.agentRosterLabel,
1303
+ resolvedAgentId,
1304
+ resolvedAgentDisplayName,
1305
+ );
1276
1306
  const evalKernelOwnerId = `agent-session:${Snowflake.next()}`;
1307
+ let disposeLocalProtocolOverride: (() => void) | undefined;
1308
+ let localProtocolOverrideReleased = false;
1309
+ const releaseLocalProtocolOverride = (): void => {
1310
+ if (localProtocolOverrideReleased) return;
1311
+ localProtocolOverrideReleased = true;
1312
+ disposeLocalProtocolOverride?.();
1313
+ };
1277
1314
 
1278
1315
  try {
1279
1316
  const getActiveModelString = (): string | undefined => {
@@ -1403,7 +1440,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1403
1440
  if (asyncJobManager) AsyncJobManager.setInstance(asyncJobManager);
1404
1441
  }
1405
1442
  if (options.localProtocolOptions) {
1406
- LocalProtocolHandler.setOverride(options.localProtocolOptions);
1443
+ disposeLocalProtocolOverride = LocalProtocolHandler.installOverride(options.localProtocolOptions);
1407
1444
  }
1408
1445
  toolSession.getArtifactsDir = getArtifactsDir;
1409
1446
  toolSession.agentOutputManager = new AgentOutputManager(
@@ -1581,6 +1618,15 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1581
1618
  } catch {
1582
1619
  notificationCfg = undefined;
1583
1620
  }
1621
+ // Consume the SKC spawn-provenance marker: read it once, then remove it
1622
+ // from this process's env so it is never re-inherited by children this
1623
+ // session later spawns (marker is per-spawn, not dynastic — each SKC child
1624
+ // spawn site sets it explicitly). Suppression under `sessionScope=primary`
1625
+ // keeps auto-spawned children (team workers, harness RPC owners) silent
1626
+ // while explicit `/session_create` opt-in (SKC_NOTIFICATIONS=1) still wins.
1627
+ const spawnProvenance = process.env[SPAWN_PROVENANCE_ENV];
1628
+ const spawnedBySkc = typeof spawnProvenance === "string" && spawnProvenance.trim().length > 0;
1629
+ delete process.env[SPAWN_PROVENANCE_ENV];
1584
1630
  if (
1585
1631
  shouldRegisterNotificationsExtension({
1586
1632
  env: process.env,
@@ -1588,6 +1634,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1588
1634
  taskDepth,
1589
1635
  parentTaskPrefix: options.parentTaskPrefix,
1590
1636
  currentAgentType: options.currentAgentType,
1637
+ spawnedBySkc,
1591
1638
  })
1592
1639
  ) {
1593
1640
  inlineExtensions.push(api => createNotificationsExtension(api, { settings }));
@@ -2061,6 +2108,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2061
2108
  agentRegistry.register({
2062
2109
  id: resolvedAgentId,
2063
2110
  displayName: resolvedAgentDisplayName,
2111
+ rosterLabel: resolvedAgentRosterLabel,
2064
2112
  kind: isSubSession ? "sub" : "main",
2065
2113
  parentId: options.parentTaskPrefix,
2066
2114
  session: null,
@@ -2385,6 +2433,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2385
2433
  } finally {
2386
2434
  agentRegistry.unregister(resolvedAgentId);
2387
2435
  unsubscribeCredentialDisabled?.();
2436
+ releaseLocalProtocolOverride();
2388
2437
  }
2389
2438
  };
2390
2439
  }
@@ -2549,6 +2598,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2549
2598
  logger.warn("Failed to clean up createAgentSession resources after startup error", {
2550
2599
  error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError),
2551
2600
  });
2601
+ } finally {
2602
+ releaseLocalProtocolOverride();
2552
2603
  }
2553
2604
  throw error;
2554
2605
  }
@@ -1,5 +1,5 @@
1
1
  import type { Message, TextContent } from "@sayknow-cli/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
  // ═══════════════════════════════════════════════════════════════════════════