@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
@@ -1,6 +1,6 @@
1
1
  import type { AgentMessage } from "@sayknow-cli/agent-core";
2
2
  import type { AssistantMessage, ImageContent, Message } from "@sayknow-cli/ai";
3
- import { type Component, Spacer, Text, TruncatedText } from "@sayknow-cli/tui";
3
+ import { type Component, Spacer, Text, TruncatedText, type TUI, truncateToWidth } from "@sayknow-cli/tui";
4
4
  import { settings } from "../../config/settings";
5
5
  import { resolveSubskillActivationForSkillInvocation } from "../../extensibility/skc-plugins";
6
6
  import { buildSkillPromptMessage, parseSkillInvocations } from "../../extensibility/skills";
@@ -21,17 +21,69 @@ import { SkillMessageComponent } from "../../modes/components/skill-message";
21
21
  import { ToolExecutionComponent } from "../../modes/components/tool-execution";
22
22
  import { UserMessageComponent } from "../../modes/components/user-message";
23
23
  import { theme } from "../../modes/theme/theme";
24
- import type { CompactionQueuedMessage, InteractiveModeContext } from "../../modes/types";
24
+ import type {
25
+ CompactionQueuedMessage,
26
+ InteractiveModeContext,
27
+ IrcArrivalSnapshot,
28
+ TranscriptRebuildPolicy,
29
+ } from "../../modes/types";
25
30
  import {
26
31
  type CustomMessage,
27
32
  isSilentAbort,
28
33
  SKILL_PROMPT_MESSAGE_TYPE,
29
34
  type SkillPromptDetails,
30
35
  } from "../../session/messages";
31
- import type { SessionContext } from "../../session/session-manager";
36
+ import {
37
+ associateSessionMessageViewportAnchorId,
38
+ getSessionMessageEntryId,
39
+ getSessionMessageViewportAnchorId,
40
+ type SessionContext,
41
+ } from "../../session/session-manager";
32
42
  import { formatBytes, formatDuration } from "../../tools/render-utils";
33
43
  import { buildAbortDisplayMessage } from "./abort-message";
