@sayknow-cli/coding-agent 0.3.1 → 0.3.4

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 (216) hide show
  1. package/bin/skc.js +4 -0
  2. package/dist/types/cli/mcp-cli.d.ts +25 -0
  3. package/dist/types/cli/plugin-cli.d.ts +2 -0
  4. package/dist/types/cli.d.ts +6 -0
  5. package/dist/types/commands/mcp.d.ts +70 -0
  6. package/dist/types/commands/plugin.d.ts +6 -0
  7. package/dist/types/commands/session.d.ts +6 -0
  8. package/dist/types/config/keybindings.d.ts +2 -2
  9. package/dist/types/config/model-profile-activation.d.ts +8 -1
  10. package/dist/types/config/model-profiles.d.ts +2 -2
  11. package/dist/types/config/model-registry.d.ts +3 -3
  12. package/dist/types/config/models-config-schema.d.ts +0 -5
  13. package/dist/types/config/settings-schema.d.ts +79 -68
  14. package/dist/types/deep-interview/plaintext-gate-guard.d.ts +11 -0
  15. package/dist/types/export/html/template.generated.d.ts +1 -1
  16. package/dist/types/extensibility/skc-plugins/compiler.d.ts +19 -0
  17. package/dist/types/extensibility/skc-plugins/constrained-hooks.d.ts +29 -0
  18. package/dist/types/extensibility/skc-plugins/index.d.ts +9 -0
  19. package/dist/types/extensibility/skc-plugins/injection.d.ts +9 -0
  20. package/dist/types/extensibility/skc-plugins/installer.d.ts +13 -0
  21. package/dist/types/extensibility/skc-plugins/mcp-policy.d.ts +26 -0
  22. package/dist/types/extensibility/skc-plugins/observability.d.ts +27 -0
  23. package/dist/types/extensibility/skc-plugins/prompt-appendix.d.ts +16 -0
  24. package/dist/types/extensibility/skc-plugins/registry.d.ts +32 -0
  25. package/dist/types/extensibility/skc-plugins/runtime-adapters.d.ts +64 -0
  26. package/dist/types/extensibility/skc-plugins/session-validation.d.ts +42 -0
  27. package/dist/types/extensibility/skc-plugins/types.d.ts +158 -2
  28. package/dist/types/extensibility/skc-plugins/validation.d.ts +8 -1
  29. package/dist/types/i18n/messages/en.d.ts +0 -1
  30. package/dist/types/main.d.ts +2 -0
  31. package/dist/types/modes/components/custom-editor.d.ts +1 -1
  32. package/dist/types/modes/components/model-selector.d.ts +8 -0
  33. package/dist/types/modes/components/settings-defs.d.ts +0 -1
  34. package/dist/types/modes/components/status-line/git-utils.d.ts +6 -0
  35. package/dist/types/modes/theme/defaults/index.d.ts +99 -0
  36. package/dist/types/modes/theme/theme.d.ts +1 -1
  37. package/dist/types/notifications/html-format.d.ts +11 -0
  38. package/dist/types/notifications/index.d.ts +149 -1
  39. package/dist/types/notifications/lifecycle-commands.d.ts +60 -0
  40. package/dist/types/notifications/lifecycle-control-runtime.d.ts +98 -0
  41. package/dist/types/notifications/lifecycle-orchestrator.d.ts +144 -0
  42. package/dist/types/notifications/operator-runtime.d.ts +52 -0
  43. package/dist/types/notifications/rate-limit-pool.d.ts +2 -0
  44. package/dist/types/notifications/recent-activity.d.ts +35 -0
  45. package/dist/types/notifications/telegram-daemon.d.ts +114 -16
  46. package/dist/types/notifications/telegram-reference.d.ts +3 -1
  47. package/dist/types/notifications/topic-registry.d.ts +12 -9
  48. package/dist/types/runtime-mcp/types.d.ts +7 -0
  49. package/dist/types/sdk.d.ts +2 -0
  50. package/dist/types/session/agent-session.d.ts +14 -4
  51. package/dist/types/session/blob-store.d.ts +25 -0
  52. package/dist/types/session/session-manager.d.ts +57 -0
  53. package/dist/types/skc-runtime/launch-tmux.d.ts +1 -0
  54. package/dist/types/skc-runtime/psmux-detect.d.ts +78 -0
  55. package/dist/types/skc-runtime/state-renderer.d.ts +5 -0
  56. package/dist/types/skc-runtime/team-runtime.d.ts +2 -0
  57. package/dist/types/skc-runtime/tmux-common.d.ts +30 -2
  58. package/dist/types/skc-runtime/tmux-sessions.d.ts +18 -0
  59. package/dist/types/skc-runtime/ultragoal-guard.d.ts +37 -1
  60. package/dist/types/skc-runtime/ultragoal-runtime.d.ts +80 -0
  61. package/dist/types/slash-commands/helpers/fast-status-report.d.ts +6 -0
  62. package/dist/types/system-prompt.d.ts +2 -0
  63. package/dist/types/task/executor.d.ts +9 -1
  64. package/dist/types/tools/browser/tab-supervisor.d.ts +31 -0
  65. package/dist/types/tools/composer-bash-policy.d.ts +14 -0
  66. package/dist/types/tools/computer-gc.d.ts +23 -0
  67. package/dist/types/tools/cron.d.ts +36 -61
  68. package/dist/types/tools/index.d.ts +3 -2
  69. package/dist/types/tools/resource-gc.d.ts +54 -0
  70. package/dist/types/utils/changelog.d.ts +1 -0
  71. package/dist/types/web/insane/url-guard.d.ts +6 -3
  72. package/dist/types/web/scrapers/types.d.ts +5 -0
  73. package/dist/types/web/scrapers/utils.d.ts +7 -1
  74. package/dist/types/web/search/index.d.ts +1 -0
  75. package/dist/types/web/search/providers/utils.d.ts +11 -4
  76. package/package.json +11 -9
  77. package/scripts/g004-tmux-smoke.ts +100 -0
  78. package/scripts/g005-daemon-smoke.ts +180 -0
  79. package/scripts/g011-daemon-path-smoke.ts +153 -0
  80. package/src/cli/args.ts +0 -1
  81. package/src/cli/fast-help.ts +29 -2
  82. package/src/cli/mcp-cli.ts +272 -0
  83. package/src/cli/plugin-cli.ts +66 -3
  84. package/src/cli/web-search-cli.ts +5 -0
  85. package/src/cli.ts +30 -12
  86. package/src/commands/mcp.ts +117 -0
  87. package/src/commands/plugin.ts +4 -0
  88. package/src/commands/session.ts +18 -0
  89. package/src/config/keybindings.ts +2 -2
  90. package/src/config/model-profile-activation.ts +62 -8
  91. package/src/config/model-profiles.ts +3 -4
  92. package/src/config/model-registry.ts +3 -6
  93. package/src/config/models-config-schema.ts +1 -1
  94. package/src/config/settings-schema.ts +88 -103
  95. package/src/deep-interview/plaintext-gate-guard.ts +94 -0
  96. package/src/defaults/skc/extensions/grok-cli-vendor/biome.json +1 -1
  97. package/src/defaults/skc/skills/deep-interview/SKILL.md +7 -6
  98. package/src/defaults/skc/skills/team/SKILL.md +5 -3
  99. package/src/defaults/skc/skills/ultragoal/SKILL.md +41 -13
  100. package/src/export/html/index.ts +2 -2
  101. package/src/export/html/template.generated.ts +1 -1
  102. package/src/export/html/template.js +0 -12
  103. package/src/extensibility/extensions/runner.ts +1 -0
  104. package/src/extensibility/skc-plugins/compiler.ts +351 -0
  105. package/src/extensibility/skc-plugins/constrained-hooks.ts +170 -0
  106. package/src/extensibility/skc-plugins/index.ts +9 -0
  107. package/src/extensibility/skc-plugins/injection.ts +109 -0
  108. package/src/extensibility/skc-plugins/installer.ts +434 -0
  109. package/src/extensibility/skc-plugins/loader.ts +3 -1
  110. package/src/extensibility/skc-plugins/mcp-policy.ts +239 -0
  111. package/src/extensibility/skc-plugins/observability.ts +84 -0
  112. package/src/extensibility/skc-plugins/paths.ts +1 -1
  113. package/src/extensibility/skc-plugins/prompt-appendix.ts +109 -0
  114. package/src/extensibility/skc-plugins/registry.ts +180 -0
  115. package/src/extensibility/skc-plugins/runtime-adapters.ts +234 -0
  116. package/src/extensibility/skc-plugins/schema.ts +250 -20
  117. package/src/extensibility/skc-plugins/session-validation.ts +147 -0
  118. package/src/extensibility/skc-plugins/types.ts +199 -3
  119. package/src/extensibility/skc-plugins/validation.ts +80 -0
  120. package/src/extensibility/skills.ts +15 -0
  121. package/src/goals/tools/goal-tool.ts +14 -1
  122. package/src/hooks/skill-state.ts +57 -0
  123. package/src/i18n/messages/de.ts +0 -1
  124. package/src/i18n/messages/en.ts +0 -1
  125. package/src/i18n/messages/es.ts +0 -1
  126. package/src/i18n/messages/fr.ts +0 -1
  127. package/src/i18n/messages/ja.ts +0 -1
  128. package/src/i18n/messages/ko.ts +0 -1
  129. package/src/i18n/messages/zh.ts +0 -1
  130. package/src/internal-urls/docs-index.generated.ts +14 -11
  131. package/src/main.ts +14 -3
  132. package/src/modes/bridge/bridge-mode.ts +11 -0
  133. package/src/modes/components/assistant-message.ts +49 -1
  134. package/src/modes/components/custom-editor.ts +2 -0
  135. package/src/modes/components/footer.ts +2 -3
  136. package/src/modes/components/hook-editor.ts +1 -1
  137. package/src/modes/components/hook-selector.ts +67 -43
  138. package/src/modes/components/model-selector.ts +65 -12
  139. package/src/modes/components/settings-defs.ts +1 -2
  140. package/src/modes/components/settings-selector.ts +2 -7
  141. package/src/modes/components/status-line/git-utils.ts +25 -0
  142. package/src/modes/components/status-line.ts +10 -11
  143. package/src/modes/components/welcome.ts +2 -3
  144. package/src/modes/controllers/extension-ui-controller.ts +0 -27
  145. package/src/modes/controllers/selector-controller.ts +59 -11
  146. package/src/modes/interactive-mode.ts +4 -1
  147. package/src/modes/shared/agent-wire/scopes.ts +1 -1
  148. package/src/modes/theme/defaults/gruvbox-dark.json +99 -0
  149. package/src/modes/theme/defaults/index.ts +2 -0
  150. package/src/modes/theme/theme.ts +0 -4
  151. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  152. package/src/notifications/html-format.ts +38 -0
  153. package/src/notifications/index.ts +242 -12
  154. package/src/notifications/lifecycle-commands.ts +238 -0
  155. package/src/notifications/lifecycle-control-runtime.ts +405 -0
  156. package/src/notifications/lifecycle-orchestrator.ts +358 -0
  157. package/src/notifications/operator-runtime.ts +171 -0
  158. package/src/notifications/rate-limit-pool.ts +19 -0
  159. package/src/notifications/recent-activity.ts +132 -0
  160. package/src/notifications/telegram-daemon.ts +778 -257
  161. package/src/notifications/telegram-reference.ts +25 -7
  162. package/src/notifications/topic-registry.ts +23 -9
  163. package/src/prompts/agents/executor.md +2 -2
  164. package/src/prompts/system/system-prompt.md +2 -2
  165. package/src/prompts/tools/cron.md +5 -3
  166. package/src/prompts/tools/read.md +1 -1
  167. package/src/runtime-mcp/transports/stdio.ts +38 -4
  168. package/src/runtime-mcp/types.ts +7 -0
  169. package/src/sdk.ts +162 -10
  170. package/src/session/agent-session.ts +210 -74
  171. package/src/session/blob-store.ts +196 -8
  172. package/src/session/session-manager.ts +762 -12
  173. package/src/skc-runtime/launch-tmux.ts +68 -19
  174. package/src/skc-runtime/psmux-detect.ts +239 -0
  175. package/src/skc-runtime/state-renderer.ts +13 -0
  176. package/src/skc-runtime/team-runtime.ts +56 -23
  177. package/src/skc-runtime/tmux-common.ts +88 -4
  178. package/src/skc-runtime/tmux-sessions.ts +111 -9
  179. package/src/skc-runtime/ultragoal-guard.ts +192 -8
  180. package/src/skc-runtime/ultragoal-runtime.ts +296 -16
  181. package/src/slash-commands/builtin-registry.ts +23 -3
  182. package/src/slash-commands/helpers/fast-status-report.ts +13 -3
  183. package/src/slash-commands/helpers/parse.ts +2 -1
  184. package/src/system-prompt.ts +9 -0
  185. package/src/task/executor.ts +31 -7
  186. package/src/task/index.ts +2 -0
  187. package/src/tools/ask.ts +5 -1
  188. package/src/tools/bash.ts +9 -0
  189. package/src/tools/browser/tab-supervisor.ts +86 -2
  190. package/src/tools/composer-bash-policy.ts +96 -0
  191. package/src/tools/computer-gc.ts +66 -0
  192. package/src/tools/computer.ts +2 -0
  193. package/src/tools/cron.ts +75 -112
  194. package/src/tools/fetch.ts +18 -2
  195. package/src/tools/index.ts +5 -9
  196. package/src/tools/read.ts +25 -55
  197. package/src/tools/renderers.ts +0 -2
  198. package/src/tools/resource-gc.ts +291 -0
  199. package/src/tools/ultragoal-ask-guard.ts +7 -1
  200. package/src/utils/changelog.ts +8 -0
  201. package/src/web/insane/url-guard.ts +18 -14
  202. package/src/web/scrapers/types.ts +143 -45
  203. package/src/web/scrapers/utils.ts +70 -19
  204. package/src/web/search/index.ts +1 -0
  205. package/src/web/search/providers/utils.ts +22 -5
  206. package/vendor/insane-search/MANIFEST.json +3 -1
  207. package/vendor/insane-search/engine/__init__.py +14 -0
  208. package/vendor/insane-search/engine/content_safety.py +151 -0
  209. package/vendor/insane-search/engine/fetch_chain.py +32 -0
  210. package/vendor/insane-search/engine/tests/test_u8.py +216 -0
  211. package/dist/types/tools/inspect-image-renderer.d.ts +0 -26
  212. package/dist/types/tools/inspect-image.d.ts +0 -31
  213. package/src/prompts/tools/inspect-image-system.md +0 -20
  214. package/src/prompts/tools/inspect-image.md +0 -32
  215. package/src/tools/inspect-image-renderer.ts +0 -103
  216. package/src/tools/inspect-image.ts +0 -172
