@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
@@ -15,17 +15,22 @@ import { prepareLaunchWorktree } from "../skc-runtime/launch-worktree";
15
15
  import {
16
16
  SKC_COORDINATOR_SESSION_ID_ENV,
17
17
  SKC_COORDINATOR_SESSION_STATE_FILE_ENV,
18
+ SKC_TMUX_OWNER_GENERATION_ENV,
18
19
  } from "../skc-runtime/session-state-sidecar";
19
20
 
20
- async function persistCoordinatorLaunchFailure(error: unknown, cwd: string): Promise<void> {
21
- const stateFile = process.env[SKC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim();
21
+ export async function persistCoordinatorLaunchFailure(
22
+ error: unknown,
23
+ cwd: string,
24
+ env: NodeJS.ProcessEnv = process.env,
25
+ ): Promise<void> {
26
+ const stateFile = env[SKC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim();
22
27
  if (!stateFile) return;
23
28
  const message = error instanceof Error ? error.message : String(error);
24
29
  const code = message.split(":", 1)[0] || "launch_failed";
25
30
  const now = new Date().toISOString();
26
31
  const payload = {
27
32
  schema_version: 1,
28
- session_id: process.env[SKC_COORDINATOR_SESSION_ID_ENV]?.trim() || null,
33
+ session_id: env[SKC_COORDINATOR_SESSION_ID_ENV]?.trim() || null,
29
34
  state: "errored",
30
35
  ready_for_input: false,
31
36
  updated_at: now,
@@ -45,6 +50,9 @@ async function persistCoordinatorLaunchFailure(error: unknown, cwd: string): Pro
45
50
  truncated: false,
46
51
  },
47
52
  error: { code, message, recoverable: true },
53
+ ...(env[SKC_COORDINATOR_SESSION_ID_ENV]?.trim()
54
+ ? { owner_generation: env[SKC_TMUX_OWNER_GENERATION_ENV] ?? null }
55
+ : {}),
48
56
  };
49
57
  await fs.mkdir(path.dirname(stateFile), { recursive: true });
50
58
  await Bun.write(stateFile, `${JSON.stringify(payload, null, 2)}\n`);
@@ -5,7 +5,7 @@ import { Args, Command, Flags } from "@sayknow-cli/utils/cli";
5
5
  import { type NotifyAction, type NotifyCommandArgs, runNotifyCliCommand } from "../cli/notify-cli";
6
6
  import { initTheme } from "../modes/theme/theme";
7
7
 
8
- const ACTIONS: NotifyAction[] = ["setup", "status", "daemon-internal"];
8
+ const ACTIONS: NotifyAction[] = ["setup", "status", "health", "test", "recovery", "daemon-internal"];
9
9
 
10
10
  export default class Notify extends Command {
11
11
  static description = "Configure Telegram notifications";
@@ -28,6 +28,8 @@ export default class Notify extends Command {
28
28
  token: Flags.string({ description: "Telegram bot token (non-interactive setup)" }),
29
29
  "chat-id": Flags.string({ description: "Telegram chat id to pair (non-interactive setup)" }),
30
30
  redact: Flags.boolean({ description: "Enable redaction of remote notification content" }),
31
+ probe: Flags.boolean({ description: "notify health: probe Telegram reachability (getMe)" }),
32
+ message: Flags.string({ description: "notify test: custom message body" }),
31
33
  "owner-id": Flags.string({ description: "Internal: daemon owner id" }),
32
34
  "agent-dir": Flags.string({ description: "Internal: agent dir for the daemon" }),
33
35
  };
@@ -53,6 +55,8 @@ export default class Notify extends Command {
53
55
  token: flags.token as string | undefined,
54
56
  chatId: (flags as Record<string, unknown>)["chat-id"] as string | undefined,
55
57
  redact: Boolean(flags.redact),
58
+ probe: Boolean(flags.probe),
59
+ message: flags.message as string | undefined,
56
60
  };
57
61
 
58
62
  if (action !== "daemon-internal") await initTheme();
@@ -145,7 +145,12 @@ export default class Session extends Command {
145
145
  }
146
146
 
147
147
  if (action === "force-close" || action === "force-remove") {
148
- const closed = forceCloseSkcTmuxSession(sessionName, process.env, flags["session-id"], flags["state-file"]);
148
+ const closed = await forceCloseSkcTmuxSession(
149
+ sessionName,
150
+ process.env,
151
+ flags["session-id"],
152
+ flags["state-file"],
153
+ );
149
154
  if (json) {
150
155
  writeJson({ ok: true, session: sessionJson(closed) });
151
156
  return;
@@ -52,6 +52,7 @@ interface AppKeybindings {
52
52
  "app.plan.toggle": true;
53
53
  "app.history.search": true;
54
54
  "app.stt.toggle": true;
55
+ "app.irc.sidebar.toggle": true;
55
56
  }
56
57
 
57
58
  export type AppKeybinding = keyof AppKeybindings;
@@ -214,6 +215,10 @@ export const KEYBINDINGS = {
214
215
  defaultKeys: "alt+h",
215
216
  description: "Toggle speech-to-text",
216
217
  },
218
+ "app.irc.sidebar.toggle": {
219
+ defaultKeys: "alt+i",
220
+ description: "Toggle IRC sidebar",
221
+ },
217
222
  } as const satisfies KeybindingDefinitions;
218
223
 
219
224
  /**
@@ -181,7 +181,7 @@ export function formatModelProfileCredentialError(profileName: string, providers
181
181
  return `Model profile "${profileName}" requires credentials for: ${providers.join(", ")}. Run /login and configure the missing provider(s), then retry.`;
182
182
  }
183
183
 
184
- function resolveModelProfileName(profileName: string, profiles: ReadonlyMap<string, unknown>): string {
184
+ export function resolveModelProfileName(profileName: string, profiles: ReadonlyMap<string, unknown>): string {
185
185
  // A retired-name alias is fallback-only: never shadow a profile that actually
186
186
  // exists under the requested name (e.g. a user-defined `codex-standard`).
187
187
  if (profiles.has(profileName)) return profileName;
@@ -70,22 +70,22 @@ const profile = (
70
70
 
71
71
  export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
72
72
  profile("codex-eco", ["openai-codex"], {
73
- default: "openai-codex/gpt-5.6-sol:medium",
74
- executor: "openai-codex/gpt-5.6-luna:low",
75
- planner: "openai-codex/gpt-5.6-luna:medium",
76
- critic: "openai-codex/gpt-5.6-luna:medium",
73
+ default: "openai-codex/gpt-5.6-terra:high",
74
+ executor: "openai-codex/gpt-5.6-luna:high",
75
+ planner: "openai-codex/gpt-5.6-terra:medium",
76
+ critic: "openai-codex/gpt-5.6-terra:high",
77
77
  architect: "openai-codex/gpt-5.6-sol:medium",
78
78
  }),
79
79
  profile("codex-medium", ["openai-codex"], {
80
- default: "openai-codex/gpt-5.6-sol:high",
81
- executor: "openai-codex/gpt-5.6-luna:medium",
82
- planner: "openai-codex/gpt-5.6-terra:medium",
83
- critic: "openai-codex/gpt-5.6-terra:high",
80
+ default: "openai-codex/gpt-5.6-sol:medium",
81
+ executor: "openai-codex/gpt-5.6-terra:xhigh",
82
+ planner: "openai-codex/gpt-5.6-terra:high",
83
+ critic: "openai-codex/gpt-5.6-terra:xhigh",
84
84
  architect: "openai-codex/gpt-5.6-sol:high",
85
85
  }),
86
86
  profile("codex-pro", ["openai-codex"], {
87
87
  default: "openai-codex/gpt-5.6-sol:xhigh",
88
- executor: "openai-codex/gpt-5.6-terra:medium",
88
+ executor: "openai-codex/gpt-5.6-terra:xhigh",
89
89
  planner: "openai-codex/gpt-5.6-sol:high",
90
90
  critic: "openai-codex/gpt-5.6-sol:xhigh",
91
91
  architect: "openai-codex/gpt-5.6-sol:max",
@@ -256,13 +256,13 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
256
256
  }),
257
257
  profile("opus-codex", ["anthropic", "openai-codex"], {
258
258
  default: "anthropic/claude-opus-4-8:xhigh",
259
- executor: "openai-codex/gpt-5.6-terra:low",
260
- planner: "openai-codex/gpt-5.6-terra:medium",
261
- critic: "openai-codex/gpt-5.6-terra:high",
259
+ executor: "openai-codex/gpt-5.6-terra:xhigh",
260
+ planner: "openai-codex/gpt-5.6-terra:high",
261
+ critic: "openai-codex/gpt-5.6-terra:xhigh",
262
262
  architect: "openai-codex/gpt-5.6-sol:xhigh",
263
263
  }),
264
264
  profile("codex-opencodego", ["openai-codex", "opencode-go"], {
265
- default: "openai-codex/gpt-5.6-sol:medium",
265
+ default: "openai-codex/gpt-5.6-terra:xhigh",
266
266
  executor: "opencode-go/deepseek-v4-pro",
267
267
  planner: "opencode-go/kimi-k2.6",
268
268
  critic: "opencode-go/mimo-v2.5-pro",
@@ -270,7 +270,7 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
270
270
  }),
271
271
  profile("fable-opus-codex", ["anthropic", "openai-codex"], {
272
272
  default: "anthropic/claude-fable-5:high",
273
- executor: "openai-codex/gpt-5.6-terra:high",
273
+ executor: "openai-codex/gpt-5.6-terra:xhigh",
274
274
  planner: "anthropic/claude-opus-4-8:medium",
275
275
  critic: "anthropic/claude-opus-4-8:high",
276
276
  architect: "openai-codex/gpt-5.6-sol:xhigh",
@@ -1,4 +1,5 @@
1
1
  import type { Effort } from "@sayknow-cli/ai/model-thinking";
2
+ import { PET_MODE_IDS, PET_SKIN_IDS, PET_SKINS } from "@sayknow-cli/tui/components/sayknow-pet";
2
3
  import { TASK_SIMPLE_MODES } from "../task/simple-mode";
3
4
  import { getThinkingLevelMetadata } from "../thinking-metadata";
4
5
  import { EDIT_MODES } from "../utils/edit-mode";
@@ -265,6 +266,7 @@ export const SETTINGS_SCHEMA = {
265
266
  "notifications.telegram.chatId": { type: "string", default: undefined },
266
267
  "notifications.telegram.rich.enabled": { type: "boolean", default: true },
267
268
  "notifications.telegram.richDraft.enabled": { type: "boolean", default: false },
269
+ "notifications.telegram.topics.nameTemplate": { type: "string", default: undefined },
268
270
  "notifications.discord.botToken": { type: "string", default: undefined },
269
271
  "notifications.discord.channelId": { type: "string", default: undefined },
270
272
  "notifications.slack.botToken": { type: "string", default: undefined },
@@ -275,6 +277,11 @@ export const SETTINGS_SCHEMA = {
275
277
  default: "lean",
276
278
  validate: (value: string) => value === "lean" || value === "verbose",
277
279
  },
280
+ "notifications.sessionScope": {
281
+ type: "string",
282
+ default: "all",
283
+ validate: (value: string) => value === "all" || value === "primary",
284
+ },
278
285
  "notifications.daemon.idleTimeoutMs": {
279
286
  type: "number",
280
287
  default: 60000,
@@ -535,6 +542,25 @@ export const SETTINGS_SCHEMA = {
535
542
  description: "Use the session name color for the editor border and status line gap",
536
543
  },
537
544
  },
545
+
546
+ "pet.mode": {
547
+ type: "enum",
548
+ values: PET_MODE_IDS,
549
+ default: "off",
550
+ ui: {
551
+ tab: "appearance",
552
+ label: "Sayknow Pet",
553
+ description: "16x16 real-pixel sayknow living beside the composer (sixel/kitty terminals)",
554
+ options: [
555
+ { value: "off", label: "Off", description: "No pet" },
556
+ ...PET_SKIN_IDS.map(id => ({
557
+ value: id,
558
+ label: PET_SKINS[id].label,
559
+ description: PET_SKINS[id].description,
560
+ })),
561
+ ],
562
+ },
563
+ },
538
564
  "statusLine.maxRows": {
539
565
  type: "number",
540
566
  default: 1,
@@ -1249,7 +1275,8 @@ export const SETTINGS_SCHEMA = {
1249
1275
  ui: {
1250
1276
  tab: "interaction",
1251
1277
  label: "Check for Updates",
1252
- description: "If false, skip update check",
1278
+ description:
1279
+ "At interactive startup, notify of newer versions; never install. Use `skc update` only for recognized Bun global, Windows npm, or bundled-installer binaries; source, linked, and unrecognized installs use their original method.",
1253
1280
  },
1254
1281
  },
1255
1282
 
@@ -2189,6 +2216,16 @@ export const SETTINGS_SCHEMA = {
2189
2216
  },
2190
2217
  },
2191
2218
 
2219
+ "irc.sidebar.enabled": {
2220
+ type: "boolean",
2221
+ default: true,
2222
+ ui: {
2223
+ tab: "tools",
2224
+ label: "IRC Sidebar",
2225
+ description: "Enable the IRC message sidebar (opens with the toggle key; starts closed)",
2226
+ },
2227
+ },
2228
+
2192
2229
  // Optional tools
2193
2230
 
2194
2231
  "renderMermaid.enabled": {
@@ -3619,6 +3656,9 @@ export interface NotificationsSettings {
3619
3656
  richDraft: {
3620
3657
  enabled: boolean;
3621
3658
  };
3659
+ topics: {
3660
+ nameTemplate: string | undefined;
3661
+ };
3622
3662
  };
3623
3663
  discord: {
3624
3664
  botToken: string | undefined;
@@ -56,6 +56,12 @@ export interface RawSettings {
56
56
  [key: string]: unknown;
57
57
  }
58
58
 
59
+ type SettingsPatch = {
60
+ readonly path: string;
61
+ readonly value: unknown;
62
+ readonly generation: number;
63
+ };
64
+
59
65
  export interface SettingsOptions {
60
66
  /** Current working directory for project settings discovery */
61
67
  cwd?: string;
@@ -208,12 +214,14 @@ export class Settings {
208
214
  /** Merged view (global + project + overrides) */
209
215
  #merged: RawSettings = {};
210
216
 
211
- /** Paths modified during this session (for partial save) */
212
- #modified = new Set<string>();
217
+ /** Latest dirty patch for each path, owned by its generation. */
218
+ #modified = new Map<string, SettingsPatch>();
219
+ #nextGeneration = 0;
213
220
 
214
221
  /** Pending save (debounced) */
215
222
  #saveTimer?: NodeJS.Timeout;
216
- #savePromise?: Promise<void>;
223
+ #saveTail: Promise<void> = Promise.resolve();
224
+ #globalModelRoleTail: Promise<void> = Promise.resolve();
217
225
 
218
226
  /** Whether to persist changes */
219
227
  #persist: boolean;
@@ -321,9 +329,13 @@ export class Settings {
321
329
  */
322
330
  set<P extends SettingPath>(path: P, value: SettingValue<P>): void {
323
331
  const prev = this.get(path);
324
- const segments = path.split(".");
325
- setByPath(this.#global, segments, value);
326
- this.#modified.add(path);
332
+ const patch: SettingsPatch = {
333
+ path,
334
+ value: structuredClone(value),
335
+ generation: ++this.#nextGeneration,
336
+ };
337
+ setByPath(this.#global, path.split("."), structuredClone(patch.value));
338
+ this.#modified.set(path, patch);
327
339
  this.#rebuildMerged();
328
340
  this.#queueSave();
329
341
 
@@ -367,12 +379,8 @@ export class Settings {
367
379
  clearTimeout(this.#saveTimer);
368
380
  this.#saveTimer = undefined;
369
381
  }
370
- if (this.#savePromise) {
371
- await this.#savePromise;
372
- }
373
- if (this.#modified.size > 0) {
374
- await this.#saveNow();
375
- }
382
+ const save = this.#modified.size > 0 ? this.#saveNow() : this.#saveTail;
383
+ await save;
376
384
  }
377
385
 
378
386
  /**
@@ -386,12 +394,8 @@ export class Settings {
386
394
  clearTimeout(this.#saveTimer);
387
395
  this.#saveTimer = undefined;
388
396
  }
389
- if (this.#savePromise) {
390
- await this.#savePromise;
391
- }
392
- if (this.#modified.size > 0) {
393
- await this.#saveNow({ throwOnError: true });
394
- }
397
+ const save = this.#modified.size > 0 ? this.#saveNow({ throwOnError: true }) : this.#saveTail;
398
+ await save;
395
399
  }
396
400
 
397
401
  async cloneForCwd(cwd: string): Promise<Settings> {
@@ -481,7 +485,6 @@ export class Settings {
481
485
  * Set a model role (helper for modelRoles record).
482
486
  */
483
487
  setModelRole(role: ModelRole | string, modelId: string): void {
484
- const current = shallowStringRecord(getByPath(this.#global, ["modelRoles"]));
485
488
  const runtimeOverrides = getByPath(this.#overrides, ["modelRoles"]);
486
489
  const updateRuntimeOverride =
487
490
  !!runtimeOverrides &&
@@ -489,12 +492,55 @@ export class Settings {
489
492
  !Array.isArray(runtimeOverrides) &&
490
493
  Object.hasOwn(runtimeOverrides, role);
491
494
 
492
- this.set("modelRoles", { ...current, [role]: modelId });
495
+ this.setGlobalModelRole(role, modelId);
493
496
 
494
497
  if (updateRuntimeOverride) {
495
498
  this.override("modelRoles", { ...shallowStringRecord(runtimeOverrides), [role]: modelId });
496
499
  }
497
500
  }
501
+
502
+ setGlobalModelRole(role: ModelRole | string, modelId: string | undefined): void {
503
+ const current = shallowStringRecord(getByPath(this.#global, ["modelRoles"]));
504
+ if (modelId === undefined) {
505
+ const { [role]: _removed, ...remaining } = current;
506
+ this.set("modelRoles", remaining);
507
+ return;
508
+ }
509
+ this.set("modelRoles", { ...current, [role]: modelId });
510
+ }
511
+
512
+ setGlobalModelRoleAndFlush(role: ModelRole | string, modelId: string | undefined): Promise<void> {
513
+ const transaction = this.#globalModelRoleTail.then(async () => {
514
+ const hadModelRoles = Object.hasOwn(this.#global, "modelRoles");
515
+ const previousModelRoles = structuredClone(this.#global.modelRoles);
516
+ const previousPatch = this.#modified.get("modelRoles");
517
+ this.setGlobalModelRole(role, modelId);
518
+ const generation = this.#modified.get("modelRoles")?.generation;
519
+ if (this.#saveTimer) {
520
+ clearTimeout(this.#saveTimer);
521
+ this.#saveTimer = undefined;
522
+ }
523
+ const save = this.#saveNow({ throwOnError: true });
524
+ try {
525
+ await save;
526
+ } catch (error) {
527
+ const currentPatch = this.#modified.get("modelRoles");
528
+ if (currentPatch?.generation === generation) {
529
+ if (hadModelRoles) this.#global.modelRoles = previousModelRoles;
530
+ else delete this.#global.modelRoles;
531
+ if (previousPatch) this.#modified.set("modelRoles", previousPatch);
532
+ else this.#modified.delete("modelRoles");
533
+ this.#rebuildMerged();
534
+ }
535
+ throw error;
536
+ }
537
+ });
538
+ this.#globalModelRoleTail = transaction.then(
539
+ () => undefined,
540
+ () => undefined,
541
+ );
542
+ return transaction;
543
+ }
498
544
  /**
499
545
  * Set an agent model override while keeping any live runtime override aligned.
500
546
  *
@@ -825,15 +871,10 @@ export class Settings {
825
871
  #queueSave(): void {
826
872
  if (!this.#persist || !this.#configPath) return;
827
873
 
828
- // Debounce: wait 100ms for more changes
829
- if (this.#saveTimer) {
830
- clearTimeout(this.#saveTimer);
831
- }
874
+ if (this.#saveTimer) clearTimeout(this.#saveTimer);
832
875
  this.#saveTimer = setTimeout(() => {
833
876
  this.#saveTimer = undefined;
834
- this.#saveNow().catch(err => {
835
- logger.warn("Settings: background save failed", { error: String(err) });
836
- });
877
+ void this.#saveNow();
837
878
  }, 100);
838
879
  }
839
880
 
@@ -841,30 +882,40 @@ export class Settings {
841
882
  if (!this.#persist || !this.#configPath || this.#modified.size === 0) return;
842
883
 
843
884
  const configPath = this.#configPath;
844
- const modifiedPaths = [...this.#modified];
845
- this.#modified.clear();
885
+ const patches = [...this.#modified.values()];
846
886
 
847
- try {
848
- await withFileLock(configPath, async () => {
849
- // Re-read to preserve external changes
887
+ const save = this.#saveTail.then(() =>
888
+ withFileLock(configPath, async () => {
850
889
  const current = await this.#loadYaml(configPath);
851
-
852
- // Apply only our modified paths
853
- for (const modPath of modifiedPaths) {
854
- const segments = modPath.split(".");
855
- const value = getByPath(this.#global, segments);
856
- setByPath(current, segments, value);
890
+ for (const patch of patches) {
891
+ setByPath(current, patch.path.split("."), patch.value);
892
+ }
893
+ await Bun.write(configPath, YAML.stringify(current, null, 2));
894
+ for (const patch of patches) {
895
+ if (this.#modified.get(patch.path)?.generation === patch.generation) {
896
+ this.#modified.delete(patch.path);
897
+ }
857
898
  }
858
-
859
- // Update our global with any external changes we preserved
860
899
  this.#global = current;
861
- await Bun.write(configPath, YAML.stringify(this.#global, null, 2));
862
- });
900
+ for (const patch of this.#modified.values()) {
901
+ setByPath(this.#global, patch.path.split("."), structuredClone(patch.value));
902
+ }
903
+ }),
904
+ );
905
+ this.#saveTail = save.then(
906
+ () => undefined,
907
+ () => undefined,
908
+ );
909
+
910
+ try {
911
+ await save;
863
912
  } catch (error) {
864
913
  logger.warn("Settings: save failed", { error: String(error) });
865
- // Re-add failed paths for retry
866
- for (const p of modifiedPaths) {
867
- this.#modified.add(p);
914
+ for (const patch of patches) {
915
+ const currentPatch = this.#modified.get(patch.path);
916
+ if (currentPatch?.generation === patch.generation) {
917
+ this.#modified.set(patch.path, patch);
918
+ }
868
919
  }
869
920
  if (options.throwOnError) {
870
921
  this.#rebuildMerged();
@@ -12,6 +12,7 @@ export const COORDINATOR_MCP_TOOL_NAMES = [
12
12
  "skc_coordinator_watch_events",
13
13
  "skc_coordinator_register_session",
14
14
  "skc_coordinator_start_session",
15
+ "skc_coordinator_stop_session",
15
16
  "skc_coordinator_send_prompt",
16
17
  "skc_coordinator_submit_question_answer",
17
18
  "skc_coordinator_read_turn",
@@ -0,0 +1,100 @@
1
+ import * as path from "node:path";
2
+ import { getAgentDir } from "@sayknow-cli/utils";
3
+ import { resolveModelProfileName } from "../config/model-profile-activation";
4
+ import { type ModelProfileDefinition, mergeModelProfiles } from "../config/model-profiles";
5
+ import { ModelsConfigFile } from "../config/model-registry";
6
+
7
+ /**
8
+ * Loads the merged built-in + custom model-profile registry the way the `skc`
9
+ * CLI resolves `--mpreset`, so coordinator MCP launches select the same
10
+ * authoritative profile the spawned child will activate. Custom profiles live
11
+ * in the shared models config, which the child inherits, so both sides agree.
12
+ */
13
+ export type CoordinatorModelProfileLoader = () =>
14
+ | Map<string, ModelProfileDefinition>
15
+ | Promise<Map<string, ModelProfileDefinition>>;
16
+
17
+ const MAX_ECHOED_MPRESET_LENGTH = 128;
18
+
19
+ /**
20
+ * Thrown by the default loader when `models.yml` exists but is invalid or
21
+ * unreadable. This lets the resolver fail closed with a distinct, stable reason
22
+ * instead of silently collapsing a broken registry to the built-ins-only set
23
+ * (which would misreport a caller's valid custom profile as unknown).
24
+ */
25
+ export class CoordinatorModelProfileRegistryError extends Error {
26
+ constructor(cause?: unknown) {
27
+ super("coordinator_model_profile_registry_error");
28
+ this.name = "CoordinatorModelProfileRegistryError";
29
+ if (cause !== undefined) (this as { cause?: unknown }).cause = cause;
30
+ }
31
+ }
32
+
33
+ export const loadCoordinatorModelProfiles: CoordinatorModelProfileLoader = () => {
34
+ const configFile = ModelsConfigFile.relocate(path.join(getAgentDir(), "models.yml"));
35
+ configFile.invalidate();
36
+ const loaded = configFile.tryLoad();
37
+ // A present-but-invalid config must fail closed; absence (`value` undefined
38
+ // with a non-error status) is fine and yields the built-in profiles only.
39
+ if (loaded.status === "error") throw new CoordinatorModelProfileRegistryError(loaded.error);
40
+ return mergeModelProfiles(loaded.value?.profiles);
41
+ };
42
+
43
+ function sortedProfileNames(profiles: ReadonlyMap<string, ModelProfileDefinition>): string[] {
44
+ return [...profiles.keys()].sort((left, right) => left.localeCompare(right));
45
+ }
46
+
47
+ export type CoordinatorMpresetResolution =
48
+ | { ok: true; mpreset: string | null }
49
+ | { ok: false; reason: "unknown_model_profile"; mpreset: string; available_profiles: string[] }
50
+ | { ok: false; reason: "model_profile_registry_error"; mpreset: string; available_profiles: string[] };
51
+
52
+ /**
53
+ * Resolve a coordinator `mpreset` argument against the merged profile registry.
54
+ *
55
+ * Only an absent (`undefined`/`null`) value is a no-op (`mpreset: null`); an
56
+ * explicit empty/whitespace string is a caller error and is rejected rather
57
+ * than silently launching at the default tier. Legacy aliases are canonicalized
58
+ * exactly like the CLI (e.g. `codex-standard` -> `codex-medium`) so coordinator
59
+ * selection stays in parity with `skc --mpreset <profile>`; the resolved value
60
+ * is the canonical profile name. Unknown names are rejected with the
61
+ * available-profile listing and never reach a spawned child command, and a
62
+ * broken registry fails closed with `model_profile_registry_error`.
63
+ */
64
+ export async function resolveCoordinatorMpreset(
65
+ raw: unknown,
66
+ loadProfiles: CoordinatorModelProfileLoader,
67
+ ): Promise<CoordinatorMpresetResolution> {
68
+ if (raw === undefined || raw === null) return { ok: true, mpreset: null };
69
+ const requested = typeof raw === "string" ? raw.trim() : "";
70
+ const echoed = requested.slice(0, MAX_ECHOED_MPRESET_LENGTH);
71
+ let profiles: Map<string, ModelProfileDefinition>;
72
+ try {
73
+ profiles = await loadProfiles();
74
+ } catch (error) {
75
+ if (error instanceof CoordinatorModelProfileRegistryError) {
76
+ return { ok: false, reason: "model_profile_registry_error", mpreset: echoed, available_profiles: [] };
77
+ }
78
+ throw error;
79
+ }
80
+ // Non-string input and explicit blank/whitespace strings can never name a
81
+ // profile; only absent/null (handled above) means "no selection".
82
+ if (typeof raw !== "string" || requested.length === 0) {
83
+ return {
84
+ ok: false,
85
+ reason: "unknown_model_profile",
86
+ mpreset: echoed,
87
+ available_profiles: sortedProfileNames(profiles),
88
+ };
89
+ }
90
+ const canonical = resolveModelProfileName(requested, profiles);
91
+ if (!profiles.has(canonical)) {
92
+ return {
93
+ ok: false,
94
+ reason: "unknown_model_profile",
95
+ mpreset: echoed,
96
+ available_profiles: sortedProfileNames(profiles),
97
+ };
98
+ }
99
+ return { ok: true, mpreset: canonical };
100
+ }
@@ -1,6 +1,12 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import { coordinatorMcpStateRoot, skcRoot } from "../skc-runtime/session-layout";
4
+ import {
5
+ DEFAULT_SESSION_IDLE_TTL_MS,
6
+ DEFAULT_SESSION_SWEEP_INTERVAL_MS,
7
+ MIN_SESSION_IDLE_TTL_MS,
8
+ MIN_SESSION_SWEEP_INTERVAL_MS,
9
+ } from "./session-reaper";
4
10
 
5
11
  export type CoordinatorMutationClass = "sessions" | "questions" | "reports";
6
12
 
@@ -16,6 +22,9 @@ export interface CoordinatorMcpConfig {
16
22
  namespace: CoordinatorNamespace;
17
23
  stateRoot: string;
18
24
  sessionCommand: string | null;
25
+ sessionIdleTtlMs: number;
26
+ sessionSweepIntervalMs: number;
27
+ forceStopEnabled: boolean;
19
28
  }
20
29
 
21
30
  export interface CoordinatorMutationRequest {
@@ -32,6 +41,16 @@ const LEGACY_MUTATION_CLASS_ALIASES = new Map<string, CoordinatorMutationClass>(
32
41
  ["report", "reports"],
33
42
  ]);
34
43
 
44
+ function parsePositiveIntMs(value: string | undefined, fallback: number, floor: number): number {
45
+ const parsed = Number.parseInt((value ?? "").trim(), 10);
46
+ if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
47
+ return Math.max(floor, parsed);
48
+ }
49
+
50
+ function parseBool(value: string | undefined): boolean {
51
+ return /^(1|true|yes|on)$/i.test((value ?? "").trim());
52
+ }
53
+
35
54
  function parseList(value: string | undefined): string[] {
36
55
  return (value ?? "")
37
56
  .split(/[\n,;:]+/)
@@ -98,6 +117,17 @@ export function buildCoordinatorMcpConfig(env: NodeJS.ProcessEnv = process.env):
98
117
  },
99
118
  stateRoot: path.resolve(stateRoot),
100
119
  sessionCommand: env.SKC_COORDINATOR_MCP_SESSION_COMMAND?.trim() || null,
120
+ sessionIdleTtlMs: parsePositiveIntMs(
121
+ env.SKC_COORDINATOR_MCP_SESSION_IDLE_TTL_MS,
122
+ DEFAULT_SESSION_IDLE_TTL_MS,
123
+ MIN_SESSION_IDLE_TTL_MS,
124
+ ),
125
+ sessionSweepIntervalMs: parsePositiveIntMs(
126
+ env.SKC_COORDINATOR_MCP_SESSION_SWEEP_INTERVAL_MS,
127
+ DEFAULT_SESSION_SWEEP_INTERVAL_MS,
128
+ MIN_SESSION_SWEEP_INTERVAL_MS,
129
+ ),
130
+ forceStopEnabled: parseBool(env.SKC_COORDINATOR_MCP_FORCE_STOP),
101
131
  };
102
132
  }
103
133