44
+ import {
45
+ formatIrcMessageBlock,
46
+ isIrcCustomType,
47
+ type ParsedIrcMessage,
48
+ parseIrcMessage,
49
+ projectIrcText,
50
+ } from "./irc-message";
51
+
52
+ export type { TranscriptRebuildPolicy } from "../../modes/types";
53
+
54
+ const IRC_INLINE_MAX_RENDER_ROWS = 2_048;
55
+ const IRC_INLINE_MAX_SOURCE_UTF8_BYTES = 64 * 1_024;
56
+ const IRC_INLINE_ELISION = " … message elided …";
57
+
58
+ class BoundedIrcTextComponent implements Component {
59
+ #text: Text;
60
+ #sourceTruncated: boolean;
61
+
62
+ constructor(text: string, sourceTruncated: boolean) {
63
+ this.#text = new Text(text, 0, 0);
64
+ this.#sourceTruncated = sourceTruncated;
65
+ }
66
+
67
+ render(width: number): string[] {
68
+ const rendered = this.#text.render(width);
69
+ if (!this.#sourceTruncated && rendered.length <= IRC_INLINE_MAX_RENDER_ROWS) return rendered;
70
+ const lines = rendered.slice(0, IRC_INLINE_MAX_RENDER_ROWS);
71
+ const marker = truncateToWidth(theme.fg("dim", IRC_INLINE_ELISION), width);
72
+ if (lines.length === 0) return [marker];
73
+ if (this.#sourceTruncated && rendered.length < IRC_INLINE_MAX_RENDER_ROWS) lines.push(marker);
74
+ else lines[lines.length - 1] = marker;
75
+ return lines;
76
+ }
34
77
 
78
+ invalidate(): void {
79
+ this.#text.invalidate();
80
+ }
81
+ }
82
+
83
+ export function prepareTranscriptRebuild(ui: TUI, policy: TranscriptRebuildPolicy): void {
84
+ if (policy === "replace-identity") ui.resetViewportAnchorIntent();
85
+ else ui.prepareViewportAnchorForTranscriptRebuild();
86
+ }
35
87
  type TextBlock = { type: "text"; text: string };
36
88
  interface RenderInitialMessagesOptions {
37
89
  preserveExistingChat?: boolean;
@@ -119,8 +171,18 @@ function getChatChildTime(component: Component): number {
119
171
  return chatChildAddedAt.get(component) ?? Date.now();
120
172
  }
121
173
 
174
+ function stableSemanticIdPart(value: string): string {
175
+ let hash = 0xcbf29ce484222325n;
176
+ for (const char of value) {
177
+ hash ^= BigInt(char.codePointAt(0)!);
178
+ hash = BigInt.asUintN(64, hash * 0x100000001b3n);
179
+ }
180
+ return hash.toString(36);
181
+ }
182
+
122
183
  export function addChatChild(ctx: InteractiveModeContext, component: Component): void {
123
184
  ctx.chatContainer.addChild(component);
185
+
124
186
  chatChildAddedAt.set(component, Date.now());
125
187
  trimChatChildren(ctx);
126
188
  }
@@ -187,8 +249,114 @@ export function trimChatChildren(ctx: InteractiveModeContext): void {
187
249
  }
188
250
 
189
251
  export class UiHelpers {
252
+ #renderedIrcInlineComponents = new Map<string, readonly Component[]>();
253
+ #viewportAnchorOccurrences = new WeakMap<object, { base: string; epoch: number; id: string }>();
254
+ #nextViewportAnchorOccurrence = new Map<string, number>();
255
+ #viewportAnchorOccurrenceEpoch = 0;
256
+ #ircSidebarHintShown = false;
257
+
190
258
  constructor(private ctx: InteractiveModeContext) {}
191
259
 
260
+ #resetViewportAnchorOccurrencePass(): void {
261
+ this.#viewportAnchorOccurrenceEpoch += 1;
262
+ this.#nextViewportAnchorOccurrence.clear();
263
+ }
264
+
265
+ #viewportAnchorOccurrenceId(message: object, base: string): string {
266
+ const existing = this.#viewportAnchorOccurrences.get(message);
267
+ if (existing?.base === base && existing.epoch === this.#viewportAnchorOccurrenceEpoch) return existing.id;
268
+ const occurrence = this.#nextViewportAnchorOccurrence.get(base) ?? 0;
269
+ this.#nextViewportAnchorOccurrence.set(base, occurrence + 1);
270
+ const id = `${base}:occurrence:${occurrence}`;
271
+ this.#viewportAnchorOccurrences.set(message, {
272
+ base,
273
+ epoch: this.#viewportAnchorOccurrenceEpoch,
274
+ id,
275
+ });
276
+ return id;
277
+ }
278
+
279
+ assistantViewportAnchorId(message: AssistantMessage): string {
280
+ const semanticId = getSessionMessageViewportAnchorId(message);
281
+ if (semanticId) return semanticId;
282
+ const entryId = getSessionMessageEntryId(message);
283
+ if (entryId) return `assistant:entry:${entryId}`;
284
+ const base = `assistant:${message.api}:${message.provider}:${message.model}:${message.timestamp}`;
285
+ const id = this.#viewportAnchorOccurrenceId(message, base);
286
+ associateSessionMessageViewportAnchorId(message, id);
287
+ return id;
288
+ }
289
+
290
+ #userViewportAnchorId(message: Extract<Message, { role: "user" }>): string {
291
+ const semanticId = getSessionMessageViewportAnchorId(message);
292
+ if (semanticId) return semanticId;
293
+ const entryId = getSessionMessageEntryId(message);
294
+ if (entryId) return `user:entry:${entryId}`;
295
+ const base = `user:local:${message.timestamp}:${stableSemanticIdPart(JSON.stringify(message.content))}`;
296
+ const id = this.#viewportAnchorOccurrenceId(message, base);
297
+ associateSessionMessageViewportAnchorId(message, id);
298
+ return id;
299
+ }
300
+
301
+ getRenderedIrcInlineComponents(): Map<string, readonly Component[]> {
302
+ return this.#renderedIrcInlineComponents;
303
+ }
304
+
305
+ removeRenderedIrcInlineComponents(observationId: string): readonly Component[] | undefined {
306
+ const components = this.#renderedIrcInlineComponents.get(observationId);
307
+ this.#renderedIrcInlineComponents.delete(observationId);
308
+ return components;
309
+ }
310
+
311
+ resetRenderedIrcInlineComponents(): readonly (readonly Component[])[] {
312
+ const components = [...this.#renderedIrcInlineComponents.values()];
313
+ this.#renderedIrcInlineComponents.clear();
314
+ return components;
315
+ }
316
+
317
+ #addIrcObservationToChat(message: ParsedIrcMessage, sidebarHint?: string): Component[] {
318
+ const bodyProjection = projectIrcText(message.text, IRC_INLINE_MAX_SOURCE_UTF8_BYTES);
319
+ const block = formatIrcMessageBlock({ ...message, text: bodyProjection.text });
320
+ const components: Component[] = [];
321
+ const header = `${theme.fg("accent", `[IRC] ${block.sender} → ${block.recipient} · ${block.time}`)}${sidebarHint ? theme.fg("dim", sidebarHint) : ""}`;
322
+ const headerComponent = new Text(header, 1, 0);
323
+ addChatChild(this.ctx, headerComponent);
324
+ components.push(headerComponent);
325
+ if (block.bodyLines.length > 0 || bodyProjection.truncated) {
326
+ const bodyComponent = new BoundedIrcTextComponent(
327
+ theme.fg("muted", ` ${block.bodyLines.join("\n ")}`),
328
+ bodyProjection.truncated,
329
+ );
330
+ addChatChild(this.ctx, bodyComponent);
331
+ components.push(bodyComponent);
332
+ }
333
+ return components;
334
+ }
335
+
336
+ addLiveIrcObservationToChat(message: ParsedIrcMessage, arrival: IrcArrivalSnapshot): Component[] {
337
+ // Requested-open panels that merely yielded at narrow widths must not
338
+ // advertise the toggle key: pressing it would close the pending request.
339
+ const showSidebarHint =
340
+ !arrival.panelVisible &&
341
+ !arrival.panelRequestedVisible &&
342
+ arrival.sidebarAvailable &&
343
+ Boolean(arrival.resolvedToggleKey) &&
344
+ !this.#ircSidebarHintShown;
345
+ if (showSidebarHint) this.#ircSidebarHintShown = true;
346
+ return this.#addIrcObservationToChat(
347
+ message,
348
+ showSidebarHint ? ` · ${arrival.resolvedToggleKey} opens sidebar` : undefined,
349
+ );
350
+ }
351
+
352
+ addRebuiltIrcObservationToChat(message: ParsedIrcMessage): Component[] {
353
+ return this.#addIrcObservationToChat(message);
354
+ }
355
+
356
+ resetIrcSidebarHint(): void {
357
+ this.#ircSidebarHintShown = false;
358
+ }
359
+
192
360
  /** Extract text content from a user message */
