@sayknow-cli/coding-agent 0.3.5 → 0.3.7

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 (238) hide show
  1. package/README.md +76 -0
  2. package/dist/types/capability/mcp.d.ts +2 -0
  3. package/dist/types/cli/local-provider-smoke.d.ts +59 -0
  4. package/dist/types/cli/notify-cli.d.ts +1 -1
  5. package/dist/types/cli/telegram-cli.d.ts +13 -0
  6. package/dist/types/cli/update-cli.d.ts +24 -1
  7. package/dist/types/commands/local-provider.d.ts +34 -0
  8. package/dist/types/commands/telegram.d.ts +22 -0
  9. package/dist/types/config/keybindings.d.ts +12 -6
  10. package/dist/types/config/model-registry.d.ts +5 -0
  11. package/dist/types/config/models-config-schema.d.ts +5 -0
  12. package/dist/types/config/settings-schema.d.ts +284 -4
  13. package/dist/types/config/settings.d.ts +7 -0
  14. package/dist/types/config/telegram-autostart.d.ts +14 -0
  15. package/dist/types/config/telegram-env-bridge.d.ts +18 -0
  16. package/dist/types/coordinator-mcp/server.d.ts +8 -0
  17. package/dist/types/defaults/skc-defaults.d.ts +7 -0
  18. package/dist/types/discovery/cursor.d.ts +4 -1
  19. package/dist/types/discovery/index.d.ts +0 -1
  20. package/dist/types/discovery/windsurf.d.ts +3 -1
  21. package/dist/types/extensibility/extensions/types.d.ts +11 -0
  22. package/dist/types/i18n/messages/en.d.ts +1 -0
  23. package/dist/types/modes/components/custom-editor.d.ts +6 -1
  24. package/dist/types/modes/components/custom-model-preset-wizard.d.ts +1 -1
  25. package/dist/types/modes/components/hook-selector.d.ts +5 -0
  26. package/dist/types/modes/components/model-selector.d.ts +2 -0
  27. package/dist/types/modes/components/settings-selector.d.ts +5 -1
  28. package/dist/types/modes/components/skill-message.d.ts +1 -0
  29. package/dist/types/modes/components/status-line/types.d.ts +5 -7
  30. package/dist/types/modes/components/status-line.d.ts +15 -3
  31. package/dist/types/modes/components/two-column-body.d.ts +9 -0
  32. package/dist/types/modes/components/welcome.d.ts +9 -1
  33. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  34. package/dist/types/modes/controllers/input-controller.d.ts +6 -1
  35. package/dist/types/modes/controllers/selector-controller.d.ts +10 -1
  36. package/dist/types/modes/interactive-mode.d.ts +2 -0
  37. package/dist/types/modes/prompt-action-autocomplete.d.ts +3 -0
  38. package/dist/types/modes/rpc/rpc-types.d.ts +1 -0
  39. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +41 -7
  40. package/dist/types/modes/slash-command-visibility.d.ts +1 -0
  41. package/dist/types/modes/theme/theme.d.ts +1 -1
  42. package/dist/types/modes/tmux-scroll.d.ts +18 -0
  43. package/dist/types/modes/types.d.ts +5 -1
  44. package/dist/types/modes/utils/terminal-bell.d.ts +3 -0
  45. package/dist/types/notifications/daemon-paths.d.ts +9 -0
  46. package/dist/types/notifications/html-format.d.ts +2 -0
  47. package/dist/types/notifications/recent-activity.d.ts +5 -1
  48. package/dist/types/notifications/telegram-daemon-cli.d.ts +21 -4
  49. package/dist/types/notifications/telegram-daemon-control.d.ts +8 -0
  50. package/dist/types/notifications/telegram-daemon.d.ts +4 -10
  51. package/dist/types/notifications/telegram-reference.d.ts +4 -0
  52. package/dist/types/notifications/threaded-render.d.ts +1 -0
  53. package/dist/types/runtime-mcp/config-writer.d.ts +9 -0
  54. package/dist/types/runtime-mcp/config.d.ts +2 -0
  55. package/dist/types/runtime-mcp/loader.d.ts +2 -0
  56. package/dist/types/runtime-mcp/manager.d.ts +2 -0
  57. package/dist/types/runtime-mcp/types.d.ts +6 -0
  58. package/dist/types/sdk.d.ts +1 -1
  59. package/dist/types/skc-runtime/launch-tmux.d.ts +2 -2
  60. package/dist/types/skc-runtime/ralplan-runtime.d.ts +3 -2
  61. package/dist/types/skc-runtime/restricted-role-agent-bash.d.ts +1 -0
  62. package/dist/types/skc-runtime/session-state-sidecar.d.ts +5 -0
  63. package/dist/types/skc-runtime/state-writer.d.ts +1 -0
  64. package/dist/types/skc-runtime/team-runtime.d.ts +1 -1
  65. package/dist/types/skc-runtime/tmux-common.d.ts +12 -0
  66. package/dist/types/skc-runtime/ultragoal-guard.d.ts +7 -1
  67. package/dist/types/skc-runtime/ultragoal-runtime.d.ts +0 -4
  68. package/dist/types/slash-commands/types.d.ts +2 -0
  69. package/dist/types/system-prompt.d.ts +7 -0
  70. package/dist/types/tools/skill.d.ts +1 -1
  71. package/dist/types/utils/cmux-workspace.d.ts +52 -0
  72. package/dist/types/web/search/index.d.ts +4 -2
  73. package/dist/types/web/search/provider.d.ts +14 -0
  74. package/dist/types/web/search/providers/utils.d.ts +42 -13
  75. package/dist/types/web/search/render.d.ts +1 -0
  76. package/dist/types/workflow/workflow-intent-diff.d.ts +19 -0
  77. package/package.json +8 -8
  78. package/scripts/build-binary.ts +3 -0
  79. package/scripts/verify-insane-vendor.ts +0 -5
  80. package/src/capability/mcp.ts +2 -0
  81. package/src/cli/local-provider-smoke.ts +632 -0
  82. package/src/cli/notify-cli.ts +4 -2
  83. package/src/cli/telegram-cli.ts +156 -0
  84. package/src/cli/update-cli.ts +179 -13
  85. package/src/cli/web-search-cli.ts +5 -7
  86. package/src/cli.ts +20 -1
  87. package/src/commands/launch.ts +47 -2
  88. package/src/commands/local-provider.ts +62 -0
  89. package/src/commands/notify.ts +1 -1
  90. package/src/commands/ralplan.ts +1 -0
  91. package/src/commands/telegram.ts +47 -0
  92. package/src/config/keybindings.ts +15 -6
  93. package/src/config/mcp-schema.json +4 -0
  94. package/src/config/model-registry.ts +55 -1
  95. package/src/config/models-config-schema.ts +9 -0
  96. package/src/config/settings-schema.ts +256 -4
  97. package/src/config/settings.ts +11 -0
  98. package/src/config/telegram-autostart.ts +107 -0
  99. package/src/config/telegram-env-bridge.ts +92 -0
  100. package/src/coordinator-mcp/server.ts +276 -34
  101. package/src/defaults/skc/skills/ralplan/SKILL.md +29 -20
  102. package/src/defaults/skc/skills/ultragoal/SKILL.md +37 -112
  103. package/src/defaults/skc-defaults.ts +16 -0
  104. package/src/discovery/builtin.ts +12 -0
  105. package/src/discovery/cursor.ts +5 -84
  106. package/src/discovery/gemini.ts +3 -89
  107. package/src/discovery/index.ts +4 -1
  108. package/src/discovery/mcp-json.ts +11 -0
  109. package/src/discovery/opencode.ts +5 -122
  110. package/src/discovery/windsurf.ts +4 -82
  111. package/src/edit/read-file.ts +9 -1
  112. package/src/extensibility/extensions/runner.ts +2 -1
  113. package/src/extensibility/extensions/types.ts +11 -0
  114. package/src/extensibility/skills.ts +30 -2
  115. package/src/hooks/skill-state.ts +33 -73
  116. package/src/i18n/messages/de.ts +1 -0
  117. package/src/i18n/messages/en.ts +1 -0
  118. package/src/i18n/messages/es.ts +1 -0
  119. package/src/i18n/messages/fr.ts +1 -0
  120. package/src/i18n/messages/ja.settings.ts +28 -0
  121. package/src/i18n/messages/ja.ts +1 -0
  122. package/src/i18n/messages/ko.settings.ts +28 -0
  123. package/src/i18n/messages/ko.ts +1 -0
  124. package/src/i18n/messages/zh.settings.ts +28 -0
  125. package/src/i18n/messages/zh.ts +1 -0
  126. package/src/internal-urls/docs-index.generated.ts +12 -7
  127. package/src/main.ts +5 -2
  128. package/src/migrate/executor.ts +23 -6
  129. package/src/modes/acp/acp-agent.ts +7 -6
  130. package/src/modes/components/agent-dashboard.ts +3 -35
  131. package/src/modes/components/custom-editor.ts +26 -0
  132. package/src/modes/components/custom-model-preset-wizard.ts +30 -189
  133. package/src/modes/components/extensions/extension-dashboard.ts +2 -48
  134. package/src/modes/components/hook-selector.ts +21 -3
  135. package/src/modes/components/model-selector.ts +246 -11
  136. package/src/modes/components/session-observer-overlay.ts +3 -3
  137. package/src/modes/components/session-selector.ts +19 -9
  138. package/src/modes/components/settings-selector.ts +551 -11
  139. package/src/modes/components/skill-message.ts +18 -10
  140. package/src/modes/components/status-line/presets.ts +11 -0
  141. package/src/modes/components/status-line/segments.ts +16 -19
  142. package/src/modes/components/status-line/types.ts +6 -2
  143. package/src/modes/components/status-line.ts +281 -117
  144. package/src/modes/components/tree-selector.ts +28 -8
  145. package/src/modes/components/two-column-body.ts +36 -0
  146. package/src/modes/components/welcome.ts +165 -30
  147. package/src/modes/controllers/command-controller.ts +26 -2
  148. package/src/modes/controllers/event-controller.ts +102 -4
  149. package/src/modes/controllers/extension-ui-controller.ts +21 -2
  150. package/src/modes/controllers/input-controller.ts +214 -47
  151. package/src/modes/controllers/selector-controller.ts +35 -33
  152. package/src/modes/interactive-mode.ts +36 -30
  153. package/src/modes/prompt-action-autocomplete.ts +48 -8
  154. package/src/modes/rpc/rpc-types.ts +1 -0
  155. package/src/modes/shared/agent-wire/deep-interview-gate.ts +72 -15
  156. package/src/modes/shared/agent-wire/workflow-gate-schema.ts +20 -0
  157. package/src/modes/slash-command-visibility.ts +9 -0
  158. package/src/modes/theme/theme.ts +5 -1
  159. package/src/modes/tmux-scroll.ts +92 -0
  160. package/src/modes/types.ts +8 -1
  161. package/src/modes/utils/hotkeys-markdown.ts +5 -2
  162. package/src/modes/utils/terminal-bell.ts +48 -0
  163. package/src/modes/utils/ui-helpers.ts +76 -3
  164. package/src/notifications/daemon-paths.ts +22 -0
  165. package/src/notifications/html-format.ts +75 -1
  166. package/src/notifications/index.ts +32 -2
  167. package/src/notifications/lifecycle-control-runtime.ts +12 -3
  168. package/src/notifications/recent-activity.ts +32 -8
  169. package/src/notifications/telegram-daemon-cli.ts +125 -13
  170. package/src/notifications/telegram-daemon-control.ts +9 -0
  171. package/src/notifications/telegram-daemon.ts +88 -46
  172. package/src/notifications/telegram-reference.ts +36 -14
  173. package/src/notifications/threaded-render.ts +14 -6
  174. package/src/prompts/agents/architect.md +7 -4
  175. package/src/prompts/agents/critic.md +15 -6
  176. package/src/prompts/agents/planner.md +8 -3
  177. package/src/prompts/system/system-prompt.md +20 -2
  178. package/src/prompts/tools/bash.md +1 -1
  179. package/src/runtime-mcp/config-writer.ts +28 -0
  180. package/src/runtime-mcp/config.ts +7 -0
  181. package/src/runtime-mcp/loader.ts +3 -0
  182. package/src/runtime-mcp/manager.ts +3 -0
  183. package/src/runtime-mcp/types.ts +6 -0
  184. package/src/sdk.ts +10 -11
  185. package/src/session/agent-session.ts +103 -13
  186. package/src/session/session-manager.ts +58 -18
  187. package/src/skc-runtime/launch-tmux.ts +34 -8
  188. package/src/skc-runtime/launch-worktree.ts +16 -2
  189. package/src/skc-runtime/ralplan-runtime.ts +22 -6
  190. package/src/skc-runtime/restricted-role-agent-bash.ts +1 -0
  191. package/src/skc-runtime/session-state-sidecar.ts +167 -24
  192. package/src/skc-runtime/state-runtime.ts +8 -5
  193. package/src/skc-runtime/state-writer.ts +5 -5
  194. package/src/skc-runtime/team-runtime.ts +21 -6
  195. package/src/skc-runtime/tmux-common.ts +16 -0
  196. package/src/skc-runtime/tmux-sessions.ts +13 -9
  197. package/src/skc-runtime/ultragoal-guard.ts +150 -24
  198. package/src/skc-runtime/ultragoal-runtime.ts +16 -124
  199. package/src/skc-runtime/workflow-manifest.generated.json +0 -8
  200. package/src/skc-runtime/workflow-manifest.ts +0 -1
  201. package/src/slash-commands/builtin-registry.ts +136 -23
  202. package/src/slash-commands/types.ts +2 -0
  203. package/src/system-prompt.ts +8 -0
  204. package/src/task/executor.ts +5 -0
  205. package/src/tools/ast-grep.ts +10 -7
  206. package/src/tools/bash.ts +39 -14
  207. package/src/tools/index.ts +8 -1
  208. package/src/tools/skill.ts +1 -1
  209. package/src/utils/cmux-workspace.ts +233 -0
  210. package/src/utils/title-generator.ts +2 -0
  211. package/src/web/kagi.ts +1 -1
  212. package/src/web/parallel.ts +2 -2
  213. package/src/web/search/index.ts +137 -52
  214. package/src/web/search/provider.ts +80 -0
  215. package/src/web/search/providers/anthropic.ts +1 -1
  216. package/src/web/search/providers/brave.ts +1 -1
  217. package/src/web/search/providers/codex.ts +1 -1
  218. package/src/web/search/providers/duckduckgo.ts +1 -1
  219. package/src/web/search/providers/exa.ts +1 -1
  220. package/src/web/search/providers/gemini.ts +7 -3
  221. package/src/web/search/providers/jina.ts +1 -1
  222. package/src/web/search/providers/kimi.ts +6 -1
  223. package/src/web/search/providers/openai-compatible.ts +1 -1
  224. package/src/web/search/providers/parallel.ts +1 -1
  225. package/src/web/search/providers/perplexity.ts +2 -2
  226. package/src/web/search/providers/searxng.ts +1 -1
  227. package/src/web/search/providers/synthetic.ts +1 -1
  228. package/src/web/search/providers/tavily.ts +1 -1
  229. package/src/web/search/providers/utils.ts +67 -17
  230. package/src/web/search/providers/xai.ts +1 -1
  231. package/src/web/search/providers/zai.ts +1 -1
  232. package/src/web/search/render.ts +3 -0
  233. package/src/workflow/workflow-intent-diff.ts +199 -0
  234. package/vendor/insane-search/MANIFEST.json +1 -3
  235. package/vendor/insane-search/engine/tests/test_u1.py +0 -28
  236. package/vendor/insane-search/engine/transport.py +2 -3
  237. package/vendor/insane-search/engine/validators.py +0 -14
  238. package/src/defaults/skc/rules/ponytail.md +0 -68
