@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
@@ -6,7 +6,7 @@
6
6
  * - `skc --mode json "prompt"` - JSON event stream
7
7
  */
8
8
  import { type AssistantMessage, type ImageContent, isContextOverflow } from "@sayknow-cli/ai";
9
- import { logger, sanitizeText } from "@sayknow-cli/utils";
9
+ import { isKnownSinkPeerClosedError, logger, sanitizeText } from "@sayknow-cli/utils";
10
10
  import type { AgentSession } from "../session/agent-session";
11
11
  import { isSilentAbort } from "../session/messages";
12
12
  import { initializeExtensions } from "./runtime-init";
@@ -24,9 +24,8 @@ export interface PrintModeOptions {
24
24
  /** Images to attach to the initial message */
25
25
  initialImages?: ImageContent[];
26
26
  /**
27
- * When true, an assistant error/abort does not call process.exit(); print mode
28
- * returns instead so the caller (e.g. RLM autonomous mode) can run its own
29
- * finalization/cleanup before the process exits.
27
+ * When true, a terminal assistant error/abort leaves process exit status
28
+ * untouched so the caller can run its own finalization and status handling.
30
29
  */
31
30
  suppressProcessExit?: boolean;
32
31
  }
@@ -42,7 +41,7 @@ export interface PrintModeOptions {
42
41
  * events from the subscription and does not run this terminal-error branch, so
43
42
  * it is intentionally NOT covered by this exit code.
44
43
  */
45
- export const CONTEXT_OVERFLOW_EXIT_CODE = 2;
44
+ export const CONTEXT_OVERFLOW_EXIT_CODE = 78;
46
45
 
47
46
  /**
48
47
  * Build an actionable stderr diagnostic for a terminal context-overflow error in
@@ -58,113 +57,250 @@ function formatContextOverflowError(message: AssistantMessage, autoCompactionEna
58
57
  return `${guidance}${providerDetail}`;
59
58
  }
60
59
 
60
+ /**
61
+ * Own process stdout only while print mode is actively writing. This lets this
62
+ * mode absorb a peer-closing its output pipe without changing error-listener
63
+ * ownership for the interactive/TUI and clipboard paths.
64
+ */
65
+ function createPrintStdoutOwner(): {
66
+ write(chunk: string): void;
67
+ flush(): Promise<void>;
68
+ dispose(): void;
69
+ } {
70
+ let epipeLatched = false;
71
+ let writeError: unknown;
72
+ let hasWriteError = false;
73
+ const pendingWrites = new Set<{ done: Promise<void>; settle(): void }>();
74
+
75
+ const isEpipe = (error: unknown): boolean => {
76
+ try {
77
+ return (error as NodeJS.ErrnoException | null | undefined)?.code === "EPIPE";
78
+ } catch {
79
+ return false;
80
+ }
81
+ };
82
+
83
+ const settlePendingWrites = (): void => {
84
+ for (const pending of [...pendingWrites]) pending.settle();
85
+ };
86
+
87
+ const handleWriteError = (error: unknown): void => {
88
+ // An EPIPE from this owned stdout sink proves its peer closed. A destroyed
89
+ // stream does not prove that independently, so it is tolerated only after
90
+ // this owner has already observed and latched an EPIPE.
91
+ if (isEpipe(error)) {
92
+ epipeLatched = true;
93
+ settlePendingWrites();
94
+ return;
95
+ }
96
+ if (epipeLatched && isKnownSinkPeerClosedError(error)) {
97
+ settlePendingWrites();
98
+ return;
99
+ }
100
+
101
+ if (!hasWriteError) {
102
+ writeError = error;
103
+ hasWriteError = true;
104
+ }
105
+ settlePendingWrites();
106
+ };
107
+
108
+ const onStdoutError = (error: Error): void => {
109
+ handleWriteError(error);
110
+ };
111
+ process.stdout.on("error", onStdoutError);
112
+
113
+ const waitForWrites = async (): Promise<void> => {
114
+ while (pendingWrites.size > 0) {
115
+ await Promise.all([...pendingWrites].map(pending => pending.done));
116
+ }
117
+ };
118
+
119
+ return {
120
+ write(chunk: string): void {
121
+ if (epipeLatched || hasWriteError) return;
122
+
123
+ let settled = false;
124
+ const completion = Promise.withResolvers<void>();
125
+ let pending: { done: Promise<void>; settle(): void };
126
+ pending = {
127
+ done: completion.promise,
128
+ settle: (): void => {
129
+ if (settled) return;
130
+ settled = true;
131
+ pendingWrites.delete(pending);
132
+ completion.resolve();
133
+ },
134
+ };
135
+ const complete = (error?: Error | null): void => {
136
+ if (settled) return;
137
+ if (error) handleWriteError(error);
138
+ pending.settle();
139
+ };
140
+
141
+ pendingWrites.add(pending);
142
+ try {
143
+ process.stdout.write(chunk, complete);
144
+ } catch (error) {
145
+ handleWriteError(error);
146
+ pending.settle();
147
+ }
148
+ },
149
+
150
+ async flush(): Promise<void> {
151
+ await waitForWrites();
152
+ if (!epipeLatched && !hasWriteError) {
153
+ this.write("");
154
+ await waitForWrites();
155
+ }
156
+ if (hasWriteError) throw writeError;
157
+ },
158
+
159
+ dispose(): void {
160
+ process.stdout.removeListener("error", onStdoutError);
161
+ },
162
+ };
163
+ }
164
+
165
+ async function writeStderrAndQuiesce(chunk: string): Promise<void> {
166
+ const completion = Promise.withResolvers<void>();
167
+ let settled = false;
168
+ const complete = (error?: unknown): void => {
169
+ if (settled) return;
170
+ settled = true;
171
+ if (error === undefined || error === null) completion.resolve();
172
+ else completion.reject(error);
173
+ };
174
+
175
+ try {
176
+ process.stderr.write(chunk, complete);
177
+ } catch (error) {
178
+ complete(error);
179
+ }
180
+ await completion.promise;
181
+ }
182
+
183
+ function throwCollectedErrors(errors: unknown[]): void {
184
+ const uniqueErrors = [...new Set(errors)];
185
+ if (uniqueErrors.length === 0) return;
186
+ if (uniqueErrors.length === 1) throw uniqueErrors[0];
187
+ throw new AggregateError(uniqueErrors, "Print mode failed while flushing output and disposing the session");
188
+ }
189
+
61
190
  /**
62
191
  * Run in print (single-shot) mode.
63
192
  * Sends prompts to the agent and outputs the result.
64
193
  */
65
194
  export async function runPrintMode(session: AgentSession, options: PrintModeOptions): Promise<void> {
66
195
  const { mode, messages = [], initialMessage, initialImages } = options;
196
+ const stdout = createPrintStdoutOwner();
197
+ const failures: unknown[] = [];
198
+ let unsubscribe: (() => void) | undefined;
67
199
 
68
- // Emit session header for JSON mode
69
- if (mode === "json") {
70
- const header = session.sessionManager.getHeader();
71
- if (header) {
72
- process.stdout.write(`${JSON.stringify(header)}\n`);
200
+ try {
201
+ // Emit session header for JSON mode.
202
+ if (mode === "json") {
203
+ const header = session.sessionManager.getHeader();
204
+ if (header) stdout.write(`${JSON.stringify(header)}\n`);
73
205
  }
74
- }
75
- // Set up extensions for print mode (no UI, no command context)
76
- await initializeExtensions(session, {
77
- reportSendError: (action, err) => {
78
- process.stderr.write(
79
- `Extension ${action === "extension_send" ? "sendMessage" : "sendUserMessage"} failed: ${err.message}\n`,
80
- );
81
- },
82
- reportRuntimeError: err => {
83
- process.stderr.write(`Extension error (${err.extensionPath}): ${err.error}\n`);
84
- },
85
- });
86
206
 
87
- // Always subscribe to enable session persistence via _handleAgentEvent
88
- session.subscribe(event => {
89
- // In JSON mode, output all events
207
+ // Set up extensions for print mode (no UI, no command context).
208
+ await initializeExtensions(session, {
209
+ reportSendError: (action, err) => {
210
+ process.stderr.write(
211
+ `Extension ${action === "extension_send" ? "sendMessage" : "sendUserMessage"} failed: ${err.message}\n`,
212
+ );
213
+ },
214
+ reportRuntimeError: err => {
215
+ process.stderr.write(`Extension error (${err.extensionPath}): ${err.error}\n`);
216
+ },
217
+ });
218
+
219
+ // AgentSession persists events internally. Print mode only needs a listener
220
+ // when it must render the JSON event stream.
90
221
  if (mode === "json") {
91
- process.stdout.write(`${JSON.stringify(event)}\n`);
222
+ unsubscribe = session.subscribe(event => {
223
+ stdout.write(`${JSON.stringify(event)}\n`);
224
+ });
92
225
  }
93
- });
94
226
 
95
- // Send initial message with attachments
96
- if (initialMessage !== undefined) {
97
- await logger.time("print:prompt:initial", () => session.prompt(initialMessage, { images: initialImages }));
98
- }
227
+ // Send initial message with attachments.
228
+ if (initialMessage !== undefined) {
229
+ await logger.time("print:prompt:initial", () => session.prompt(initialMessage, { images: initialImages }));
230
+ }
99
231
 
100
- // Send remaining messages
101
- for (const message of messages) {
102
- await logger.time("print:prompt:next", () => session.prompt(message));
103
- }
232
+ // Send remaining messages.
233
+ for (const message of messages) {
234
+ await logger.time("print:prompt:next", () => session.prompt(message));
235
+ }
104
236
 
105
- // In text mode, output final response
106
- if (mode === "text") {
107
- const state = session.state;
108
- const lastMessage = state.messages.findLast(message => message.role === "assistant");
109
-
110
- if (lastMessage?.role === "assistant") {
111
- const assistantMsg = lastMessage as AssistantMessage;
112
-
113
- // Check for error/aborted — skip silent-abort (plan-mode compaction transition)
114
- if (
115
- (assistantMsg.stopReason === "error" || assistantMsg.stopReason === "aborted") &&
116
- !isSilentAbort(assistantMsg.errorMessage)
117
- ) {
118
- // Context-overflow is an expected, recoverable-in-principle condition — not
119
- // an opaque crash. Auto-compaction has already run inside session.prompt();
120
- // if we still land here the request could not be made to fit. In this
121
- // text-mode final-response path, surface an actionable diagnostic and a
122
- // distinct exit code so text-mode (`skc -p`) callers can detect context
123
- // exhaustion instead of parsing the raw provider error string. (JSON mode
124
- // does not reach this branch and is intentionally out of scope.)
125
- const isOverflow =
126
- assistantMsg.stopReason === "error" && isContextOverflow(assistantMsg, session.model?.contextWindow);
127
- const errorLine = isOverflow
128
- ? formatContextOverflowError(assistantMsg, session.autoCompactionEnabled)
129
- : sanitizeText(assistantMsg.errorMessage || `Request ${assistantMsg.stopReason}`);
130
- const exitCode = isOverflow ? CONTEXT_OVERFLOW_EXIT_CODE : 1;
131
- const flushed = process.stderr.write(`${errorLine}\n`);
132
- // When the caller owns finalization (RLM autonomous), return instead of
133
- // exiting so its cleanup runs; the caller surfaces a non-zero exit itself.
134
- if (!options.suppressProcessExit) {
135
- if (flushed) {
136
- process.exit(exitCode);
137
- } else {
138
- process.stderr.once("drain", () => process.exit(exitCode));
139
- }
237
+ // In text mode, output final response.
238
+ if (mode === "text") {
239
+ const lastMessage = session.state.messages.findLast(message => message.role === "assistant");
240
+ if (lastMessage?.role === "assistant") {
241
+ const assistantMsg = lastMessage as AssistantMessage;
242
+ let printContent = true;
243
+
244
+ // Check for error/aborted — skip silent-abort (plan-mode compaction transition).
245
+ if (
246
+ (assistantMsg.stopReason === "error" || assistantMsg.stopReason === "aborted") &&
247
+ !isSilentAbort(assistantMsg.errorMessage)
248
+ ) {
249
+ const isOverflow =
250
+ assistantMsg.stopReason === "error" && isContextOverflow(assistantMsg, session.model?.contextWindow);
251
+ const errorLine = isOverflow
252
+ ? formatContextOverflowError(assistantMsg, session.autoCompactionEnabled)
253
+ : sanitizeText(assistantMsg.errorMessage || `Request ${assistantMsg.stopReason}`);
254
+ const exitCode = isOverflow ? CONTEXT_OVERFLOW_EXIT_CODE : 1;
255
+
256
+ if (!options.suppressProcessExit) process.exitCode = exitCode;
257
+ await writeStderrAndQuiesce(`${errorLine}\n`);
258
+ printContent = false;
140
259
  }
141
- }
142
260
 
143
- if (
144
- assistantMsg.errorMessage &&
145
- assistantMsg.stopReason !== "error" &&
146
- assistantMsg.stopReason !== "aborted"
147
- ) {
148
- process.stderr.write(`${sanitizeText(assistantMsg.errorMessage)}\n`);
149
- }
261
+ if (
262
+ assistantMsg.errorMessage &&
263
+ assistantMsg.stopReason !== "error" &&
264
+ assistantMsg.stopReason !== "aborted"
265
+ ) {
266
+ await writeStderrAndQuiesce(`${sanitizeText(assistantMsg.errorMessage)}\n`);
267
+ }
150
268
 
151
- // Output text content
152
- for (const content of assistantMsg.content) {
153
- if (content.type === "text") {
154
- process.stdout.write(`${sanitizeText(content.text)}\n`);
269
+ if (printContent) {
270
+ for (const content of assistantMsg.content) {
271
+ if (content.type === "text") stdout.write(`${sanitizeText(content.text)}\n`);
272
+ }
155
273
  }
156
274
  }
157
275
  }
158
- }
159
276
 
160
- // Ensure stdout is fully flushed before returning
161
- // This prevents race conditions where the process exits before all output is written
162
- await new Promise<void>((resolve, reject) => {
163
- process.stdout.write("", err => {
164
- if (err) reject(err);
165
- else resolve();
166
- });
167
- });
277
+ // Observe callback and stream errors from every preceding print-mode write.
278
+ await stdout.flush();
279
+ } catch (error) {
280
+ failures.push(error);
281
+ } finally {
282
+ // The JSON subscriber remains live while disposal emits its final events.
283
+ try {
284
+ await session.dispose();
285
+ } catch (error) {
286
+ failures.push(error);
287
+ }
288
+
289
+ // Disposal may have written JSON or delivered a late stdout error.
290
+ try {
291
+ await stdout.flush();
292
+ } catch (error) {
293
+ failures.push(error);
294
+ }
295
+
296
+ try {
297
+ unsubscribe?.();
298
+ } catch (error) {
299
+ failures.push(error);
300
+ } finally {
301
+ stdout.dispose();
302
+ }
303
+ }
168
304
 
169
- await session.dispose();
305
+ throwCollectedErrors(failures);
170
306
  }
@@ -3,7 +3,13 @@
3
3
  *
4
4
  * Spawns the agent in RPC mode and provides a typed API for all operations.
5
5
  */
6
- import type { AgentEvent, AgentMessage, AgentToolResult, ThinkingLevel } from "@sayknow-cli/agent-core";
6
+ import {
7
+ type AgentEvent,
8
+ type AgentMessage,
9
+ type AgentToolResult,
10
+ type ResolvedThinkingLevel,
11
+ ThinkingLevel,
12
+ } from "@sayknow-cli/agent-core";
7
13
  import type { CompactionResult } from "@sayknow-cli/agent-core/compaction";
8
14
  import type { ImageContent, Model } from "@sayknow-cli/ai";
9
15
  import { isRecord, ptree, readJsonl } from "@sayknow-cli/utils";
@@ -64,6 +70,12 @@ export interface RpcClientOptions {
64
70
 
65
71
  export type ModelInfo = Pick<Model, "provider" | "id" | "contextWindow" | "reasoning" | "thinking">;
66
72
 
73
+ export type RpcDefaultModelSelection = {
74
+ readonly provider: string;
75
+ readonly modelId: string;
76
+ readonly thinkingLevel: ResolvedThinkingLevel;
77
+ };
78
+
67
79
  export type RpcEventListener = (event: AgentEvent) => void;
68
80
  export type RpcSessionEventListener = (event: AgentSessionEvent) => void;
69
81
 
@@ -94,6 +106,7 @@ export function defineRpcClientTool<
94
106
  }
95
107
 
96
108
  const agentWireEventTypes = new Set<AgentWireEventType>(AGENT_WIRE_EVENT_TYPES);
109
+ const DEFAULT_MODEL_SELECTION_TIMEOUT_MS = 600_000;
97
110
  const coreAgentEventTypes = new Set<AgentEvent["type"]>([
98
111
  "agent_start",
99
112
  "agent_end",
@@ -107,6 +120,29 @@ const coreAgentEventTypes = new Set<AgentEvent["type"]>([
107
120
  "tool_execution_end",
108
121
  ]);
109
122
 
123
+ function isResolvedThinkingLevel(value: unknown): value is ResolvedThinkingLevel {
124
+ return (
125
+ value === ThinkingLevel.Off ||
126
+ value === ThinkingLevel.Minimal ||
127
+ value === ThinkingLevel.Low ||
128
+ value === ThinkingLevel.Medium ||
129
+ value === ThinkingLevel.High ||
130
+ value === ThinkingLevel.XHigh ||
131
+ value === ThinkingLevel.Max
132
+ );
133
+ }
134
+
135
+ function isDefaultModelSelection(value: unknown): value is RpcDefaultModelSelection {
136
+ return (
137
+ isRecord(value) &&
138
+ typeof value.provider === "string" &&
139
+ value.provider.trim().length > 0 &&
140
+ typeof value.modelId === "string" &&
141
+ value.modelId.trim().length > 0 &&
142
+ isResolvedThinkingLevel(value.thinkingLevel)
143
+ );
144
+ }
145
+
110
146
  function isRpcResponse(value: unknown): value is RpcResponse {
111
147
  if (!isRecord(value)) return false;
112
148
  if (value.type !== "response") return false;
@@ -626,6 +662,27 @@ export class RpcClient {
626
662
  return this.#getData(response);
627
663
  }
628
664
 
665
+ async setDefaultModelSelection(
666
+ provider: string,
667
+ modelId: string,
668
+ thinkingLevel: ResolvedThinkingLevel,
669
+ ): Promise<RpcDefaultModelSelection> {
670
+ const response = await this.#send(
671
+ {
672
+ type: "set_default_model_selection",
673
+ provider,
674
+ modelId,
675
+ thinkingLevel,
676
+ },
677
+ DEFAULT_MODEL_SELECTION_TIMEOUT_MS,
678
+ );
679
+ if (!response.success) return this.#getData(response);
680
+ if (response.command !== "set_default_model_selection" || !isDefaultModelSelection(response.data)) {
681
+ throw new Error("Invalid set_default_model_selection response");
682
+ }
683
+ return this.#getData(response);
684
+ }
685
+
629
686
  /**
630
687
  * Cycle to next model.
631
688
  */