@sayknow-cli/coding-agent 0.3.7 → 0.3.8

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/append-only-mode.d.ts +31 -0
  2. package/dist/types/capability/rule.d.ts +8 -0
  3. package/dist/types/cli/completion-cli.d.ts +41 -0
  4. package/dist/types/cli/config-cli.d.ts +1 -0
  5. package/dist/types/cli/fixture-report.d.ts +20 -0
  6. package/dist/types/cli/fixture-report.test.d.ts +1 -0
  7. package/dist/types/cli/nofile-limit.d.ts +11 -0
  8. package/dist/types/cli/notify-cli.d.ts +7 -0
  9. package/dist/types/cli/update-cli.d.ts +22 -0
  10. package/dist/types/cli.d.ts +124 -1
  11. package/dist/types/commands/completion.d.ts +29 -0
  12. package/dist/types/commands/config.d.ts +3 -0
  13. package/dist/types/commands/deep-interview.d.ts +3 -0
  14. package/dist/types/config/file-lock.d.ts +7 -6
  15. package/dist/types/config/keybindings.d.ts +2 -2
  16. package/dist/types/config/model-profile-activation.d.ts +6 -0
  17. package/dist/types/config/model-profiles.d.ts +1 -0
  18. package/dist/types/config/settings-schema.d.ts +105 -1
  19. package/dist/types/config/settings.d.ts +8 -0
  20. package/dist/types/discovery/index.d.ts +1 -0
  21. package/dist/types/export/html/template.generated.d.ts +1 -1
  22. package/dist/types/export/ttsr.d.ts +14 -3
  23. package/dist/types/extensibility/extensions/runner.d.ts +2 -1
  24. package/dist/types/extensibility/extensions/types.d.ts +8 -0
  25. package/dist/types/goals/runtime.d.ts +1 -0
  26. package/dist/types/hooks/native-skill-hook.d.ts +12 -0
  27. package/dist/types/main.d.ts +4 -1
  28. package/dist/types/modes/components/assistant-message.d.ts +5 -1
  29. package/dist/types/modes/components/model-selector.d.ts +4 -0
  30. package/dist/types/modes/components/queued-message-selector.d.ts +12 -0
  31. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  32. package/dist/types/modes/components/status-line.d.ts +9 -0
  33. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  34. package/dist/types/modes/controllers/event-controller.d.ts +5 -0
  35. package/dist/types/modes/controllers/input-controller.d.ts +1 -3
  36. package/dist/types/modes/interactive-mode.d.ts +2 -0
  37. package/dist/types/modes/print-mode.d.ts +13 -1
  38. package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -1
  39. package/dist/types/modes/rpc/rpc-types.d.ts +14 -0
  40. package/dist/types/modes/shared/agent-wire/event-contract.d.ts +19 -0
  41. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +23 -1
  42. package/dist/types/modes/types.d.ts +1 -0
  43. package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
  44. package/dist/types/notifications/config.d.ts +14 -1
  45. package/dist/types/notifications/daemon-paths.d.ts +1 -0
  46. package/dist/types/notifications/index.d.ts +6 -3
  47. package/dist/types/notifications/lifecycle-commands.d.ts +11 -3
  48. package/dist/types/notifications/telegram-daemon.d.ts +23 -3
  49. package/dist/types/notifications/telegram-reference.d.ts +0 -5
  50. package/dist/types/notifications/threaded-render.d.ts +6 -0
  51. package/dist/types/runtime-api-key.d.ts +7 -0
  52. package/dist/types/sdk.d.ts +3 -0
  53. package/dist/types/session/agent-session.d.ts +61 -6
  54. package/dist/types/session/agent-storage.d.ts +1 -0
  55. package/dist/types/session/file-mention-pruning.d.ts +8 -0
  56. package/dist/types/session/messages.d.ts +4 -0
  57. package/dist/types/session/session-manager.d.ts +17 -1
  58. package/dist/types/skc-runtime/deep-interview-ambiguity.d.ts +39 -0
  59. package/dist/types/skc-runtime/deep-interview-recorder.d.ts +10 -1
  60. package/dist/types/skc-runtime/deep-interview-state.d.ts +17 -2
  61. package/dist/types/skc-runtime/session-state-sidecar.d.ts +7 -0
  62. package/dist/types/skc-runtime/state-writer.d.ts +3 -0
  63. package/dist/types/skc-runtime/ultragoal-receipt-freshness.d.ts +42 -0
  64. package/dist/types/skc-runtime/ultragoal-runtime.d.ts +120 -13
  65. package/dist/types/skill-state/active-state.d.ts +18 -1
  66. package/dist/types/system-prompt.d.ts +18 -2
  67. package/dist/types/task/executor.d.ts +3 -0
  68. package/dist/types/task/fork-context-budget.d.ts +7 -0
  69. package/dist/types/task/token-log.d.ts +30 -0
  70. package/dist/types/task/token-log.test.d.ts +1 -0
  71. package/dist/types/tools/bisect.d.ts +81 -0
  72. package/dist/types/tools/computer.d.ts +2 -2
  73. package/dist/types/tools/index.d.ts +1 -0
  74. package/dist/types/tools/search.d.ts +2 -2
  75. package/dist/types/tools/ultragoal-ask-guard.d.ts +9 -2
  76. package/dist/types/utils/file-mentions.d.ts +14 -0
  77. package/dist/types/utils/git.d.ts +32 -0
  78. package/dist/types/web/scrapers/youtube.d.ts +8 -0
  79. package/dist/types/workflow/workflow-intent-diff.d.ts +8 -2
  80. package/dist/types/workflow/workflow-intent-report-consumer.d.ts +50 -0
  81. package/dist/types/workflow/workflow-intent-report-schema.d.ts +2 -0
  82. package/dist/types/workflow/workflow-intent-report.d.ts +48 -0
  83. package/package.json +8 -8
  84. package/src/append-only-mode.ts +58 -0
  85. package/src/autoresearch/prompt.md +1 -1
  86. package/src/autoresearch/tools/log-experiment.ts +1 -1
  87. package/src/capability/rule.ts +9 -0
  88. package/src/cli/completion-cli.ts +234 -0
  89. package/src/cli/config-cli.ts +61 -9
  90. package/src/cli/fast-help.ts +1 -0
  91. package/src/cli/fixture-report.test.ts +230 -0
  92. package/src/cli/fixture-report.ts +171 -0
  93. package/src/cli/nofile-limit.ts +48 -0
  94. package/src/cli/notify-cli.ts +92 -8
  95. package/src/cli/telegram-cli.ts +1 -1
  96. package/src/cli/update-cli.ts +85 -9
  97. package/src/cli.ts +29 -1
  98. package/src/commands/completion.ts +84 -0
  99. package/src/commands/config.ts +2 -0
  100. package/src/commands/contribution-prep.ts +1 -1
  101. package/src/commands/deep-interview.ts +2 -1
  102. package/src/config/file-lock.ts +73 -34
  103. package/src/config/keybindings.ts +2 -2
  104. package/src/config/model-profile-activation.ts +65 -0
  105. package/src/config/model-profiles.ts +31 -2
  106. package/src/config/settings-schema.ts +68 -1
  107. package/src/config/settings.ts +22 -0
  108. package/src/defaults/skc/skills/team/SKILL.md +17 -14
  109. package/src/defaults/skc/skills/ultragoal/SKILL.md +59 -4
  110. package/src/defaults/skc/skills/ultragoal/pipeline-validation-contracts.md +30 -0
  111. package/src/defaults/skc-defaults.ts +9 -0
  112. package/src/discovery/helpers.ts +10 -0
  113. package/src/discovery/index.ts +1 -0
  114. package/src/export/html/template.generated.ts +1 -1
  115. package/src/export/html/template.js +60 -2
  116. package/src/export/ttsr.ts +96 -18
  117. package/src/extensibility/extensions/runner.ts +183 -219
  118. package/src/extensibility/extensions/types.ts +9 -0
  119. package/src/extensibility/skc-plugins/runtime-adapters.ts +156 -44
  120. package/src/goals/runtime.ts +10 -2
  121. package/src/hindsight/backend.ts +1 -3
  122. package/src/hooks/native-skill-hook.ts +124 -4
  123. package/src/internal-urls/docs-index.generated.ts +14 -5
  124. package/src/main.ts +95 -43
  125. package/src/memories/index.ts +12 -1
  126. package/src/modes/components/assistant-message.ts +241 -82
  127. package/src/modes/components/custom-editor.ts +1 -0
  128. package/src/modes/components/model-selector.ts +54 -11
  129. package/src/modes/components/queued-message-selector.ts +92 -0
  130. package/src/modes/components/settings-selector.ts +4 -0
  131. package/src/modes/components/status-line.ts +167 -54
  132. package/src/modes/components/tool-execution.ts +44 -19
  133. package/src/modes/components/welcome.ts +181 -44
  134. package/src/modes/controllers/command-controller.ts +40 -4
  135. package/src/modes/controllers/event-controller.ts +66 -47
  136. package/src/modes/controllers/extension-ui-controller.ts +4 -10
  137. package/src/modes/controllers/input-controller.ts +180 -18
  138. package/src/modes/controllers/selector-controller.ts +127 -24
  139. package/src/modes/interactive-mode.ts +51 -16
  140. package/src/modes/print-mode.ts +43 -4
  141. package/src/modes/prompt-action-autocomplete.ts +21 -20
  142. package/src/modes/rpc/rpc-mode.ts +29 -3
  143. package/src/modes/rpc/rpc-types.ts +9 -0
  144. package/src/modes/shared/agent-wire/command-validation.ts +4 -0
  145. package/src/modes/shared/agent-wire/event-contract.ts +22 -0
  146. package/src/modes/shared/agent-wire/event-envelope.ts +82 -0
  147. package/src/modes/shared/agent-wire/scopes.ts +1 -0
  148. package/src/modes/types.ts +1 -0
  149. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  150. package/src/modes/utils/ui-helpers.ts +20 -7
  151. package/src/notifications/config.ts +25 -5
  152. package/src/notifications/daemon-paths.ts +2 -0
  153. package/src/notifications/index.ts +117 -14
  154. package/src/notifications/lifecycle-commands.ts +37 -7
  155. package/src/notifications/lifecycle-orchestrator.ts +19 -6
  156. package/src/notifications/telegram-daemon-control.ts +5 -3
  157. package/src/notifications/telegram-daemon.ts +302 -51
  158. package/src/notifications/telegram-reference.ts +0 -16
  159. package/src/notifications/threaded-render.ts +17 -1
  160. package/src/prompts/agents/architect.md +19 -10
  161. package/src/prompts/agents/critic.md +16 -18
  162. package/src/prompts/agents/executor.md +14 -11
  163. package/src/prompts/agents/planner.md +18 -13
  164. package/src/prompts/ci-green-request.md +3 -8
  165. package/src/prompts/goals/goal-continuation.md +0 -5
  166. package/src/prompts/goals/goal-mode-active.md +0 -5
  167. package/src/prompts/system/agent-creation-architect.md +2 -2
  168. package/src/prompts/system/plan-mode-active.md +1 -1
  169. package/src/prompts/system/plan-mode-subagent.md +3 -3
  170. package/src/prompts/system/project-prompt.md +0 -13
  171. package/src/prompts/system/subagent-system-prompt.md +3 -2
  172. package/src/prompts/system/system-prompt.md +29 -17
  173. package/src/prompts/system/volatile-project-context.md +10 -0
  174. package/src/prompts/system/web-search.md +4 -7
  175. package/src/prompts/tools/apply-patch.md +6 -0
  176. package/src/prompts/tools/ast-edit.md +1 -0
  177. package/src/prompts/tools/bash.md +1 -1
  178. package/src/prompts/tools/bisect.md +22 -0
  179. package/src/prompts/tools/browser.md +1 -1
  180. package/src/prompts/tools/computer.md +2 -2
  181. package/src/prompts/tools/cron.md +0 -2
  182. package/src/prompts/tools/eval.md +2 -2
  183. package/src/prompts/tools/find.md +1 -1
  184. package/src/prompts/tools/github.md +6 -6
  185. package/src/prompts/tools/goal.md +1 -3
  186. package/src/prompts/tools/image-gen.md +12 -5
  187. package/src/prompts/tools/irc.md +5 -4
  188. package/src/prompts/tools/job.md +2 -2
  189. package/src/prompts/tools/lsp.md +1 -1
  190. package/src/prompts/tools/monitor.md +2 -2
  191. package/src/prompts/tools/patch.md +2 -2
  192. package/src/prompts/tools/replace.md +2 -21
  193. package/src/prompts/tools/rewind.md +1 -1
  194. package/src/prompts/tools/search-tool-bm25.md +1 -11
  195. package/src/prompts/tools/search.md +1 -1
  196. package/src/prompts/tools/skill.md +3 -2
  197. package/src/prompts/tools/ssh.md +2 -1
  198. package/src/prompts/tools/subagent.md +2 -2
  199. package/src/prompts/tools/task.md +4 -5
  200. package/src/prompts/tools/todo-write.md +1 -1
  201. package/src/prompts/tools/vim.md +1 -0
  202. package/src/prompts/tools/web-search.md +2 -0
  203. package/src/prompts/tools/write.md +8 -2
  204. package/src/runtime-api-key.ts +16 -0
  205. package/src/sdk.ts +52 -28
  206. package/src/session/agent-session.ts +1011 -141
  207. package/src/session/agent-storage.ts +4 -0
  208. package/src/session/file-mention-pruning.ts +64 -0
  209. package/src/session/messages.ts +4 -0
  210. package/src/session/session-manager.ts +103 -4
  211. package/src/skc-runtime/deep-interview-ambiguity.ts +206 -0
  212. package/src/skc-runtime/deep-interview-recorder.ts +62 -8
  213. package/src/skc-runtime/deep-interview-state.ts +44 -2
  214. package/src/skc-runtime/session-state-sidecar.ts +263 -20
  215. package/src/skc-runtime/state-runtime.ts +12 -2
  216. package/src/skc-runtime/state-writer.ts +15 -0
  217. package/src/skc-runtime/ultragoal-guard.ts +55 -66
  218. package/src/skc-runtime/ultragoal-receipt-freshness.ts +315 -0
  219. package/src/skc-runtime/ultragoal-runtime.ts +1663 -159
  220. package/src/skc-runtime/workflow-manifest.generated.json +113 -2
  221. package/src/skc-runtime/workflow-manifest.ts +74 -1
  222. package/src/skill-state/active-state.ts +141 -5
  223. package/src/skill-state/deep-interview-mutation-guard.ts +67 -3
  224. package/src/slash-commands/builtin-registry.ts +218 -71
  225. package/src/system-prompt.ts +57 -16
  226. package/src/task/executor.ts +132 -24
  227. package/src/task/fork-context-advisory.ts +2 -7
  228. package/src/task/fork-context-budget.ts +9 -0
  229. package/src/task/index.ts +13 -6
  230. package/src/task/output-manager.ts +20 -8
  231. package/src/task/receipt.ts +26 -7
  232. package/src/task/token-log.test.ts +188 -0
  233. package/src/task/token-log.ts +179 -0
  234. package/src/tools/bisect.ts +367 -0
  235. package/src/tools/computer.ts +246 -43
  236. package/src/tools/index.ts +3 -0
  237. package/src/tools/output-meta.ts +116 -4
  238. package/src/tools/search.ts +8 -2
  239. package/src/tools/skill.ts +15 -0
  240. package/src/tools/telegram-send.ts +6 -0
  241. package/src/tools/ultragoal-ask-guard.ts +39 -5
  242. package/src/utils/file-mentions.ts +39 -7
  243. package/src/utils/git.ts +73 -0
  244. package/src/web/scrapers/youtube.ts +24 -12
  245. package/src/workflow/workflow-intent-diff.ts +29 -6
  246. package/src/workflow/workflow-intent-report-consumer.ts +221 -0
  247. package/src/workflow/workflow-intent-report-schema.ts +129 -0
  248. package/src/workflow/workflow-intent-report.ts +135 -0