193
361
  getUserMessageText(message: Message): string {
194
362
  if (message.role !== "user") return "";
@@ -306,50 +474,9 @@ export class UiHelpers {
306
474
  addChatChild(this.ctx, component);
307
475
  break;
308
476
  }
309
- if (
310
- message.customType === "irc:incoming" ||
311
- message.customType === "irc:autoreply" ||
312
- message.customType === "irc:relay"
313
- ) {
314
- const details = (
315
- message as CustomMessage<{
316
- from?: string;
317
- to?: string;
318
- message?: string;
319
- reply?: string;
320
- body?: string;
321
- kind?: "message" | "reply";
322
- }>
323
- ).details;
324
- let arrow: string;
325
- let body: string;
326
- if (message.customType === "irc:incoming") {
327
- const peer = details?.from ?? "?";
328
- body = details?.message ?? "";
329
- arrow = `⇦ ${peer}`;
330
- } else if (message.customType === "irc:autoreply") {
331
- const peer = details?.to ?? "?";
332
- body = details?.reply ?? "";
333
- arrow = `⇨ ${peer}`;
334
- } else {
335
- const from = details?.from ?? "?";
336
- const to = details?.to ?? "?";
337
- body = details?.body ?? "";
338
- arrow = `${from} ⇨ ${to}`;
339
- }
340
- const components: Component[] = [];
341
- const header = `${theme.fg("accent", `[IRC] ${arrow}`)}`;
342
- const headerComponent = new Text(header, 1, 0);
343
- addChatChild(this.ctx, headerComponent);
344
- components.push(headerComponent);
345
- if (body) {
346
- for (const line of body.split("\n")) {
347
- const lineComponent = new Text(theme.fg("muted", ` ${line}`), 0, 0);
348
- addChatChild(this.ctx, lineComponent);
349
- components.push(lineComponent);
350
- }
351
- }
352
- return components;
477
+ if (message.role === "custom" && isIrcCustomType(message.customType)) {
478
+ const parsed = parseIrcMessage(message);
479
+ if (parsed) return this.addRebuiltIrcObservationToChat(parsed);
353
480
  }
354
481
  if (message.customType === "subagent:steer" || message.customType === "subagent:steer:relay") {
355
482
  const details = (
@@ -423,7 +550,11 @@ export class UiHelpers {
423
550
  const textContent = this.ctx.getUserMessageText(message);
424
551
  if (textContent) {
425
552
  const isSynthetic = message.role === "developer" ? true : (message.synthetic ?? false);
426
- const userComponent = new UserMessageComponent(textContent, isSynthetic);
553
+ const userComponent = new UserMessageComponent(
554
+ textContent,
555
+ isSynthetic,
556
+ message.role === "user" && !isSynthetic ? this.#userViewportAnchorId(message) : undefined,
557
+ );
427
558
  addChatChild(this.ctx, userComponent);
428
559
  if (options?.populateHistory && message.role === "user" && !isSynthetic) {
429
560
  this.ctx.editor.addToHistory(textContent);
@@ -432,8 +563,11 @@ export class UiHelpers {
432
563
  break;
433
564
  }
434
565
  case "assistant": {
435
- const assistantComponent = new AssistantMessageComponent(message, this.ctx.hideThinkingBlock, () =>
436
- this.ctx.ui.requestRender(),
566
+ const assistantComponent = new AssistantMessageComponent(
567
+ message,
568
+ this.ctx.hideThinkingBlock,
569
+ () => this.ctx.ui.requestRender(),
570
+ this.assistantViewportAnchorId(message),
437
571
  );
438
572
  addChatChild(this.ctx, assistantComponent);
439
573
  break;
@@ -471,12 +605,35 @@ export class UiHelpers {
471
605
  const readToolCallArgs = new Map<string, Record<string, unknown>>();
472
606
  const readToolCallAssistantComponents = new Map<string, AssistantMessageComponent>();
473
607
  const deferredMessages: AgentMessage[] = [];
608
+ const persistedIrcObservationIds = new Set<string>();
609
+ const now = Date.now();
610
+ this.#resetViewportAnchorOccurrencePass();
611
+ this.#renderedIrcInlineComponents.clear();
474
612
  for (const message of sessionContext.messages) {
475
613
  // Defer compaction summaries so they render at the bottom (visible after scroll)
476
614
  if (message.role === "compactionSummary") {
477
615
  deferredMessages.push(message);
478
616
  continue;
479
617
  }
618
+ if (message.role === "custom" && isIrcCustomType(message.customType)) {
619
+ const parsed = parseIrcMessage(message);
620
+ if (parsed) {
621
+ persistedIrcObservationIds.add(parsed.observationId);
622
+ const record = this.ctx.ircLedger.getRecord(parsed.observationId);
623
+ if (
624
+ record &&
625
+ (record.mode === "persistent" || now < record.expiresAt!) &&
626
+ !this.#renderedIrcInlineComponents.has(record.observationId)
627
+ ) {
628
+ this.#renderedIrcInlineComponents.set(
629
+ record.observationId,
630
+ this.addRebuiltIrcObservationToChat(record),
631
+ );
632
+ }
633
+ }
634
+ continue;
635
+ }
636
+
480
637
  // Assistant messages need special handling for tool calls
481
638
  if (message.role === "assistant") {
482
639
  this.ctx.addMessageToChat(message);
@@ -627,6 +784,15 @@ export class UiHelpers {
627
784
  for (const message of deferredMessages) {
628
785
  this.ctx.addMessageToChat(message, options);
629
786
  }
787
+ for (const record of this.ctx.ircLedger.getInlineProjection(now)) {
788
+ if (
789
+ persistedIrcObservationIds.has(record.observationId) ||
790
+ this.#renderedIrcInlineComponents.has(record.observationId)
791
+ ) {
792
+ continue;
793
+ }
794
+ this.#renderedIrcInlineComponents.set(record.observationId, this.addRebuiltIrcObservationToChat(record));
795
+ }
630
796
 
631
797
  this.ctx.pendingTools.clear();
632
798
  this.ctx.ui.requestRender();
@@ -1,6 +1,17 @@
1
1
  import * as crypto from "node:crypto";
2
2
  import type { Settings } from "../config/settings";
3
3
 
4
+ /**
5
+ * Env marker set by SKC's own programmatic separate-process child spawn sites
6
+ * (team workers, harness RPC owners) and carrying the spawning session id.
7
+ *
8
+ * Presence — not the value — marks a session as SKC-spawned. It is consumed
9
+ * (read once, then deleted from the child's own env) at startup so it is
10
+ * per-spawn rather than dynastic: a grandchild is marked only if its own spawn
11
+ * site marks it, never by inheriting a marked ancestor's environment.
12
+ */
13
+ export const SPAWN_PROVENANCE_ENV = "SKC_SPAWNED_BY_SESSION";
14
+
4
15
  export interface NotificationConfig {
5
16
  enabled: boolean;
6
17
  botToken?: string;
@@ -15,6 +26,12 @@ export interface NotificationConfig {
15
26
  };
16
27
  redact: boolean;
17
28
  verbosity: "lean" | "verbose";
29
+ /**
30
+ * Which sessions may register a notification endpoint. `all` (default)
31
+ * preserves historical behavior; `primary` suppresses SKC-spawned children
32
+ * (those carrying {@link SPAWN_PROVENANCE_ENV}) unless they explicitly opt in.
33
+ */
34
+ sessionScope: "all" | "primary";
18
35
  idleTimeoutMs: number;
19
36
  rich: {
20
37
  enabled: boolean;
@@ -22,6 +39,15 @@ export interface NotificationConfig {
22
39
  richDraft: {
23
40
  enabled: boolean;
24
41
  };
42
+ topics: {
43
+ /**
44
+ * Optional Telegram forum-topic name template with `{repo}`, `{branch}`,
45
+ * and `{title}` placeholders. Unset preserves the built-in
46
+ * `{repo}/{branch} - {title}` composition (with its title/repo/branch
47
+ * fallbacks).
48
+ */
49
+ nameTemplate?: string;
50
+ };
25
51
  }
26
52
 
27
53
  /** Read typed config from Settings. */
@@ -40,6 +66,7 @@ export function getNotificationConfig(settings: Settings): NotificationConfig {
40
66
  },
41
67
  redact: settings.get("notifications.redact"),
42
68
  verbosity: settings.get("notifications.verbosity") === "verbose" ? "verbose" : "lean",
69
+ sessionScope: settings.get("notifications.sessionScope") === "primary" ? "primary" : "all",
43
70
  idleTimeoutMs: settings.get("notifications.daemon.idleTimeoutMs"),
44
71
  rich: {
45
72
  enabled: settings.get("notifications.telegram.rich.enabled"),
@@ -47,6 +74,9 @@ export function getNotificationConfig(settings: Settings): NotificationConfig {
47
74
  richDraft: {
48
75
  enabled: settings.get("notifications.telegram.richDraft.enabled"),
49
76
  },
77
+ topics: {
78
+ nameTemplate: settings.get("notifications.telegram.topics.nameTemplate"),
79
+ },
50
80
  };
51
81
  }
52
82
 
@@ -95,11 +125,25 @@ export function shouldRegisterNotificationsExtension(input: {
95
125
  parentTaskPrefix?: string;
96
126
  /** Role-agent type/name; present for task sessions even if depth metadata is lost. */
97
127
  currentAgentType?: string;
128
+ /**
129
+ * True when this session was launched by one of SKC's own programmatic
130
+ * separate-process child spawn sites (marked via {@link SPAWN_PROVENANCE_ENV}).
131
+ * Under `notifications.sessionScope = "primary"` such children are suppressed
132
+ * unless they explicitly opt in, so an interactive parent that fans out work
133
+ * does not flood the paired chat with topics for children the user never
134
+ * asked for. User-opened sessions (CLI/tmux/headless) never carry the marker.
135
+ */
136
+ spawnedBySkc?: boolean;
98
137
  }): boolean {
99
138
  if ((input.taskDepth ?? 0) > 0 || input.parentTaskPrefix || input.currentAgentType) return false;
100
139
  if (completionNotifyDisabledByEnv(input.env)) return false;
101
140
  if (input.env.SKC_NOTIFICATIONS === "0") return false;
102
141
  if (input.env.SKC_NOTIFICATIONS === "1" || input.env.SKC_NOTIFICATIONS_TOKEN) return true;
142
+ // Spawned-child suppression sits below explicit opt-in (so Telegram
143
+ // `/session_create` and cold `/session_resume`, which launch with
144
+ // SKC_NOTIFICATIONS=1, keep their fully bidirectional topic) and above global
145
+ // auto-on (so their children stay silent under `primary`).
146
+ if (input.spawnedBySkc && input.cfg?.sessionScope === "primary") return false;
103
147
  return input.cfg ? isGloballyConfigured(input.cfg) : false;
104
148
  }
105
149
 
@@ -1,5 +1,13 @@
1
1
  import * as path from "node:path";
2
2
 
3
+ /**
4
+ * Owner heartbeat freshness window. A daemon ownership record older than this
5
+ * (without a live pid) is considered stale. Lives here, in the lightweight
6
+ * paths module, so secret-safe consumers (e.g. the notification service) can
7
+ * reuse it without importing the heavy daemon runtime.
8
+ */
9
+ export const HEARTBEAT_TTL_MS = 20_000;
10
+
3
11
  export interface DaemonPaths {
4
12
  dir: string;
5
13
  lock: string;