@@ -50,7 +50,11 @@ import {
50
50
  type SummaryOptions,
51
51
  shouldCompact,
52
52
  } from "@sayknow-cli/agent-core/compaction";
53
- import { DEFAULT_PRUNE_CONFIG, pruneToolOutputs } from "@sayknow-cli/agent-core/compaction/pruning";
53
+ import {
54
+ DEFAULT_PRUNE_CONFIG,
55
+ pruneAssistantToolArguments,
56
+ pruneToolOutputs,
57
+ } from "@sayknow-cli/agent-core/compaction/pruning";
54
58
  import type {
55
59
  AssistantMessage,
56
60
  Context,
@@ -127,7 +131,7 @@ import {
127
131
  import { type AsyncJob, type AsyncJobDeliveryState, AsyncJobManager } from "../async";
128
132
  import { reset as resetCapabilities } from "../capability";
129
133
  import type { Rule } from "../capability/rule";
130
- import { MODEL_ROLE_IDS, type ModelRegistry } from "../config/model-registry";
134
+ import { MODEL_ROLE_IDS, type ModelRegistry, SKC_MODEL_ASSIGNMENT_TARGETS } from "../config/model-registry";
131
135
  import {
132
136
  extractExplicitThinkingSelector,
133
137
  formatModelSelectorValue,
@@ -246,6 +250,7 @@ import { releaseTabsForOwner } from "../tools/browser/tab-supervisor";
246
250
  import type { CheckpointState } from "../tools/checkpoint";
247
251
  import { outputMeta, wrapToolWithMetaNotice } from "../tools/output-meta";
248
252
  import { normalizeLocalScheme, resolveToCwd } from "../tools/path-utils";
253
+ import { registerResourceGcSession } from "../tools/resource-gc";
249
254
  import { getLatestTodoPhasesFromEntries, type TodoItem, type TodoPhase } from "../tools/todo-write";
250
255
  import { ToolAbortError, ToolError } from "../tools/tool-errors";
251
256
  import { clampTimeout } from "../tools/tool-timeouts";
@@ -421,6 +426,13 @@ export interface AgentSessionConfig {
421
426
  * **MUST NOT** dispose it on their own teardown.
422
427
  */
423
428
  ownedAsyncJobManager?: AsyncJobManager;
429
+ /**
430
+ * MCPManager whose lifecycle this session owns (top-level sessions that
431
+ * connected plugin-bundle MCP servers). Only the owned manager is
432
+ * disconnected on dispose; subagents and callers that merely observe the
433
+ * process-global manager **MUST NOT** dispose it on their own teardown.
434
+ */
435
+ ownedMcpManager?: MCPManager;
424
436
  /** Optional fork-context seed used to initialize a child session before its first prompt. */
425
437
  forkContextSeed?: ForkContextSeed;
426
438
  /** Optional provider state override. Fork-context children should omit this by default. */
@@ -953,11 +965,14 @@ export class AgentSession {
953
965
  // Python execution state
954
966
  #evalAbortControllers = new Set<AbortController>();
955
967
  #evalKernelOwnerId: string;
968
+ /** Idempotent unregister handle for this session's resource-GC registration. */
969
+ #unregisterResourceGc?: () => void;
956
970
  /**
957
971
  * AsyncJobManager owned by this session (top-level only). Subagents leave
958
972
  * this undefined and **MUST NOT** dispose the global instance on teardown.
959
973
  */
960
974
  readonly #ownedAsyncJobManager: AsyncJobManager | undefined;
975
+ readonly #ownedMcpManager: MCPManager | undefined;
961
976
  #pendingPythonMessages: PythonExecutionMessage[] = [];
962
977
  #activeEvalExecutions = new Set<Promise<unknown>>();
963
978
  #evalExecutionDisposing = false;
@@ -1087,6 +1102,16 @@ export class AgentSession {
1087
1102
  #lastSuccessfulYieldToolCallId: string | undefined = undefined;
1088
1103
  #promptGeneration = 0;
1089
1104
  #providerSessionState = new Map<string, ProviderSessionState>();
1105
+ /**
1106
+ * Provider keys for which the Anthropic fast-mode auto-fallback fired this
1107
+ * session (the provider rejected `speed:"fast"` and we retried without it).
1108
+ * Provider/API-session scoped — matching the provider's own per-session
1109
+ * `fastModeDisabled` flag — NOT model-keyed. Transient (never persisted): it
1110
+ * suppresses the current-model fast indicator and dedups the one-time warning
1111
+ * WITHOUT mutating the user's intended `serviceTier`, so task subagents still
1112
+ * inherit the intended tier and a different provider still shows fast.
1113
+ */
1114
+ #fastModeAutoDisabledProviderKeys = new Set<string>();
1090
1115
  #hindsightSessionState: HindsightSessionState | undefined = undefined;
1091
1116
  readonly rawSseDebugBuffer: RawSseDebugBuffer;
1092
1117
 
@@ -1158,9 +1183,19 @@ export class AgentSession {
1158
1183
  this.sessionManager = config.sessionManager;
1159
1184
  this.settings = config.settings;
1160
1185
  this.taskDepth = config.taskDepth ?? 0;
1186
+ // Register this session with the process-wide resource GC (idle/RSS browser-tab eviction
1187
+ // + stale screenshot cleanup). Session-keyed so concurrent sessions share one timer safely.
1188
+ const resourceGcSessionId = this.sessionManager.getSessionId();
1189
+ if (resourceGcSessionId) {
1190
+ this.#unregisterResourceGc = registerResourceGcSession({
1191
+ sessionId: resourceGcSessionId,
1192
+ settings: this.settings,
1193
+ });
1194
+ }
1161
1195
  // Power assertions are taken per turn (see #beginInFlight); nothing acquired here.
1162
1196
  this.#evalKernelOwnerId = config.evalKernelOwnerId ?? `agent-session:${Snowflake.next()}`;
1163
1197
  this.#ownedAsyncJobManager = config.ownedAsyncJobManager;
1198
+ this.#ownedMcpManager = config.ownedMcpManager;
1164
1199
  this.#scopedModels = config.scopedModels ?? [];
1165
1200
  this.#thinkingLevel = config.thinkingLevel;
1166
1201
  this.#promptTemplates = config.promptTemplates ?? [];
@@ -1976,12 +2011,18 @@ export class AgentSession {
1976
2011
  const currentGrantsAnthropicPriority =
1977
2012
  this.serviceTier === "priority" || this.serviceTier === "claude-only";
1978
2013
  if (assistantMsg.disabledFeatures?.includes("priority") && currentGrantsAnthropicPriority) {
1979
- this.setServiceTier(undefined);
1980
- this.emitNotice(
1981
- "warning",
1982
- "Priority/fast mode rejected for this model; retried without it. Fast mode is now off.",
1983
- "priority",
1984
- );
2014
+ // The provider auto-dropped `speed:"fast"` for the current model's
2015
+ // provider this turn. Record a transient, provider-scoped marker
2016
+ // instead of clearing the user's intended tier, so task subagents
2017
+ // still inherit it and a different provider still gets fast mode.
2018
+ // Warn once per provider until the user re-arms with `/fast on`.
2019
+ if (this.#markFastModeAutoDisabledForCurrentModel()) {
2020
+ this.emitNotice(
2021
+ "warning",
2022
+ "Priority/fast mode rejected for this model; retried without it. Fast mode is off for this model until you re-enable it with /fast on.",
2023
+ "priority",
2024
+ );
2025
+ }
1985
2026
  }
1986
2027
  // Resolve TTSR resume gate before checking for new deferred injections.
1987
2028
  // Gate on #ttsrAbortPending, not stopReason: a non-TTSR abort (e.g. streaming
@@ -3240,10 +3281,14 @@ export class AgentSession {
3240
3281
  AsyncJobManager.setInstance(undefined);
3241
3282
  }
3242
3283
  }
3243
- const mcpManager = MCPManager.instance();
3244
- if (mcpManager) {
3245
- await mcpManager.disconnectAll();
3246
- if (MCPManager.instance() === mcpManager) {
3284
+ // Only disconnect the MCP manager THIS session owns (top-level sessions that
3285
+ // connected plugin-bundle MCP servers). Subagents and callers that merely
3286
+ // observe the process-global manager must never tear down a manager they do
3287
+ // not own. Mirrors the ownedAsyncJobManager rule above.
3288
+ const ownedMcpManager = this.#ownedMcpManager;
3289
+ if (ownedMcpManager) {
3290
+ await ownedMcpManager.disconnectAll();
3291
+ if (MCPManager.instance() === ownedMcpManager) {
3247
3292
  MCPManager.setInstance(undefined);
3248
3293
  }
3249
3294
  }
@@ -3252,6 +3297,8 @@ export class AgentSession {
3252
3297
  // browsers disconnect, headless close gracefully). Scoped by the session id the
3253
3298
  // browser tool tagged tabs with, so other live sessions' tabs are untouched.
3254
3299
  // No-op when this session opened no tabs. Failure is logged, not thrown.
3300
+ this.#unregisterResourceGc?.();
3301
+ this.#unregisterResourceGc = undefined;
3255
3302
  await releaseTabsForOwner(this.sessionManager.getSessionId()).catch((error: unknown) =>
3256
3303
  logger.warn("session dispose: releaseTabsForOwner failed", { error }),
3257
3304
  );
@@ -3291,6 +3338,8 @@ export class AgentSession {
3291
3338
  async disposeChildSubprocesses(timeoutMs = SIGNAL_TEARDOWN_TIMEOUT_MS): Promise<void> {
3292
3339
  const sessionId = this.sessionManager.getSessionId();
3293
3340
  const kernelOwnerId = this.#evalKernelOwnerId;
3341
+ this.#unregisterResourceGc?.();
3342
+ this.#unregisterResourceGc = undefined;
3294
3343
  const work = Promise.allSettled([
3295
3344
  // kill:true so a forced exit also reaps spawned-app Chrome we own (headless
3296
3345
  // always closes; connected/attached browsers only disconnect — never killed).
@@ -5198,6 +5247,19 @@ export class AgentSession {
5198
5247
  attribution: "user",
5199
5248
  timestamp: Date.now(),
5200
5249
  });
5250
+ // A live agent loop polls the steering queue at every tool/turn boundary
5251
+ // and consumes this message on its own. But when a steer is queued while no
5252
+ // loop is actively running — e.g. the session still reports busy only
5253
+ // because a finished prompt is unwinding (deferred agent_end / post-prompt
5254
+ // work) — nothing delivers it until the next explicit prompt or a
5255
+ // user-interrupt abort, so it stalls until the user presses Esc. Schedule a
5256
+ // continue so the steer is delivered promptly. A live loop (or an
5257
+ // already-drained queue) makes the scheduled continue a no-op.
5258
+ if (this.#canAutoContinueForSteer()) {
5259
+ this.#scheduleAgentContinue({
5260
+ shouldContinue: () => this.#canAutoContinueForSteer() && this.agent.hasQueuedSteering(),
5261
+ });
5262
+ }
5201
5263
  }
5202
5264
 
5203
5265
  /**
@@ -5242,6 +5304,21 @@ export class AgentSession {
5242
5304
  return last?.role === "assistant";
5243
5305
  }
5244
5306
 
5307
+ /**
5308
+ * Gate for idle / winding-down steer auto-continue. Unlike the follow-up gate
5309
+ * this checks `agent.state.isStreaming` (a live agent loop) rather than the
5310
+ * public `isStreaming` (which stays true while a finished prompt unwinds), so a
5311
+ * steer queued during the unwind window is still delivered. A live loop returns
5312
+ * false here because it polls the steering queue itself.
5313
+ */
5314
+ #canAutoContinueForSteer(): boolean {
5315
+ if (this.agent.state.isStreaming) return false;
5316
+ if (this.isRetrying) return false;
5317
+ const messages = this.agent.state.messages;
5318
+ const last = messages[messages.length - 1];
5319
+ return last?.role === "assistant";
5320
+ }
5321
+
5245
5322
  queueDeferredMessage(message: CustomMessage): void {
5246
5323
  this.#queueHiddenNextTurnMessage(message, true);
5247
5324
  }
@@ -5604,7 +5681,7 @@ export class AgentSession {
5604
5681
  }
5605
5682
 
5606
5683
  #syncTodoPhasesFromBranch(): void {
5607
- const phases = getLatestTodoPhasesFromEntries(this.sessionManager.getBranch());
5684
+ const phases = getLatestTodoPhasesFromEntries(this.sessionManager.getActivePathEntriesCanonical());
5608
5685
  // Strip completed/abandoned tasks — they were done in a previous run,
5609
5686
  // so they have no bearing on progress tracking for the new turn.
5610
5687
  for (const phase of phases) {
@@ -5613,6 +5690,42 @@ export class AgentSession {
5613
5690
  this.setTodoPhases(phases.filter(p => p.tasks.length > 0));
5614
5691
  }
5615
5692
 
5693
+ async #applyCompactionPostAppend(
5694
+ compactionEntryId: string,
5695
+ firstKeptEntryId: string,
5696
+ fromExtension?: boolean,
5697
+ ): Promise<CompactionEntry | undefined> {
5698
+ const eviction = this.sessionManager.evictCompactedContent(firstKeptEntryId, compactionEntryId);
5699
+ if (eviction.evictedEntries > 0) await this.sessionManager.rewriteEntries();
5700
+ const sessionContext = this.buildDisplaySessionContext();
5701
+ this.agent.replaceMessages(sessionContext.messages);
5702
+ this.#syncTodoPhasesFromBranch();
5703
+ this.#closeCodexProviderSessionsForHistoryRewrite();
5704
+
5705
+ // Get the saved compaction entry for the hook without materializing all entries.
5706
+ const savedCompactionEntry = this.sessionManager.getEntryForFidelity(compactionEntryId) as
5707
+ | CompactionEntry
5708
+ | undefined;
5709
+
5710
+ if (this.#extensionRunner && savedCompactionEntry) {
5711
+ await this.#extensionRunner.emit({
5712
+ type: "session_compact",
5713
+ compactionEntry: savedCompactionEntry,
5714
+ fromExtension: fromExtension ?? false,
5715
+ });
5716
+ }
5717
+
5718
+ return savedCompactionEntry;
5719
+ }
5720
+
5721
+ async applyCompactionPostAppendForTests(
5722
+ compactionEntryId: string,
5723
+ firstKeptEntryId: string,
5724
+ fromExtension?: boolean,
5725
+ ): Promise<CompactionEntry | undefined> {
5726
+ return this.#applyCompactionPostAppend(compactionEntryId, firstKeptEntryId, fromExtension);
5727
+ }
5728
+
5616
5729
  #cloneTodoPhases(phases: TodoPhase[]): TodoPhase[] {
5617
5730
  return phases.map(phase => ({
5618
5731
  name: phase.name,
@@ -6242,33 +6355,81 @@ export class AgentSession {
6242
6355
  return resolveServiceTier(this.#subagentServiceTier(), provider) === "priority";
6243
6356
  }
6244
6357
 
6358
+ /**
6359
+ * Provider/API-session key used to scope the fast-mode auto-disable marker.
6360
+ * Mirrors the provider's own per-session `fastModeDisabled` scope (the key in
6361
+ * {@link #providerSessionState} cleared by `clearAnthropicFastModeFallback`),
6362
+ * so the marker is provider-scoped, never model-keyed. Returns `undefined`
6363
+ * when no model/provider is selected.
6364
+ */
6365
+ #fastModeProviderKey(provider: string | undefined = this.model?.provider): string | undefined {
6366
+ return provider;
6367
+ }
6368
+
6369
+ /**
6370
+ * Record that the current model's provider had fast mode auto-dropped this
6371
+ * session. Returns `true` only when the provider key is newly marked, so the
6372
+ * caller emits the one-time warning exactly once per provider until re-arm.
6373
+ */
6374
+ #markFastModeAutoDisabledForCurrentModel(): boolean {
6375
+ const key = this.#fastModeProviderKey();
6376
+ if (key === undefined) return false;
6377
+ if (this.#fastModeAutoDisabledProviderKeys.has(key)) return false;
6378
+ this.#fastModeAutoDisabledProviderKeys.add(key);
6379
+ return true;
6380
+ }
6381
+
6382
+ /** True when `provider`'s fast mode was auto-disabled this session. */
6383
+ #isFastModeAutoDisabledForProvider(provider?: string): boolean {
6384
+ const key = this.#fastModeProviderKey(provider);
6385
+ return key !== undefined && this.#fastModeAutoDisabledProviderKeys.has(key);
6386
+ }
6387
+
6388
+ /**
6389
+ * Re-arm fast mode after an auto-disable: clear the provider's sticky
6390
+ * `fastModeDisabled` fallback flag and the session auto-disable markers so the
6391
+ * next request carries `speed:"fast"` again and a future rejection can warn
6392
+ * once more. Called on explicit re-enable (`/fast on`, re-arming tier change),
6393
+ * never by the transient Q1 auto-disable path.
6394
+ */
6395
+ #rearmFastMode(): void {
6396
+ clearAnthropicFastModeFallback(this.#providerSessionState);
6397
+ this.#fastModeAutoDisabledProviderKeys.clear();
6398
+ }
6399
+
6245
6400
  /**
6246
6401
  * True when the configured `serviceTier` resolves to `"priority"` for the
6247
- * *currently selected model's provider*. Returns false for scoped tiers
6248
- * that don't match (e.g. `"openai-only"` on an anthropic model) and when
6249
- * no model is selected.
6402
+ * *currently selected model's provider* AND fast mode was not auto-disabled
6403
+ * for that provider this session. This is the current-model EFFECTIVE
6404
+ * predicate (what the next request actually does); use {@link isFastForProvider}
6405
+ * for pure configured intent (e.g. subagent/`modelRoles` display rows).
6250
6406
  */
6251
6407
  isFastModeActive(): boolean {
6252
- return this.isFastForProvider(this.model?.provider);
6408
+ const provider = this.model?.provider;
6409
+ return this.isFastForProvider(provider) && !this.#isFastModeAutoDisabledForProvider(provider);
6253
6410
  }
6254
6411
 
6255
6412
  setServiceTier(serviceTier: ServiceTier | undefined): void {
6256
- if (this.serviceTier === serviceTier) return;
6257
- // Re-arming priority on Anthropic? Clear the per-session auto-fallback
6258
- // sticky disable so the next request actually carries `speed: "fast"`
6259
- // again. Without this, `/fast on` (or user switching to a tier that
6260
- // grants anthropic priority) after an auto-disable is a silent no-op
6261
- // and the warning notice fires every turn.
6413
+ // Re-arming a priority-granting tier always clears the per-session
6414
+ // auto-fallback sticky disable AND the auto-disable markers so the next
6415
+ // request carries `speed: "fast"` again — even when the tier is unchanged
6416
+ // (re-selecting the same tier is a deliberate re-arm), and before the
6417
+ // no-op early-return below.
6262
6418
  if (serviceTier === "priority" || serviceTier === "claude-only") {
6263
- clearAnthropicFastModeFallback(this.#providerSessionState);
6419
+ this.#rearmFastMode();
6264
6420
  }
6421
+ if (this.serviceTier === serviceTier) return;
6265
6422
  this.agent.serviceTier = serviceTier;
6266
6423
  this.sessionManager.appendServiceTierChange(serviceTier ?? null);
6267
6424
  }
6268
6425
 
6269
6426
  setFastMode(enabled: boolean): void {
6270
6427
  if (enabled && this.isFastModeEnabled()) {
6271
- // Already on under any scope — keep the user's scoped value.
6428
+ // Intent already grants fast mode under some scope — keep the user's
6429
+ // scoped value but still re-arm, so an explicit `/fast on` after a
6430
+ // provider auto-disable actually clears the sticky fallback + markers
6431
+ // (otherwise it is a silent no-op). No history append: intent is unchanged.
6432
+ this.#rearmFastMode();
6272
6433
  return;
6273
6434
  }
6274
6435
  this.setServiceTier(enabled ? "priority" : undefined);
@@ -6326,19 +6487,23 @@ export class AgentSession {
6326
6487
  async #pruneToolOutputs(): Promise<{ prunedCount: number; tokensSaved: number } | undefined> {
6327
6488
  const branchEntries = this.sessionManager.getBranch();
6328
6489
  const result = pruneToolOutputs(branchEntries, DEFAULT_PRUNE_CONFIG);
6329
- if (result.prunedCount === 0) {
6490
+ const argumentResult = pruneAssistantToolArguments(branchEntries, DEFAULT_PRUNE_CONFIG);
6491
+ const tokensSaved = result.tokensSaved + argumentResult.argumentTokensSaved;
6492
+ const prunedCount = result.prunedCount + argumentResult.argumentPrunedCount;
6493
+ if (prunedCount === 0) {
6330
6494
  return undefined;
6331
6495
  }
6332
6496
 
6333
6497
  // getBranch() returns materialized copies for blob-externalized entries, so
6334
6498
  // the pruning mutations must be written back into the canonical store.
6335
- this.sessionManager.applyEntryMessageUpdates(result.prunedEntries);
6499
+ const combined = [...result.prunedEntries, ...argumentResult.prunedEntries];
6500
+ this.sessionManager.applyEntryMessageUpdates(combined);
6336
6501
  await this.sessionManager.rewriteEntries();
6337
6502
  const sessionContext = this.buildDisplaySessionContext();
6338
6503
  this.agent.replaceMessages(sessionContext.messages);
6339
6504
  this.#syncTodoPhasesFromBranch();
6340
6505
  this.#closeCodexProviderSessionsForHistoryRewrite();
6341
- return result;
6506
+ return { prunedCount, tokensSaved };
6342
6507
  }
6343
6508
 
6344
6509
  /**
@@ -6456,7 +6621,7 @@ export class AgentSession {
6456
6621
  throw new CompactionCancelledError();
6457
6622
  }
6458
6623
 
6459
- this.sessionManager.appendCompaction(
6624
+ const compactionEntryId = this.sessionManager.appendCompaction(
6460
6625
  summary,
6461
6626
  shortSummary,
6462
6627
  firstKeptEntryId,
@@ -6465,24 +6630,7 @@ export class AgentSession {
6465
6630
  fromExtension,
6466
6631
  preserveData,
6467
6632
  );
6468
- const newEntries = this.sessionManager.getEntries();
6469
- const sessionContext = this.buildDisplaySessionContext();
6470
- this.agent.replaceMessages(sessionContext.messages);
6471
- this.#syncTodoPhasesFromBranch();
6472
- this.#closeCodexProviderSessionsForHistoryRewrite();
6473
-
6474
- // Get the saved compaction entry for the hook
6475
- const savedCompactionEntry = newEntries.find(e => e.type === "compaction" && e.summary === summary) as
6476
- | CompactionEntry
6477
- | undefined;
6478
-
6479
- if (this.#extensionRunner && savedCompactionEntry) {
6480
- await this.#extensionRunner.emit({
6481
- type: "session_compact",
6482
- compactionEntry: savedCompactionEntry,
6483
- fromExtension,
6484
- });
6485
- }
6633
+ await this.#applyCompactionPostAppend(compactionEntryId, firstKeptEntryId, fromExtension);
6486
6634
 
6487
6635
  const compactionResult: CompactionResult = {
6488
6636
  summary,
@@ -7480,11 +7628,14 @@ export class AgentSession {
7480
7628
  availableModels: Model[],
7481
7629
  currentModel: Model | undefined,
7482
7630
  ): ResolvedModelRoleValue {
7631
+ const target = SKC_MODEL_ASSIGNMENT_TARGETS[role as keyof typeof SKC_MODEL_ASSIGNMENT_TARGETS];
7483
7632
  const roleModelStr =
7484
- role === "default"
7485
- ? (this.settings.getModelRole("default") ??
7486
- (currentModel ? `${currentModel.provider}/${currentModel.id}` : undefined))
7487
- : this.settings.getModelRole(role);
7633
+ target?.settingsPath === "task.agentModelOverrides"
7634
+ ? this.settings.get("task.agentModelOverrides")[role]
7635
+ : role === "default"
7636
+ ? (this.settings.getModelRole("default") ??
7637
+ (currentModel ? `${currentModel.provider}/${currentModel.id}` : undefined))
7638
+ : this.settings.getModelRole(role);
7488
7639
 
7489
7640
  if (!roleModelStr) {
7490
7641
  return { model: undefined, thinkingLevel: undefined, explicitThinkingLevel: false, warning: undefined };
@@ -7969,7 +8120,7 @@ export class AgentSession {
7969
8120
  return;
7970
8121
  }
7971
8122
 
7972
- this.sessionManager.appendCompaction(
8123
+ const compactionEntryId = this.sessionManager.appendCompaction(
7973
8124
  summary,
7974
8125
  shortSummary,
7975
8126
  firstKeptEntryId,
@@ -7978,24 +8129,7 @@ export class AgentSession {
7978
8129
  fromExtension,
7979
8130
  preserveData,
7980
8131
  );
7981
- const newEntries = this.sessionManager.getEntries();
7982
- const sessionContext = this.buildDisplaySessionContext();
7983
- this.agent.replaceMessages(sessionContext.messages);
7984
- this.#syncTodoPhasesFromBranch();
7985
- this.#closeCodexProviderSessionsForHistoryRewrite();
7986
-
7987
- // Get the saved compaction entry for the hook
7988
- const savedCompactionEntry = newEntries.find(e => e.type === "compaction" && e.summary === summary) as
7989
- | CompactionEntry
7990
- | undefined;
7991
-
7992
- if (this.#extensionRunner && savedCompactionEntry) {
7993
- await this.#extensionRunner.emit({
7994
- type: "session_compact",
7995
- compactionEntry: savedCompactionEntry,
7996
- fromExtension,
7997
- });
7998
- }
8132
+ await this.#applyCompactionPostAppend(compactionEntryId, firstKeptEntryId, fromExtension);
7999
8133
 
8000
8134
  const result: CompactionResult = {
8001
8135
  summary,
@@ -9613,7 +9747,7 @@ export class AgentSession {
9613
9747
  cancelled: boolean;
9614
9748
  }> {
9615
9749
  const previousSessionFile = this.sessionFile;
9616
- const selectedEntry = this.sessionManager.getEntry(entryId);
9750
+ const selectedEntry = this.sessionManager.getEntryForFidelity(entryId);
9617
9751
 
9618
9752
  if (selectedEntry?.type !== "message" || selectedEntry.message.role !== "user") {
9619
9753
  throw new Error("Invalid entry ID for branching");
@@ -9711,7 +9845,7 @@ export class AgentSession {
9711
9845
  throw new Error("No model available for summarization");
9712
9846
  }
9713
9847
 
9714
- const targetEntry = this.sessionManager.getEntry(targetId);
9848
+ const targetEntry = this.sessionManager.getEntryForFidelity(targetId);
9715
9849
  if (!targetEntry) {
9716
9850
  throw new Error(`Entry ${targetId} not found`);
9717
9851
  }
@@ -9867,9 +10001,11 @@ export class AgentSession {
9867
10001
 
9868
10002
  for (const entry of entries) {
9869
10003
  if (entry.type !== "message") continue;
9870
- if (entry.message.role !== "user") continue;
10004
+ const fidelityEntry = this.sessionManager.getEntryForFidelity(entry.id);
10005
+ if (fidelityEntry?.type !== "message") continue;
10006
+ if (fidelityEntry.message.role !== "user") continue;
9871
10007
 
9872
- const text = this.#extractUserMessageText(entry.message.content);
10008
+ const text = this.#extractUserMessageText(fidelityEntry.message.content);
9873
10009
  if (text) {
9874
10010
  result.push({ entryId: entry.id, text });
9875
10011
  }