@@ -28,13 +28,13 @@ import type { ImageContent, TextContent } from "@sayknow-cli/ai";
28
28
  import { NotificationServer } from "@sayknow-cli/natives";
29
29
  import { logger, postmortem } from "@sayknow-cli/utils";
30
30
  import { Settings } from "../config/settings";
31
- import type { ExtensionCommandContext, ExtensionContext, ExtensionFactory } from "../extensibility/extensions";
31
+ import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "../extensibility/extensions";
32
32
  import { registerAskAnswerSource } from "../tools/ask-answer-registry";
33
33
  import { registerTelegramFileSink } from "./attachment-registry";
34
34
  import {
35
35
  getNotificationConfig,
36
- isGloballyConfigured,
37
36
  isSessionNotificationsEnabled,
37
+ isTelegramConfigured,
38
38
  type NotificationConfig,
39
39
  sessionTag,
40
40
  } from "./config";
@@ -100,7 +100,7 @@ export interface SessionCloseFrame {
100
100
  chatId: string;
101
101
  token: string;
102
102
  target: SessionCloseTarget;
103
- /** Hard-kill even if a live pane is attached (SKC-managed only). */
103
+ /** Required force-only close flag; false/omitted is rejected by daemon policy. */
104
104
  force?: boolean;
105
105
  }