@@ -147,6 +147,8 @@ export interface MCPDiscoverOptions {
147
147
  filterExa?: boolean;
148
148
  /** Whether to filter out browser MCP servers when builtin browser tool is enabled (default: false) */
149
149
  filterBrowser?: boolean;
150
+ /** Only connect servers with autoload !== false (default: false) */
151
+ autoloadOnly?: boolean;
150
152
  /** Called when starting to connect to servers */
151
153
  onConnecting?: (serverNames: string[]) => void;
152
154
  }
@@ -325,6 +327,7 @@ export class MCPManager {
325
327
  enableProjectConfig: options?.enableProjectConfig,
326
328
  filterExa: options?.filterExa,
327
329
  filterBrowser: options?.filterBrowser,
330
+ autoloadOnly: options?.autoloadOnly,
328
331
  });
329
332
  const result = await this.connectServers(configs, sources, options?.onConnecting);
330
333
  result.exaApiKeys = exaApiKeys;
@@ -61,6 +61,12 @@ export interface MCPAuthConfig {
61
61
  interface MCPServerConfigBase {
62
62
  /** Whether this server is enabled (default: true) */
63
63
  enabled?: boolean;
64
+ /**
65
+ * Whether to connect this server automatically at session startup (default: true).
66
+ * When false the server stays configured-but-disconnected until connected
67
+ * explicitly (e.g. `/mcp connect <name>`).
68
+ */
69
+ autoload?: boolean;
64
70
  /** Connection timeout in milliseconds (default: 30000) */
65
71
  timeout?: number;
66
72
  /** Authentication configuration (optional) */
package/src/sdk.ts CHANGED
@@ -121,6 +121,7 @@ import { AgentOutputManager } from "./task/output-manager";
121
121
  import { parseThinkingLevel, resolveThinkingLevelForModel, toReasoningEffort } from "./thinking";
122
122
  import { collectDiscoverableTools, type DiscoverableTool } from "./tool-discovery/tool-index";
123
123
  import {
124
+ applyConfiguredSearchTimeout,
124
125
  BashTool,
125
126
  BUILTIN_TOOLS,
126
127
  computeEssentialBuiltinNames,
@@ -129,10 +130,10 @@ import {
129
130
  EditTool,
130
131
  EvalTool,
131
132
  FindTool,
133
+ getConfiguredSearchProviderPreference,
132
134
  getSearchTools,
133
135
  HIDDEN_TOOLS,
134
136
  isConfigurableSearchProviderId,
135
- isSearchProviderPreference,
136
137
  type LspStartupServerInfo,
137
138
  loadSshTool,
138
139
  ReadTool,
@@ -142,7 +143,6 @@ import {
142
143
  setPreferredImageProvider,
143
144
  setPreferredSearchProvider,
144
145
  setSearchFallbackProviders,
145
- setSearchHardTimeoutMs,
146
146
  type Tool,
147
147
  type ToolSession,
148
148
  WebSearchTool,
@@ -257,7 +257,7 @@ export interface CreateAgentSessionOptions {
257
257
  thinkingLevel?: ThinkingLevel;
258
258
  /** Runtime substitution metadata for the initial model_change session event. */
259
259
  modelSubstitution?: { requestedModel: Model; reason: string };
260
- /** Models available for cycling (Ctrl+P in interactive mode) */
260
+ /** Models available for cycling (Alt+N in interactive mode) */
261
261
  scopedModels?: ScopedModelSelection[];
262
262
 
263
263
  /** System prompt blocks. Array replaces default, function receives default blocks and returns final blocks. */
@@ -934,20 +934,15 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
934
934
  slashCommandsPromise.catch(() => {});
935
935
 
936
936
  // Initialize provider preferences from settings
937
- const webSearchProvider = settings.get("providers.webSearch");
938
- if (typeof webSearchProvider === "string" && isSearchProviderPreference(webSearchProvider)) {
939
- setPreferredSearchProvider(webSearchProvider);
940
- }
937
+ const webSearchProvider = getConfiguredSearchProviderPreference(settings);
938
+ setPreferredSearchProvider(webSearchProvider);
941
939
  const webSearchFallback = settings.get("web_search.fallback");
942
940
  if (Array.isArray(webSearchFallback)) {
943
941
  setSearchFallbackProviders(
944
942
  webSearchFallback.filter(value => typeof value === "string" && isConfigurableSearchProviderId(value)),
945
943
  );
946
944
  }
947
- const webSearchTimeout = settings.get("web_search.timeout");
948
- if (typeof webSearchTimeout === "number" && Number.isFinite(webSearchTimeout) && webSearchTimeout > 0) {
949
- setSearchHardTimeoutMs(webSearchTimeout * 1000);
950
- }
945
+ applyConfiguredSearchTimeout(settings);
951
946
 
952
947
  const imageProvider = settings.get("providers.image");
953
948
  if (
@@ -1844,6 +1839,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1844
1839
  intentField,
1845
1840
  mcpDiscoveryMode: false,
1846
1841
  mcpDiscoveryServerSummaries: [],
1842
+ toolDiscoveryActive: effectiveDiscoveryMode === "all",
1843
+ discoverableTools: discoverableBuiltinTools
1844
+ .map(tool => ({ name: tool.name, summary: tool.summary }))
1845
+ .sort((a, b) => a.name.localeCompare(b.name)),
1847
1846
  eagerTasks,
1848
1847
  secretsEnabled,
1849
1848
  workspaceTree: workspaceTreePromise,
@@ -260,6 +260,7 @@ import { type EditMode, resolveEditMode } from "../utils/edit-mode";
260
260
  import { resolveFileDisplayMode } from "../utils/file-display-mode";
261
261
  import { extractFileMentions, generateFileMentionMessages } from "../utils/file-mentions";
262
262
  import { buildNamedToolChoice, buildNamedToolChoiceResult } from "../utils/tool-choice";
263
+ import { buildWorkflowIntentDiff, WORKFLOW_INTENT_DIFF_CUSTOM_TYPE } from "../workflow/workflow-intent-diff";
263
264
  import type { AuthStorage } from "./auth-storage";
264
265
  import type { ClientBridge, ClientBridgePermissionOption, ClientBridgePermissionOutcome } from "./client-bridge";
265
266
  import {
@@ -526,6 +527,15 @@ export interface SessionStats {
526
527
  type RetryFallbackChains = Record<string, string[]>;
527
528
 
528
529
  type RetryFallbackRevertPolicy = "never" | "cooldown-expiry";
530
+ type RetryErrorClassification =
531
+ | "none"
532
+ | "overflow"
533
+ | "terminal"
534
+ | "usage_limit"
535
+ | "first_event_timeout"
536
+ | "transient"
537
+ | "local_unavailable"
538
+ | "unknown";
529
539
 
530
540
  interface RetryFallbackSelector {
531
541
  raw: string;
@@ -563,6 +573,37 @@ function formatRetryFallbackBaseSelector(selector: RetryFallbackSelector): strin
563
573
  return `${selector.provider}/${selector.id}`;
564
574
  }
565
575
 
576
+ function isLocalModelEndpoint(model: Model | undefined): boolean {
577
+ if (!model) return false;
578
+ if (model.provider === "ollama" || model.provider === "lm-studio" || model.provider === "llama.cpp") {
579
+ return true;
580
+ }
581
+ try {
582
+ const hostname = new URL(model.baseUrl).hostname.toLowerCase();
583
+ if (
584
+ hostname === "localhost" ||
585
+ hostname === "0.0.0.0" ||
586
+ hostname === "::1" ||
587
+ hostname === "[::1]" ||
588
+ hostname.endsWith(".local")
589
+ ) {
590
+ return true;
591
+ }
592
+ if (/^127\./.test(hostname) || /^10\./.test(hostname) || /^192\.168\./.test(hostname)) {
593
+ return true;
594
+ }
595
+ const private172 = /^172\.(\d{1,2})\./.exec(hostname);
596
+ if (private172) {
597
+ const secondOctet = Number(private172[1]);
598
+ return secondOctet >= 16 && secondOctet <= 31;
599
+ }
600
+ } catch {
601
+ // A malformed base URL is configuration, not availability. Keep it visible.
602
+ return false;
603
+ }
604
+ return false;
605
+ }
606
+
566
607
  const IRC_REPLY_MAX_BYTES = 4096;
567
608
 
568
609
  /**
@@ -862,7 +903,7 @@ function extractPermissionLocations(
862
903
  * custom messages queued during streaming) and is matched by the custom-role
863
904
  * `message_start` dequeue branch; user-message pushes leave it undefined and
864
905
  * rely on the existing text-equality match. */
865
- type QueuedDisplayEntry = { text: string; tag?: string };
906
+ type QueuedDisplayEntry = { text: string; tag?: string; sequence: number };
866
907
 
867
908
  /** A custom message contributed at the before-agent-start point. */
868
909
  export type BeforeAgentStartInternalMessage = Pick<
@@ -916,6 +957,7 @@ export class AgentSession {
916
957
  /** Tracks pending follow-up messages for UI display. Removed when delivered.
917
958
  * See `#steeringMessages` for entry shape. */
918
959
  #followUpMessages: QueuedDisplayEntry[] = [];
960
+ #queuedDisplaySequence = 0;
919
961
  /** Messages queued to be included with the next user prompt as context ("asides"). */
920
962
  #pendingNextTurnMessages: CustomMessage[] = [];
921
963
  #scheduledHiddenNextTurnGeneration: number | undefined = undefined;
@@ -1585,6 +1627,14 @@ export class AgentSession {
1585
1627
  this.#planCompactAbortPending = false;
1586
1628
  }
1587
1629
 
1630
+ #createQueuedDisplayEntry(text: string, tag?: string): QueuedDisplayEntry {
1631
+ const entry: QueuedDisplayEntry = { text, sequence: ++this.#queuedDisplaySequence };
1632
+ if (tag !== undefined) {
1633
+ entry.tag = tag;
1634
+ }
1635
+ return entry;
1636
+ }
1637
+
1588
1638
  /** Register a compact display string for a custom message that the caller is
1589
1639
  * about to dispatch via `promptCustomMessage` / `sendCustomMessage`.
1590
1640
  * Returns a stable tag the caller MUST embed in
@@ -1598,7 +1648,7 @@ export class AgentSession {
1598
1648
  const tag = `skc-cmd-${Date.now()}-${++this.#customDisplayTagCounter}`;
1599
1649
  const displayText = text.trim();
1600
1650
  if (!displayText) return tag;
1601
- const entry: QueuedDisplayEntry = { text: displayText, tag };
1651
+ const entry = this.#createQueuedDisplayEntry(displayText, tag);
1602
1652
  if (mode === "steer") {
1603
1653
  this.#steeringMessages.push(entry);
1604
1654
  } else {
@@ -4769,6 +4819,7 @@ export class AgentSession {
4769
4819
 
4770
4820
  // Expand file-based prompt templates if requested
4771
4821
  const expandedText = expandPromptTemplates ? expandPromptTemplate(text, [...this.#promptTemplates]) : text;
4822
+ const workflowIntentDiff = options?.synthetic ? null : buildWorkflowIntentDiff(expandedText);
4772
4823
 
4773
4824
  // If streaming, queue via steer() or followUp() based on option
4774
4825
  if (this.isStreaming) {
@@ -4780,9 +4831,16 @@ export class AgentSession {
4780
4831
  } else {
4781
4832
  await this.#queueSteer(expandedText, options?.images);
4782
4833
  }
4834
+ if (workflowIntentDiff) {
4835
+ this.sessionManager.appendCustomEntry(WORKFLOW_INTENT_DIFF_CUSTOM_TYPE, workflowIntentDiff);
4836
+ }
4783
4837
  return;
4784
4838
  }
4785
4839
 
4840
+ if (workflowIntentDiff) {
4841
+ this.sessionManager.appendCustomEntry(WORKFLOW_INTENT_DIFF_CUSTOM_TYPE, workflowIntentDiff);
4842
+ }
4843
+
4786
4844
  // Skip eager todo prelude when the user has already queued a directive
4787
4845
  const hasPendingUserDirective = this.#toolChoiceQueue.inspect().includes("user-force");
4788
4846
  const eagerTodoPrelude =
@@ -5236,7 +5294,7 @@ export class AgentSession {
5236
5294
  */
5237
5295
  async #queueSteer(text: string, images?: ImageContent[]): Promise<void> {
5238
5296
  const displayText = text || (images && images.length > 0 ? "[Image]" : "");
5239
- this.#steeringMessages.push({ text: displayText });
5297
+ this.#steeringMessages.push(this.#createQueuedDisplayEntry(displayText));
5240
5298
  const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
5241
5299
  if (images && images.length > 0) {
5242
5300
  content.push(...images);
@@ -5267,7 +5325,7 @@ export class AgentSession {
5267
5325
  */
5268
5326
  async #queueFollowUp(text: string, images?: ImageContent[]): Promise<void> {
5269
5327
  const displayText = text || (images && images.length > 0 ? "[Image]" : "");
5270
- this.#followUpMessages.push({ text: displayText });
5328
+ this.#followUpMessages.push(this.#createQueuedDisplayEntry(displayText));
5271
5329
  const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
5272
5330
  if (images && images.length > 0) {
5273
5331
  content.push(...images);
@@ -5643,18 +5701,21 @@ export class AgentSession {
5643
5701
  * record — no orphan state can outlive the queue entry.
5644
5702
  */
5645
5703
  popLastQueuedMessage(): string | undefined {
5646
- // Pop from steering first (LIFO)
5647
- if (this.#steeringMessages.length > 0) {
5704
+ const steeringEntry = this.#steeringMessages.at(-1);
5705
+ const followUpEntry = this.#followUpMessages.at(-1);
5706
+
5707
+ if (steeringEntry && (!followUpEntry || steeringEntry.sequence > followUpEntry.sequence)) {
5648
5708
  const entry = this.#steeringMessages.pop();
5649
5709
  this.agent.popLastSteer();
5650
5710
  return entry?.text;
5651
5711
  }
5652
- // Then from follow-up
5653
- if (this.#followUpMessages.length > 0) {
5712
+
5713
+ if (followUpEntry) {
5654
5714
  const entry = this.#followUpMessages.pop();
5655
5715
  this.agent.popLastFollowUp();
5656
5716
  return entry?.text;
5657
5717
  }
5718
+
5658
5719
  return undefined;
5659
5720
  }
5660
5721
 
@@ -8221,6 +8282,12 @@ export class AgentSession {
8221
8282
  */
8222
8283
  #isRetryableError(message: AssistantMessage): boolean {
8223
8284
  const classification = this.#classifyErrorForRetry(message);
8285
+ if (classification === "local_unavailable") {
8286
+ return this.#hasRetryFallbackCandidate({
8287
+ currentSelector: this.model ? formatRetryFallbackSelector(this.model, this.thinkingLevel) : undefined,
8288
+ requireNonLocal: true,
8289
+ });
8290
+ }
8224
8291
  return (
8225
8292
  classification === "usage_limit" ||
8226
8293
  classification === "transient" ||
@@ -8260,6 +8327,12 @@ export class AgentSession {
8260
8327
  return /timed?\s*out while waiting for the first event|timeout waiting for first/i.test(errorMessage);
8261
8328
  }
8262
8329
 
8330
+ #isLocalProviderAvailabilityErrorMessage(errorMessage: string): boolean {
8331
+ return /connection.?refused|econnrefused|timed?\s*out|timeout|fetch failed|network.?error|socket hang up|terminated|service.?unavailable|server.?error|internal.?error|503|model_not_found|model not found|no such model|unknown model|model .*not.*(found|available|loaded)|not ready|not.?ready|warming|loading|currently loading|try again|out of memory|\boom\b|memory guard|insufficient memory|not enough memory|failed to allocate|kv.?cache|malformed (stream|streaming|sse)|invalid (stream|streaming|sse)|stream envelope error|unexpected end of (json|input)|unterminated json|no error details in response/i.test(
8332
+ errorMessage,
8333
+ );
8334
+ }
8335
+
8263
8336
  /**
8264
8337
  * Whether a first-event timeout on the error's provider should fail closed —
8265
8338
  * i.e. retry a bounded number of times (capped at retry.maxRetries) and then
@@ -8303,13 +8376,14 @@ export class AgentSession {
8303
8376
  * -> usage_limit (rotation) -> first_event_timeout (bounded retry) ->
8304
8377
  * transient (retry) -> unknown (retry).
8305
8378
  */
8306
- #classifyErrorForRetry(
8307
- message: AssistantMessage,
8308
- ): "none" | "overflow" | "terminal" | "usage_limit" | "first_event_timeout" | "transient" | "unknown" {
8379
+ #classifyErrorForRetry(message: AssistantMessage): RetryErrorClassification {
8309
8380
  if (message.stopReason !== "error" || !message.errorMessage) return "none";
8310
8381
  const contextWindow = this.model?.contextWindow ?? 0;
8311
8382
  if (isContextOverflow(message, contextWindow)) return "overflow";
8312
8383
  const err = message.errorMessage;
8384
+ if (isLocalModelEndpoint(this.model) && this.#isLocalProviderAvailabilityErrorMessage(err)) {
8385
+ return "local_unavailable";
8386
+ }
8313
8387
  // Stream-envelope errors are only transient in the pre-message_start
8314
8388
  // variant; any other envelope failure is structural and must surface.
8315
8389
  if (/anthropic stream envelope error:/i.test(err)) {
@@ -8444,6 +8518,20 @@ export class AgentSession {
8444
8518
  return chain;
8445
8519
  }
8446
8520
 
8521
+ #hasRetryFallbackCandidate(options: { currentSelector: string | undefined; requireNonLocal?: boolean }): boolean {
8522
+ if (!options.currentSelector) return false;
8523
+ const role = this.#activeRetryFallback?.role ?? this.#resolveRetryFallbackRole(options.currentSelector);
8524
+ if (!role) return false;
8525
+ for (const selector of this.#findRetryFallbackCandidates(role, options.currentSelector)) {
8526
+ if (this.#isRetryFallbackSelectorSuppressed(selector)) continue;
8527
+ const candidate = this.#modelRegistry.find(selector.provider, selector.id);
8528
+ if (!candidate) continue;
8529
+ if (options.requireNonLocal && isLocalModelEndpoint(candidate)) continue;
8530
+ return true;
8531
+ }
8532
+ return false;
8533
+ }
8534
+
8447
8535
  #findRetryFallbackCandidates(role: string, currentSelector: string): RetryFallbackSelector[] {
8448
8536
  const chain = this.#getRetryFallbackEffectiveChain(role);
8449
8537
  if (chain.length <= 1) return [];
@@ -8497,7 +8585,7 @@ export class AgentSession {
8497
8585
  });
8498
8586
  }
8499
8587
 
8500
- async #tryRetryModelFallback(currentSelector: string): Promise<boolean> {
8588
+ async #tryRetryModelFallback(currentSelector: string, options?: { requireNonLocal?: boolean }): Promise<boolean> {
8501
8589
  const role = this.#activeRetryFallback?.role ?? this.#resolveRetryFallbackRole(currentSelector);
8502
8590
  if (!role) return false;
8503
8591
 
@@ -8505,6 +8593,7 @@ export class AgentSession {
8505
8593
  if (this.#isRetryFallbackSelectorSuppressed(selector)) continue;
8506
8594
  const candidate = this.#modelRegistry.find(selector.provider, selector.id);
8507
8595
  if (!candidate) continue;
8596
+ if (options?.requireNonLocal && isLocalModelEndpoint(candidate)) continue;
8508
8597
  const apiKey = await this.#modelRegistry.getApiKey(candidate, this.sessionId);
8509
8598
  if (!apiKey) continue;
8510
8599
  await this.#applyRetryFallbackCandidate(role, selector, currentSelector);
@@ -8615,6 +8704,7 @@ export class AgentSession {
8615
8704
  if (!retrySettings.enabled) return false;
8616
8705
  const retryClassification = this.#classifyErrorForRetry(message);
8617
8706
  const unboundedClass = retryClassification === "transient" || retryClassification === "unknown";
8707
+ const localUnavailable = retryClassification === "local_unavailable";
8618
8708
 
8619
8709
  const generation = this.#promptGeneration;
8620
8710
  this.#retryAttempt++;
@@ -8668,7 +8758,7 @@ export class AgentSession {
8668
8758
  const currentSelector = this.model ? formatRetryFallbackSelector(this.model, this.thinkingLevel) : undefined;
8669
8759
  if (!switchedCredential && currentSelector) {
8670
8760
  this.#noteRetryFallbackCooldown(currentSelector, parsedRetryAfterMs, errorMessage);
8671
- switchedModel = await this.#tryRetryModelFallback(currentSelector);
8761
+ switchedModel = await this.#tryRetryModelFallback(currentSelector, { requireNonLocal: localUnavailable });
8672
8762
  if (switchedModel) {
8673
8763
  delayMs = 0;
8674
8764
  } else if (parsedRetryAfterMs && parsedRetryAfterMs > delayMs) {
@@ -1461,9 +1461,25 @@ function materializeResidentEntrySync<T extends FileEntry | SessionEntry>(
1461
1461
  function materializeResidentEntriesSync<T extends FileEntry | SessionEntry>(
1462
1462
  entries: T[],
1463
1463
  stores: ResidentBlobStores,
1464
+ missingPolicy: ResidentBlobMissingPolicy = "throw",
1464
1465
  ): T[] {
1465
1466
  const cache = new Map<string, string>();
1466
- return entries.map(entry => materializeResidentEntrySync(entry, stores, cache));
1467
+ return entries.map(entry => materializeResidentEntrySync(entry, stores, cache, missingPolicy));
1468
+ }
1469
+
1470
+ function materializeResidentEntryForReadSync<T extends FileEntry | SessionEntry>(
1471
+ entry: T,
1472
+ stores: ResidentBlobStores,
1473
+ cache: Map<string, string>,
1474
+ ): T {
1475
+ return materializeResidentEntrySync(entry, stores, cache, "placeholder");
1476
+ }
1477
+
1478
+ function materializeResidentEntriesForReadSync<T extends FileEntry | SessionEntry>(
1479
+ entries: T[],
1480
+ stores: ResidentBlobStores,
1481
+ ): T[] {
1482
+ return materializeResidentEntriesSync(entries, stores, "placeholder");
1467
1483
  }
1468
1484
 
1469
1485
  function materializeResidentEntryForPersistenceSync<T extends FileEntry | SessionEntry>(
@@ -1520,12 +1536,24 @@ function cloneSessionEntry(entry: SessionEntry): SessionEntry {
1520
1536
  function materializeProviderVisibleEntrySync(entry: SessionEntry, stores: ResidentBlobStores): SessionEntry {
1521
1537
  if (entry.type === "compaction") {
1522
1538
  const cache = new Map<string, string>();
1523
- const summary = materializeResidentValueSync(entry.summary, stores, "summary", cache);
1524
- const shortSummary = materializeResidentValueSync(entry.shortSummary, stores, "shortSummary", cache);
1539
+ const summary = materializeResidentValueSync(entry.summary, stores, "summary", cache, "placeholder");
1540
+ const shortSummary = materializeResidentValueSync(
1541
+ entry.shortSummary,
1542
+ stores,
1543
+ "shortSummary",
1544
+ cache,
1545
+ "placeholder",
1546
+ );
1525
1547
  const remote = entry.preserveData?.openaiRemoteCompaction;
1526
1548
  const remoteRecord = isRecord(remote) ? remote : undefined;
1527
1549
  const replacementHistory = remoteRecord
1528
- ? materializeResidentValueSync(remoteRecord.replacementHistory, stores, "replacementHistory", cache)
1550
+ ? materializeResidentValueSync(
1551
+ remoteRecord.replacementHistory,
1552
+ stores,
1553
+ "replacementHistory",
1554
+ cache,
1555
+ "placeholder",
1556
+ )
1529
1557
  : undefined;
1530
1558
  const preserveData =
1531
1559
  remoteRecord && replacementHistory !== undefined && replacementHistory !== remoteRecord.replacementHistory
@@ -1545,7 +1573,13 @@ function materializeProviderVisibleEntrySync(entry: SessionEntry, stores: Reside
1545
1573
  };
1546
1574
  }
1547
1575
  if (entry.type === "branch_summary") {
1548
- const summary = materializeResidentValueSync(entry.summary, stores, "summary", new Map<string, string>());
1576
+ const summary = materializeResidentValueSync(
1577
+ entry.summary,
1578
+ stores,
1579
+ "summary",
1580
+ new Map<string, string>(),
1581
+ "placeholder",
1582
+ );
1549
1583
  return typeof summary === "string" ? { ...entry, summary } : { ...entry };
1550
1584
  }
1551
1585
  return cloneSessionEntry(entry);
@@ -2751,7 +2785,10 @@ export class SessionManager {
2751
2785
  }
2752
2786
 
2753
2787
  captureState(): SessionManagerStateSnapshot {
2754
- const materializedFileEntries = materializeResidentEntriesSync(this.#fileEntries, this.#residentBlobStores());
2788
+ const materializedFileEntries = materializeResidentEntriesForReadSync(
2789
+ this.#fileEntries,
2790
+ this.#residentBlobStores(),
2791
+ );
2755
2792
  return {
2756
2793
  sessionId: this.#sessionId,
2757
2794
  sessionName: this.#sessionName,
@@ -2875,7 +2912,7 @@ export class SessionManager {
2875
2912
 
2876
2913
  const oldSessionFile = this.#sessionFile;
2877
2914
  const oldSessionId = this.#sessionId;
2878
- const materializedEntries = materializeResidentEntriesSync(this.#fileEntries, this.#residentBlobStores());
2915
+ const materializedEntries = materializeResidentEntriesForReadSync(this.#fileEntries, this.#residentBlobStores());
2879
2916
 
2880
2917
  // Close the current writer
2881
2918
  await this.#closePersistWriter();
@@ -2947,7 +2984,10 @@ export class SessionManager {
2947
2984
  hadSessionFile = this.storage.existsSync(oldSessionFile);
2948
2985
  let movedSessionFile = false;
2949
2986
  let movedArtifactDir = false;
2950
- const materializedEntries = materializeResidentEntriesSync(this.#fileEntries, this.#residentBlobStores());
2987
+ const materializedEntries = materializeResidentEntriesForReadSync(
2988
+ this.#fileEntries,
2989
+ this.#residentBlobStores(),
2990
+ );
2951
2991
  const restoreResidentStateAfterFailure = (): void => {
2952
2992
  this.#fileEntries = materializedEntries;
2953
2993
  this.#resetResidentTextBlobStore();
@@ -4006,7 +4046,7 @@ export class SessionManager {
4006
4046
  getLeafEntry(): SessionEntry | undefined {
4007
4047
  if (!this.#leafId) return undefined;
4008
4048
  const entry = this.#byId.get(this.#leafId);
4009
- return entry ? materializeResidentEntrySync(entry, this.#residentBlobStores(), new Map()) : undefined;
4049
+ return entry ? materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), new Map()) : undefined;
4010
4050
  }
4011
4051
 
4012
4052
  /**
@@ -4176,7 +4216,7 @@ export class SessionManager {
4176
4216
  const entry = this.#byId.get(id);
4177
4217
  return entry
4178
4218
  ? rehydrateColdSpillEntry(
4179
- materializeResidentEntrySync(entry, this.#residentBlobStores(), new Map()),
4219
+ materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), new Map()),
4180
4220
  this.#blobStore,
4181
4221
  this.#residentBlobStoresForColdRehydrate(),
4182
4222
  )
@@ -4193,7 +4233,7 @@ export class SessionManager {
4193
4233
  visited.add(current.id);
4194
4234
  path.push(
4195
4235
  rehydrateColdSpillEntry(
4196
- materializeResidentEntrySync(current, this.#residentBlobStores(), cache),
4236
+ materializeResidentEntryForReadSync(current, this.#residentBlobStores(), cache),
4197
4237
  this.#blobStore,
4198
4238
  this.#residentBlobStoresForColdRehydrate(),
4199
4239
  ),
@@ -4233,7 +4273,7 @@ export class SessionManager {
4233
4273
  .filter((entry): entry is SessionEntry => entry.type !== "session")
4234
4274
  .map(entry =>
4235
4275
  rehydrateColdSpillEntry(
4236
- materializeResidentEntrySync(entry, this.#residentBlobStores(), cache),
4276
+ materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), cache),
4237
4277
  this.#blobStore,
4238
4278
  this.#residentBlobStoresForColdRehydrate(),
4239
4279
  ),
@@ -4244,7 +4284,7 @@ export class SessionManager {
4244
4284
  this.#publicMaterializerCallCount++;
4245
4285
  this.#getEntryMaterializerCallCount++;
4246
4286
  const entry = this.#byId.get(id);
4247
- return entry ? materializeResidentEntrySync(entry, this.#residentBlobStores(), new Map()) : undefined;
4287
+ return entry ? materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), new Map()) : undefined;
4248
4288
  }
4249
4289
 
4250
4290
  /**
@@ -4255,7 +4295,7 @@ export class SessionManager {
4255
4295
  const children: SessionEntry[] = [];
4256
4296
  for (const entry of this.#byId.values()) {
4257
4297
  if (entry.parentId === parentId) {
4258
- children.push(materializeResidentEntrySync(entry, this.#residentBlobStores(), cache));
4298
+ children.push(materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), cache));
4259
4299
  }
4260
4300
  }
4261
4301
  return children;
@@ -4310,7 +4350,7 @@ export class SessionManager {
4310
4350
  while (current) {
4311
4351
  if (visited.has(current.id)) break;
4312
4352
  visited.add(current.id);
4313
- path.push(materializeResidentEntrySync(current, this.#residentBlobStores(), cache));
4353
+ path.push(materializeResidentEntryForReadSync(current, this.#residentBlobStores(), cache));
4314
4354
  current = current.parentId ? this.#byId.get(current.parentId) : undefined;
4315
4355
  }
4316
4356
  path.reverse();
@@ -4375,7 +4415,7 @@ export class SessionManager {
4375
4415
  }
4376
4416
  if (entry.type !== "message" && entry.type !== "custom_message")
4377
4417
  return materializeProviderVisibleEntrySync(entry, this.#residentBlobStores());
4378
- const materialized = materializeResidentEntrySync(entry, this.#residentBlobStores(), new Map());
4418
+ const materialized = materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), new Map());
4379
4419
  const rehydrated = rehydrateColdSpillEntry(
4380
4420
  materialized,
4381
4421
  this.#blobStore,
@@ -4434,7 +4474,7 @@ export class SessionManager {
4434
4474
  const resolvedTextBlobCache = new Map<string, string>();
4435
4475
  const materializedEntries = this.#fileEntries
4436
4476
  .filter((e): e is SessionEntry => e.type !== "session")
4437
- .map(entry => materializeResidentEntrySync(entry, this.#residentBlobStores(), resolvedTextBlobCache));
4477
+ .map(entry => materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), resolvedTextBlobCache));
4438
4478
  this.#materializedEntriesCache = materializedEntries;
4439
4479
  this.#materializedEntriesRevision = this.#entryRevision;
4440
4480
  return materializedEntries;
@@ -4591,7 +4631,7 @@ export class SessionManager {
4591
4631
 
4592
4632
  // Filter out LabelEntry from path - we'll recreate them from the resolved map
4593
4633
  const pathWithoutLabels = branchPath.filter(e => e.type !== "label");
4594
- const materializedPathWithoutLabels = materializeResidentEntriesSync(
4634
+ const materializedPathWithoutLabels = materializeResidentEntriesForReadSync(
4595
4635
  pathWithoutLabels,
4596
4636
  this.#residentBlobStores(),
4597
4637
  );
@@ -6,6 +6,7 @@ import type { Args } from "../cli/args";
6
6
  import { tmuxRuntimeSessionPath } from "./session-layout";
7
7
  import { SKC_COORDINATOR_SESSION_ID_ENV, SKC_COORDINATOR_SESSION_STATE_FILE_ENV } from "./session-state-sidecar";
8
8
  import {
9
+ buildSkcTmuxExactSessionTarget,
9
10
  buildSkcTmuxProfileCommands,
10
11
  buildSkcTmuxSessionName,
11
12
  buildSkcTmuxSessionSlug,
@@ -21,6 +22,7 @@ import {
21
22
  import { findSkcTmuxSessionByName, findSkcTmuxSessionByScope, type SkcTmuxSessionStatus } from "./tmux-sessions";
22
23
 
23
24
  export {
25
+ buildSkcTmuxExactSessionTarget,
24
26
  buildSkcTmuxProfileCommands,
25
27
  SKC_DEFAULT_TMUX_SESSION,
26
28
  SKC_TMUX_COMMAND_ENV,
@@ -201,12 +203,24 @@ function buildWindowsPowerShellInnerCommand(context: CommandResolutionContext, r
201
203
  const envLines = Object.entries({ [SKC_TMUX_LAUNCHED_ENV]: "1", ...(context.extraEnv ?? {}) }).map(
202
204
  ([key, value]) => `$env:${key} = ${powershellQuote(value)}`,
203
205
  );
204
- const invocation = ["&", ...command.map(powershellQuote), ...stripRootTmuxFlag(rawArgs).map(powershellQuote)].join(
205
- " ",
206
- );
206
+ // The inner `&` invocation must wrap the resolved skc command in a
207
+ // script-block so the trailing PowerShell exec-policy flags from the
208
+ // outer invocation are not forwarded into the bun / node / .bat
209
+ // binary the skc command resolves to. Without the wrapping, the flags
210
+ // reach the runtime binary and cause an immediate failure that closes
211
+ // the psmux pane before the skc --tmux attach can land.
212
+ const resolvedCommand = command.map(powershellQuote).join(" ");
213
+ const innerArgs = stripRootTmuxFlag(rawArgs).map(powershellQuote).join(" ");
214
+ const invocation = `& { ${resolvedCommand} ${innerArgs} }`;
207
215
  const exitLine = "if ($null -ne $LASTEXITCODE) { exit $LASTEXITCODE } else { exit 1 }";
208
216
  const script = [...envLines, invocation, exitLine].join("\n");
209
- const encodedCommand = Buffer.from(script, "utf16le").toString("base64");
217
+ // PowerShell -EncodedCommand requires a UTF-16LE BOM (0xFF 0xFE) at the
218
+ // start of the decoded buffer; without it pwsh may misinterpret the
219
+ // leading bytes and reject the script with a parse error, which would
220
+ // kill the psmux pane before the skc --tmux attach could land.
221
+ const bom = Buffer.from([0xff, 0xfe]);
222
+ const body = Buffer.from(script, "utf16le");
223
+ const encodedCommand = Buffer.concat([bom, body]).toString("base64");
210
224
  return `pwsh -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand ${encodedCommand}`;
211
225
  }
212
226
 
@@ -395,7 +409,11 @@ function readCurrentBranch(cwd: string): string | null {
395
409
  }
396
410
 
397
411
  function cleanupCreatedTmuxSession(plan: TmuxLaunchPlan, spawnSync: TmuxSpawnSync, options: TmuxSpawnOptions): void {
398
- spawnSync(plan.tmuxCommand, ["kill-session", "-t", `=${plan.sessionName}`], options);
412
+ spawnSync(
413
+ plan.tmuxCommand,
414
+ ["kill-session", "-t", buildSkcTmuxExactSessionTarget(plan.sessionName, { env: options.env })],
415
+ options,
416
+ );
399
417
  }
400
418
  function isTmuxAttachDisconnectError(result: TmuxSpawnResult): boolean {
401
419
  if (result.signalCode === "SIGHUP") return true;
@@ -499,7 +517,11 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
499
517
  };
500
518
 
501
519
  if (plan.attachSessionName) {
502
- const attached = spawnSync(plan.tmuxCommand, ["attach-session", "-t", `=${plan.attachSessionName}`], options);
520
+ const attached = spawnSync(
521
+ plan.tmuxCommand,
522
+ ["attach-session", "-t", buildSkcTmuxExactSessionTarget(plan.attachSessionName, { env })],
523
+ options,
524
+ );
503
525
  if (attached.exitCode === 0) return true;
504
526
  }
505
527
 
@@ -510,7 +532,7 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
510
532
  buildSkcTmuxWindowTitle(plan.project ?? plan.cwd, plan.branch),
511
533
  spawnSync,
512
534
  options,
513
- `=${plan.sessionName}`,
535
+ buildSkcTmuxExactSessionTarget(plan.sessionName, { env }),
514
536
  );
515
537
 
516
538
  const profile = applySkcTmuxProfile({
@@ -535,7 +557,11 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
535
557
  }
536
558
  }
537
559
  if (created.exitCode !== 0) return false;
538
- const attached = spawnSync(plan.tmuxCommand, ["attach-session", "-t", `=${plan.sessionName}`], options);
560
+ const attached = spawnSync(
561
+ plan.tmuxCommand,
562
+ ["attach-session", "-t", buildSkcTmuxExactSessionTarget(plan.sessionName, { env })],
563
+ options,
564
+ );
539
565
  if (attached.exitCode === 0) return true;
540
566
  if (isTmuxAttachDisconnectError(attached)) {
541
567
  (context.diagnosticWriter ?? safeStderrWrite)(formatTmuxLaunchDiagnostic("attach disconnected", attached.stderr));