106
106
 
@@ -336,6 +336,8 @@ interface SessionRuntime {
336
336
  disposeAnswerSource: () => void;
337
337
  /** Deregisters this session's Telegram file sink. */
338
338
  disposeFileSink: () => void;
339
+ /** Deregisters this session's unattended workflow-gate listener. */
340
+ disposeGateListener: () => void;
339
341
  redact: boolean;
340
342
  verbosity: "lean" | "verbose";
341
343
  sessionTag: string;
@@ -348,6 +350,12 @@ interface SessionRuntime {
348
350
  /** Assistant text already flushed before an ask this turn (turn-scoped dedupe
349
351
  * so turn_end does not re-emit the pre-ask lead-in). Reset each turn. */
350
352
  preAskFlushedText?: string;
353
+ /** Live streaming: opt-in flag, monotonic per-turn ref, and emit throttle state. */
354
+ stream: boolean;
355
+ turnSeq?: number;
356
+ liveRef?: string;
357
+ lastLiveAt?: number;
358
+ lastLiveText?: string;
351
359
  /** Cancels the postmortem cleanup that emits `session_closed` on process teardown. */
352
360
  cancelPostmortemCleanup: () => void;
353
361
  }
@@ -358,6 +366,8 @@ interface ResolvedSettings {
358
366
  settingsAvailable: boolean;
359
367
  }
360
368
 
369
+ const TELEGRAM_FILE_REDACTION_ERROR = "Telegram file attachments are disabled while notifications redaction is on.";
370
+
361
371
  const defaultConfig: NotificationConfig = {
362
372
  enabled: false,
363
373
  botToken: undefined,
@@ -379,7 +389,36 @@ export function notificationsEnabled(): boolean {
379
389
  return process.env.SKC_NOTIFICATIONS === "1" || Boolean(process.env.SKC_NOTIFICATIONS_TOKEN);
380
390
  }
381
391
 
382
- function resolveSettings(): ResolvedSettings {
392
+ // Live streaming (opt-in): emit throttled non-finalized `turn_stream` frames as
393
+ // the assistant message streams so remote clients can edit ONE message live. The
394
+ // finalized frame (turn_end) carries the same messageRef and stays authoritative,
395
+ // so a dropped live frame self-heals. Off unless SKC_NOTIFICATIONS_STREAM=1.
396
+ function streamingEnabled(): boolean {
397
+ return process.env.SKC_NOTIFICATIONS_STREAM === "1";
398
+ }
399
+ function streamIntervalMs(): number {
400
+ return Math.max(200, Number(process.env.SKC_NOTIFICATIONS_STREAM_INTERVAL_MS) || 500);
401
+ }
402
+ // Max chars of a turn's assistant text carried by the FINALIZED turn_stream (and
403
+ // the pre-ask capture). Default 3500 keeps the mirror a glanceable per-turn
404
+ // summary; a client that splits long messages (the Telegram daemon does so via
405
+ // splitTelegramHtml, scheduling each chunk through the shared rate-limit pool so
406
+ // the fan-out never bypasses the per-chat limit) can raise it with
407
+ // SKC_NOTIFICATIONS_TURN_MAX to deliver full turns. The value is clamped to a
408
+ // finite [280, TURN_TEXT_MAX_CEILING] range: a non-finite or non-positive env
409
+ // (unset, NaN, Infinity, <= 0) falls back to the default, so the cap can never
410
+ // be unbounded. Live frames are intentionally NOT raised — they stay one
411
+ // editable preview message rather than fanning a long in-progress turn across
412
+ // sends.
413
+ const TURN_TEXT_MAX_CEILING = 40_000;
414
+ function turnTextMax(): number {
415
+ const raw = Number(process.env.SKC_NOTIFICATIONS_TURN_MAX);
416
+ if (!Number.isFinite(raw) || raw <= 0) return 3500;
417
+ return Math.min(TURN_TEXT_MAX_CEILING, Math.max(280, raw));
418
+ }
419
+ function resolveSettings(settingsOverride?: Settings): ResolvedSettings {
420
+ if (settingsOverride)
421
+ return { settings: settingsOverride, cfg: getNotificationConfig(settingsOverride), settingsAvailable: true };
383
422
  try {
384
423
  const settings = Settings.instance;
385
424
  return { settings, cfg: getNotificationConfig(settings), settingsAvailable: true };
@@ -491,7 +530,7 @@ function sessionIdFromFile(file: string | undefined): string | undefined {
491
530
  return underscore >= 0 ? base.slice(underscore + 1) : undefined;
492
531
  }
493
532
 
494
- export const createNotificationsExtension: ExtensionFactory = api => {
533
+ export function createNotificationsExtension(api: ExtensionAPI, options: { settings?: Settings } = {}): void {
495
534
  const runtimes = new Map<string, SessionRuntime>();
496
535
  const disabledSessions = new Set<string>();
497
536
  const sessionId = (ctx: ExtensionContext): string => ctx.sessionManager.getSessionId();
@@ -503,9 +542,16 @@ export const createNotificationsExtension: ExtensionFactory = api => {
503
542
  runtimes.delete(id);
504
543
  try {
505
544
  rt.cancelPostmortemCleanup();
545
+ } catch {}
546
+ try {
506
547
  rt.disposeAnswerSource();
548
+ } catch {}
549
+ try {
507
550
  rt.disposeFileSink();
508
551
  } catch {}
552
+ try {
553
+ rt.disposeGateListener();
554
+ } catch {}
509
555
  // Resolve any still-pending interactive asks so the ask tool is not left hanging.
510
556
  for (const pending of rt.pendingInteractive.values()) pending.resolve(undefined);
511
557
  rt.pendingInteractive.clear();
@@ -529,10 +575,14 @@ export const createNotificationsExtension: ExtensionFactory = api => {
529
575
  return isSessionNotificationsEnabled({ cfg, env: process.env, sessionDisabled: disabledSessions.has(id) });
530
576
  }
531
577
 
578
+ function isNotificationEligibleContext(ctx: ExtensionContext): boolean {
579
+ return ctx.sessionMetadata?.kind !== "sub";
580
+ }
581
+
532
582
  async function startSession(ctx: ExtensionContext): Promise<"started" | "already" | "disabled" | "failed"> {
533
583
  const id = sessionId(ctx);
534
- const { settings, cfg, settingsAvailable } = resolveSettings();
535
- if (!isEnabledForSession(id, cfg)) return "disabled";
584
+ const { settings, cfg, settingsAvailable } = resolveSettings(options.settings);
585
+ if (!isNotificationEligibleContext(ctx) || !isEnabledForSession(id, cfg)) return "disabled";
536
586
  if (runtimes.has(id)) return "already";
537
587
 
538
588
  const stateRoot = path.join(ctx.cwd, ".skc", "state");
@@ -660,6 +710,10 @@ export const createNotificationsExtension: ExtensionFactory = api => {
660
710
  tag,
661
711
  );
662
712
  const disposeFileSink = registerTelegramFileSink(id, async file => {
713
+ if (runtime?.redact ?? redact) {
714
+ return { ok: false, error: TELEGRAM_FILE_REDACTION_ERROR };
715
+ }
716
+
663
717
  try {
664
718
  const data = await fs.promises.readFile(file.path);
665
719
  server.pushFrame(
@@ -683,9 +737,11 @@ export const createNotificationsExtension: ExtensionFactory = api => {
683
737
  pendingInteractive,
684
738
  disposeAnswerSource,
685
739
  disposeFileSink,
740
+ disposeGateListener: () => {},
686
741
  cancelPostmortemCleanup: () => {},
687
742
  redact,
688
743
  verbosity,
744
+ stream: streamingEnabled(),
689
745
  sessionTag: tag,
690
746
  busy: false,
691
747
  pendingInbound: new Set<number>(),
@@ -702,7 +758,7 @@ export const createNotificationsExtension: ExtensionFactory = api => {
702
758
  });
703
759
  logger.info(`notifications: serving session ${id} at ${endpoint.url} (unattended=${unattended})`);
704
760
 
705
- if (settingsAvailable && settings && isGloballyConfigured(cfg)) {
761
+ if (settingsAvailable && settings && isTelegramConfigured(cfg)) {
706
762
  try {
707
763
  await ensureTelegramDaemonRunning({ settings, cwd: ctx.cwd, sessionId: id });
708
764
  } catch (e) {
@@ -726,7 +782,7 @@ export const createNotificationsExtension: ExtensionFactory = api => {
726
782
 
727
783
  // Unattended: a real ask emits a workflow gate; register it repliable by gate_id.
728
784
  if (unattended && gate?.onGateEmitted) {
729
- gate.onGateEmitted(g => {
785
+ runtime.disposeGateListener = gate.onGateEmitted(g => {
730
786
  const options = (g.options ?? []).map(o => String((o as { label?: unknown }).label ?? ""));
731
787
  gateOptions.set(g.gate_id, options);
732
788
  const promptCtx = g.context as { prompt?: unknown; title?: unknown } | undefined;
@@ -761,7 +817,7 @@ export const createNotificationsExtension: ExtensionFactory = api => {
761
817
  async handler(args: string, ctx: ExtensionCommandContext): Promise<void> {
762
818
  const id = sessionId(ctx);
763
819
  const command = args.trim().split(/\s+/, 1)[0]?.toLowerCase() || "status";
764
- const resolved = resolveSettings();
820
+ const resolved = resolveSettings(options.settings);
765
821
  const enabledWithoutLocalOff = isSessionNotificationsEnabled({
766
822
  cfg: resolved.cfg,
767
823
  env: process.env,
@@ -781,6 +837,10 @@ export const createNotificationsExtension: ExtensionFactory = api => {
781
837
  }
782
838
 
783
839
  if (command === "on") {
840
+ if (!isNotificationEligibleContext(ctx)) {
841
+ ctx.ui.notify("Notifications are disabled for subagent sessions.", "warning");
842
+ return;
843
+ }
784
844
  if (process.env.SKC_NOTIFICATIONS === "0") {
785
845
  ctx.ui.notify(
786
846
  "Notifications remain disabled: SKC_NOTIFICATIONS=0 is an authoritative opt-out.",
@@ -878,6 +938,10 @@ export const createNotificationsExtension: ExtensionFactory = api => {
878
938
  rt.sessionTag,
879
939
  );
880
940
  rt.disposeFileSink = registerTelegramFileSink(newId, async file => {
941
+ if (rt.redact) {
942
+ return { ok: false, error: TELEGRAM_FILE_REDACTION_ERROR };
943
+ }
944
+
881
945
  try {
882
946
  const data = await fs.promises.readFile(file.path);
883
947
  rt.server.pushFrame(
@@ -1033,7 +1097,15 @@ export const createNotificationsExtension: ExtensionFactory = api => {
1033
1097
  if (!text || text === rt.preAskFlushedText) return;
1034
1098
  rt.preAskFlushedText = text;
1035
1099
  try {
1036
- rt.server.pushFrame(JSON.stringify({ type: "turn_stream", sessionId: id, phase: "finalized", text }));
1100
+ rt.server.pushFrame(
1101
+ JSON.stringify({
1102
+ type: "turn_stream",
1103
+ sessionId: id,
1104
+ phase: "finalized",
1105
+ text,
1106
+ ...(rt.liveRef ? { messageRef: rt.liveRef } : {}),
1107
+ }),
1108
+ );
1037
1109
  } catch (e) {
1038
1110
  logger.warn(`notifications: pushFrame (turn) failed: ${String(e)}`);
1039
1111
  }
@@ -1057,12 +1129,43 @@ export const createNotificationsExtension: ExtensionFactory = api => {
1057
1129
  const id = sessionId(ctx);
1058
1130
  const rt = runtimes.get(id);
1059
1131
  if (!rt) return;
1060
- const text = rt.redact ? undefined : summaryFromMessage(event.message, 3500);
1132
+ const text = rt.redact ? undefined : summaryFromMessage(event.message, turnTextMax());
1061
1133
  if (text) flushTurnText(rt, id, text);
1062
1134
  // Reset per-turn streaming state so the next turn starts fresh and a later
1063
1135
  // turn with identical text is not falsely deduped.
1064
1136
  rt.currentTurnText = undefined;
1065
1137
  rt.preAskFlushedText = undefined;
1138
+ rt.liveRef = undefined;
1139
+ rt.lastLiveAt = undefined;
1140
+ rt.lastLiveText = undefined;
1141
+ });
1142
+
1143
+ // Live streaming (opt-in): push throttled in-progress assistant text as
1144
+ // non-finalized turn_stream frames so remote clients edit one message as the
1145
+ // turn streams. The finalized frame (turn_end) carries the same messageRef and
1146
+ // lands the authoritative text. Suppressed under redaction.
1147
+ api.on("message_update", (event, ctx) => {
1148
+ const id = sessionId(ctx);
1149
+ const rt = runtimes.get(id);
1150
+ if (!rt?.stream || rt.redact) return;
1151
+ if ((event.message as { role?: unknown }).role !== "assistant") return;
1152
+ if (rt.liveRef === undefined) {
1153
+ rt.turnSeq = (rt.turnSeq ?? 0) + 1;
1154
+ rt.liveRef = String(rt.turnSeq);
1155
+ }
1156
+ const now = Date.now();
1157
+ if (now - (rt.lastLiveAt ?? 0) < streamIntervalMs()) return;
1158
+ const text = summaryFromMessage(event.message, 3500);
1159
+ if (!text || text === rt.lastLiveText) return;
1160
+ rt.lastLiveAt = now;
1161
+ rt.lastLiveText = text;
1162
+ try {
1163
+ rt.server.pushFrame(
1164
+ JSON.stringify({ type: "turn_stream", sessionId: id, phase: "live", text, messageRef: rt.liveRef }),
1165
+ );
1166
+ } catch (e) {
1167
+ logger.warn(`notifications: pushFrame (live) failed: ${String(e)}`);
1168
+ }
1066
1169
  });
1067
1170
 
1068
1171
  // Stream agent-produced images (computer/browser/tool screenshots) as
@@ -1076,7 +1179,7 @@ export const createNotificationsExtension: ExtensionFactory = api => {
1076
1179
  // flush can emit it before the ask prompt. Role-scoped: message_end also
1077
1180
  // fires for the user prompt, which must never be mirrored back as turn output.
1078
1181
  if ((event.message as { role?: unknown }).role === "assistant") {
1079
- const turnText = summaryFromMessage(event.message, 3500);
1182
+ const turnText = summaryFromMessage(event.message, turnTextMax());
1080
1183
  if (turnText) rt.currentTurnText = turnText;
1081
1184
  }
1082
1185
  for (const img of imageAttachmentsFromMessage(event.message)) {
@@ -1099,4 +1202,4 @@ export const createNotificationsExtension: ExtensionFactory = api => {
1099
1202
  api.on("session_shutdown", async (_event, ctx) => {
1100
1203
  await stopSession(sessionId(ctx));
1101
1204
  });
1102
- };
1205
+ }
@@ -15,6 +15,19 @@ import * as os from "node:os";
15
15
  import type { SessionCloseTarget, SessionCreateTarget, SessionLifecycleResponse, SessionResumeTarget } from "./index";
16
16
 
17
17
  export type LifecycleCommandVerb = "session_create" | "session_close" | "session_resume";
18
+ function normalizeLifecycleCommandToken(
19
+ token: string,
20
+ ctx: { chatType?: string; botUsername?: string } = {},
21
+ ): string | undefined {
22
+ const at = token.indexOf("@");
23
+ const command = at === -1 ? token : token.slice(0, at);
24
+ if (!/^\/session_(create|close|resume|recent)\b/.test(command)) return undefined;
25
+ if (at === -1) return ctx.chatType === undefined || ctx.chatType === "private" ? command : undefined;
26
+
27
+ const botUsername = ctx.botUsername;
28
+ if (!botUsername || token.slice(at + 1).toLowerCase() !== botUsername.toLowerCase()) return undefined;
29
+ return command;
30
+ }
18
31
 
19
32
  /** A parsed, validated lifecycle command (transport identity added by caller). */
20
33
  export type ParsedLifecycleCommand =
@@ -36,10 +49,24 @@ const USAGE = [
36
49
  "/session_recent [create|resume]",
37
50
  ].join("\n");
38
51
 
39
- /** True when the text begins a /session_* command (cheap pre-gate). */
40
- export function isLifecycleCommandText(text: string | undefined): boolean {
52
+ /** True when the text begins with any /session_* token, regardless of addressability. */
53
+ export function isLifecycleCommandLikeText(text: string | undefined): boolean {
54
+ if (!text) return false;
55
+ const [token] = text.trim().split(/\s+/, 1);
56
+ if (!token) return false;
57
+ const at = token.indexOf("@");
58
+ const command = at === -1 ? token : token.slice(0, at);
59
+ return /^\/session_(create|close|resume|recent)\b/.test(command);
60
+ }
61
+
62
+ /** True when the text begins an addressable /session_* command (cheap pre-gate). */
63
+ export function isLifecycleCommandText(
64
+ text: string | undefined,
65
+ ctx: { chatType?: string; botUsername?: string } = {},
66
+ ): boolean {
41
67
  if (!text) return false;
42
- return /^\/session_(create|close|resume|recent)\b/.test(text.trim());
68
+ const [rawCommand] = text.trim().split(/\s+/, 1);
69
+ return normalizeLifecycleCommandToken(rawCommand ?? "", ctx) !== undefined;
43
70
  }
44
71
 
45
72
  /**
@@ -50,9 +77,14 @@ export function isLifecycleCommandText(text: string | undefined): boolean {
50
77
  * The caller MUST have already enforced paired-chat authorization; this function
51
78
  * performs grammar + target validation only.
52
79
  */
53
- export function parseLifecycleCommand(text: string | undefined): ParsedLifecycleCommand {
54
- if (!isLifecycleCommandText(text)) return { kind: "none" };
80
+ export function parseLifecycleCommand(
81
+ text: string | undefined,
82
+ ctx: { chatType?: string; botUsername?: string } = {},
83
+ ): ParsedLifecycleCommand {
55
84
  const raw = (text ?? "").trim();
85
+ const [rawCommand, ...args] = raw.split(/\s+/);
86
+ const command = normalizeLifecycleCommandToken(rawCommand ?? "", ctx);
87
+ if (command === undefined) return { kind: "none" };
56
88
 
57
89
  // MVP: reject any initial-prompt separator outright (no prompt handling yet).
58
90
  if (/\s--(\s|$)/.test(raw)) {
@@ -63,8 +95,6 @@ export function parseLifecycleCommand(text: string | undefined): ParsedLifecycle
63
95
  };
64
96
  }
65
97
 
66
- const [command, ...args] = raw.split(/\s+/);
67
-
68
98
  if (command === "/session_recent") {
69
99
  const which = args[0];
70
100
  if (which === undefined || which === "create" || which === "resume") {
@@ -252,7 +252,17 @@ export async function handleLifecycleRequest(
252
252
  return { status: "error", reason: "rate_limited", message: "create rate limit exceeded" };
253
253
  }
254
254
 
255
- // 4. Preallocate ids + write in_progress (fsynced) BEFORE any spawn.
255
+ // 4. Close is intentionally force-only until a graceful close contract exists.
256
+ if (frame.type === "session_close" && frame.force !== true) {
257
+ await deps.audit({ ...baseAudit, event: "rejected", reason: "invalid_target" });
258
+ return {
259
+ status: "error",
260
+ reason: "invalid_target",
261
+ message: "session_close requires force=true; graceful close is not supported",
262
+ };
263
+ }
264
+
265
+ // 5. Preallocate ids + write in_progress (fsynced) BEFORE any spawn.
256
266
  const lifecycleRequestId = frame.type === "session_create" ? frame.lifecycleRequestId : deps.newLifecycleRequestId();
257
267
  const intendedSessionId =
258
268
  frame.type === "session_create" ? frame.intendedSessionId || deps.newSessionId() : deps.newSessionId();
@@ -344,15 +354,18 @@ export async function handleLifecycleRequest(
344
354
  await deps.audit({ ...baseAudit, event: "success", sessionId: resumed.sessionId });
345
355
  return { status: "ok", entry, mode: resumed.mode };
346
356
  } catch (err) {
347
- // A side effect may have occurred; do not auto-respawn. Mark terminal
348
- // uncertain so a retry reconciles instead of duplicating.
357
+ // A side effect may have occurred; do not repeat it automatically. Mark
358
+ // terminal-uncertain so a retry reconciles instead of duplicating work.
359
+ let reason: LifecycleErrorReason = "terminal_uncertain";
360
+ if (frame.type === "session_create") reason = "spawn_failed";
361
+ if (frame.type === "session_close") reason = "close_refused";
349
362
  Object.assign(entry, {
350
363
  state: "terminal_uncertain",
351
364
  updatedAt: deps.now(),
352
- reason: "spawn_failed",
365
+ reason,
353
366
  });
354
367
  await deps.store.write(doc);
355
- await deps.audit({ ...baseAudit, event: "terminal_uncertain", reason: "spawn_failed" });
356
- return { status: "error", reason: "terminal_uncertain", message: `lifecycle effect failed: ${String(err)}` };
368
+ await deps.audit({ ...baseAudit, event: "terminal_uncertain", reason });
369
+ return { status: "error", reason: "terminal_uncertain", message: `${frame.type} effect failed: ${String(err)}` };
357
370
  }
358
371
  }
@@ -20,7 +20,7 @@ import type {
20
20
  DaemonStatus,
21
21
  } from "../daemon/control-types";
22
22
  import { resolveSkcRuntimeSpawnInfo } from "../daemon/runtime";
23
- import { getNotificationConfig, isGloballyConfigured, tokenFingerprint } from "./config";
23
+ import { getNotificationConfig, isTelegramConfigured, tokenFingerprint } from "./config";
24
24
  import {
25
25
  daemonPaths,
26
26
  isFreshLiveOwner,
@@ -159,7 +159,7 @@ export class TelegramDaemonController implements BuiltInDaemonController {
159
159
  async status(): Promise<DaemonStatus> {
160
160
  const runtime = this.runtimeInfo();
161
161
  const cfg = getNotificationConfig(this.settings);
162
- const configured = isGloballyConfigured(cfg) && Boolean(cfg.botToken) && Boolean(cfg.chatId);
162
+ const configured = isTelegramConfigured(cfg);
163
163
  if (!configured) {
164
164
  return { kind: this.kind, configured: false, health: "not_configured", runtime };
165
165
  }
@@ -269,6 +269,7 @@ export class TelegramDaemonController implements BuiltInDaemonController {
269
269
  { settings: this.settings, roots, tokenFingerprint: fp, chatId },
270
270
  this.spawnDeps(),
271
271
  );
272
+ warnings.push(...spawned.warnings);
272
273
  const after = await this.status();
273
274
  return this.result(
274
275
  action,
@@ -351,6 +352,7 @@ export class TelegramDaemonController implements BuiltInDaemonController {
351
352
  { settings: this.settings, roots, tokenFingerprint: fp, chatId },
352
353
  this.spawnDeps(),
353
354
  );
355
+ warnings.push(...spawned.warnings);
354
356
  const after = await this.status();
355
357
  if (spawned.result === "attached") {
356
358
  // A live owner already exists; attaching to it is a valid running end-state.
@@ -360,7 +362,7 @@ export class TelegramDaemonController implements BuiltInDaemonController {
360
362
  }
361
363
  return this.result(
362
364
  action,
363
- spawned.result !== "disabled",
365
+ spawned.result !== "disabled" && spawned.result !== "blocked",
364
366
  `reloaded telegram daemon (${spawned.result})`,
365
367
  before,
366
368
  after,