@sayknow-cli/coding-agent 0.3.7 → 0.3.9

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 (272) hide show
  1. package/dist/types/append-only-mode.d.ts +31 -0
  2. package/dist/types/capability/rule.d.ts +8 -0
  3. package/dist/types/cli/completion-cli.d.ts +41 -0
  4. package/dist/types/cli/config-cli.d.ts +1 -0
  5. package/dist/types/cli/fixture-report.d.ts +20 -0
  6. package/dist/types/cli/fixture-report.test.d.ts +1 -0
  7. package/dist/types/cli/nofile-limit.d.ts +11 -0
  8. package/dist/types/cli/notify-cli.d.ts +7 -0
  9. package/dist/types/cli/update-cli.d.ts +22 -0
  10. package/dist/types/cli.d.ts +124 -1
  11. package/dist/types/commands/completion.d.ts +29 -0
  12. package/dist/types/commands/config.d.ts +3 -0
  13. package/dist/types/commands/deep-interview.d.ts +3 -0
  14. package/dist/types/config/file-lock.d.ts +7 -6
  15. package/dist/types/config/keybindings.d.ts +2 -2
  16. package/dist/types/config/model-profile-activation.d.ts +6 -0
  17. package/dist/types/config/model-profiles.d.ts +1 -0
  18. package/dist/types/config/settings-schema.d.ts +119 -1
  19. package/dist/types/config/settings.d.ts +8 -0
  20. package/dist/types/discovery/index.d.ts +1 -0
  21. package/dist/types/export/html/template.generated.d.ts +1 -1
  22. package/dist/types/export/ttsr.d.ts +14 -3
  23. package/dist/types/extensibility/extensions/runner.d.ts +2 -1
  24. package/dist/types/extensibility/extensions/types.d.ts +8 -0
  25. package/dist/types/goals/runtime.d.ts +1 -0
  26. package/dist/types/hooks/native-skill-hook.d.ts +12 -0
  27. package/dist/types/main.d.ts +4 -1
  28. package/dist/types/modes/components/assistant-message.d.ts +5 -1
  29. package/dist/types/modes/components/model-selector.d.ts +4 -0
  30. package/dist/types/modes/components/queued-message-selector.d.ts +12 -0
  31. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  32. package/dist/types/modes/components/status-line.d.ts +9 -0
  33. package/dist/types/modes/components/thinking-selector.d.ts +2 -2
  34. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  35. package/dist/types/modes/controllers/event-controller.d.ts +5 -0
  36. package/dist/types/modes/controllers/input-controller.d.ts +1 -3
  37. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  38. package/dist/types/modes/interactive-mode.d.ts +4 -0
  39. package/dist/types/modes/print-mode.d.ts +13 -1
  40. package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -1
  41. package/dist/types/modes/rpc/rpc-mode.d.ts +1 -3
  42. package/dist/types/modes/rpc/rpc-socket-security.d.ts +3 -0
  43. package/dist/types/modes/rpc/rpc-types.d.ts +14 -0
  44. package/dist/types/modes/shared/agent-wire/event-contract.d.ts +19 -0
  45. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +23 -1
  46. package/dist/types/modes/types.d.ts +3 -0
  47. package/dist/types/modes/utils/injected-user-submission.d.ts +52 -0
  48. package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
  49. package/dist/types/notifications/config-commands.d.ts +9 -0
  50. package/dist/types/notifications/config.d.ts +30 -1
  51. package/dist/types/notifications/daemon-paths.d.ts +1 -0
  52. package/dist/types/notifications/index.d.ts +8 -3
  53. package/dist/types/notifications/lifecycle-commands.d.ts +12 -3
  54. package/dist/types/notifications/lifecycle-control-runtime.d.ts +2 -1
  55. package/dist/types/notifications/reply-sent-store.d.ts +53 -0
  56. package/dist/types/notifications/rich-draft.d.ts +68 -0
  57. package/dist/types/notifications/rich-render.d.ts +90 -0
  58. package/dist/types/notifications/telegram-daemon.d.ts +45 -3
  59. package/dist/types/notifications/telegram-reference.d.ts +7 -5
  60. package/dist/types/notifications/threaded-render.d.ts +14 -0
  61. package/dist/types/runtime-api-key.d.ts +7 -0
  62. package/dist/types/sdk.d.ts +3 -0
  63. package/dist/types/session/agent-session.d.ts +61 -6
  64. package/dist/types/session/agent-storage.d.ts +1 -0
  65. package/dist/types/session/file-mention-pruning.d.ts +8 -0
  66. package/dist/types/session/messages.d.ts +4 -0
  67. package/dist/types/session/session-manager.d.ts +17 -1
  68. package/dist/types/skc-runtime/deep-interview-ambiguity.d.ts +39 -0
  69. package/dist/types/skc-runtime/deep-interview-recorder.d.ts +10 -1
  70. package/dist/types/skc-runtime/deep-interview-state.d.ts +17 -2
  71. package/dist/types/skc-runtime/session-state-sidecar.d.ts +7 -0
  72. package/dist/types/skc-runtime/state-writer.d.ts +3 -0
  73. package/dist/types/skc-runtime/tmux-common.d.ts +1 -0
  74. package/dist/types/skc-runtime/ultragoal-receipt-freshness.d.ts +42 -0
  75. package/dist/types/skc-runtime/ultragoal-runtime.d.ts +120 -13
  76. package/dist/types/skill-state/active-state.d.ts +18 -1
  77. package/dist/types/system-prompt.d.ts +18 -2
  78. package/dist/types/task/executor.d.ts +3 -0
  79. package/dist/types/task/fork-context-budget.d.ts +7 -0
  80. package/dist/types/task/token-log.d.ts +30 -0
  81. package/dist/types/task/token-log.test.d.ts +1 -0
  82. package/dist/types/tools/bisect.d.ts +81 -0
  83. package/dist/types/tools/computer.d.ts +2 -2
  84. package/dist/types/tools/index.d.ts +1 -0
  85. package/dist/types/tools/search.d.ts +2 -2
  86. package/dist/types/tools/ultragoal-ask-guard.d.ts +9 -2
  87. package/dist/types/utils/file-mentions.d.ts +14 -0
  88. package/dist/types/utils/git.d.ts +32 -0
  89. package/dist/types/utils/pasted-image-path.d.ts +30 -0
  90. package/dist/types/web/scrapers/youtube.d.ts +8 -0
  91. package/dist/types/workflow/workflow-intent-diff.d.ts +8 -2
  92. package/dist/types/workflow/workflow-intent-report-consumer.d.ts +50 -0
  93. package/dist/types/workflow/workflow-intent-report-schema.d.ts +2 -0
  94. package/dist/types/workflow/workflow-intent-report.d.ts +48 -0
  95. package/package.json +8 -8
  96. package/src/append-only-mode.ts +58 -0
  97. package/src/autoresearch/prompt.md +1 -1
  98. package/src/autoresearch/tools/log-experiment.ts +1 -1
  99. package/src/capability/rule.ts +9 -0
  100. package/src/cli/completion-cli.ts +234 -0
  101. package/src/cli/config-cli.ts +61 -9
  102. package/src/cli/fast-help.ts +1 -0
  103. package/src/cli/fixture-report.test.ts +230 -0
  104. package/src/cli/fixture-report.ts +171 -0
  105. package/src/cli/nofile-limit.ts +48 -0
  106. package/src/cli/notify-cli.ts +103 -17
  107. package/src/cli/telegram-cli.ts +1 -1
  108. package/src/cli/update-cli.ts +148 -33
  109. package/src/cli.ts +29 -1
  110. package/src/commands/completion.ts +84 -0
  111. package/src/commands/config.ts +2 -0
  112. package/src/commands/contribution-prep.ts +1 -1
  113. package/src/commands/deep-interview.ts +2 -1
  114. package/src/config/file-lock.ts +73 -34
  115. package/src/config/keybindings.ts +3 -3
  116. package/src/config/model-profile-activation.ts +65 -0
  117. package/src/config/model-profiles.ts +31 -2
  118. package/src/config/settings-schema.ts +76 -1
  119. package/src/config/settings.ts +22 -0
  120. package/src/defaults/skc/skills/team/SKILL.md +17 -14
  121. package/src/defaults/skc/skills/ultragoal/SKILL.md +59 -4
  122. package/src/defaults/skc/skills/ultragoal/pipeline-validation-contracts.md +30 -0
  123. package/src/defaults/skc-defaults.ts +9 -0
  124. package/src/discovery/helpers.ts +10 -0
  125. package/src/discovery/index.ts +1 -0
  126. package/src/export/html/template.generated.ts +1 -1
  127. package/src/export/html/template.js +60 -2
  128. package/src/export/ttsr.ts +96 -18
  129. package/src/extensibility/extensions/runner.ts +183 -219
  130. package/src/extensibility/extensions/types.ts +9 -0
  131. package/src/extensibility/skc-plugins/runtime-adapters.ts +156 -44
  132. package/src/goals/runtime.ts +10 -2
  133. package/src/hindsight/backend.ts +1 -3
  134. package/src/hooks/native-skill-hook.ts +124 -4
  135. package/src/internal-urls/docs-index.generated.ts +17 -8
  136. package/src/lsp/client.ts +1 -0
  137. package/src/main.ts +95 -43
  138. package/src/memories/index.ts +12 -1
  139. package/src/modes/components/assistant-message.ts +241 -82
  140. package/src/modes/components/custom-editor.ts +1 -0
  141. package/src/modes/components/footer.ts +7 -2
  142. package/src/modes/components/model-selector.ts +54 -11
  143. package/src/modes/components/queued-message-selector.ts +92 -0
  144. package/src/modes/components/settings-selector.ts +4 -0
  145. package/src/modes/components/status-line.ts +167 -54
  146. package/src/modes/components/thinking-selector.ts +10 -8
  147. package/src/modes/components/tool-execution.ts +44 -19
  148. package/src/modes/components/welcome.ts +181 -44
  149. package/src/modes/controllers/command-controller.ts +40 -4
  150. package/src/modes/controllers/event-controller.ts +77 -49
  151. package/src/modes/controllers/extension-ui-controller.ts +12 -11
  152. package/src/modes/controllers/input-controller.ts +192 -35
  153. package/src/modes/controllers/selector-controller.ts +168 -24
  154. package/src/modes/interactive-mode.ts +56 -16
  155. package/src/modes/print-mode.ts +43 -4
  156. package/src/modes/prompt-action-autocomplete.ts +21 -20
  157. package/src/modes/rpc/rpc-mode.ts +32 -12
  158. package/src/modes/rpc/rpc-socket-security.ts +13 -1
  159. package/src/modes/rpc/rpc-types.ts +9 -0
  160. package/src/modes/shared/agent-wire/command-validation.ts +4 -0
  161. package/src/modes/shared/agent-wire/event-contract.ts +22 -0
  162. package/src/modes/shared/agent-wire/event-envelope.ts +82 -0
  163. package/src/modes/shared/agent-wire/scopes.ts +1 -0
  164. package/src/modes/types.ts +3 -0
  165. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  166. package/src/modes/utils/injected-user-submission.ts +94 -0
  167. package/src/modes/utils/ui-helpers.ts +20 -7
  168. package/src/notifications/config-commands.ts +20 -0
  169. package/src/notifications/config.ts +52 -5
  170. package/src/notifications/daemon-paths.ts +2 -0
  171. package/src/notifications/index.ts +144 -18
  172. package/src/notifications/lifecycle-commands.ts +74 -23
  173. package/src/notifications/lifecycle-control-runtime.ts +8 -3
  174. package/src/notifications/lifecycle-orchestrator.ts +19 -6
  175. package/src/notifications/reply-sent-store.ts +134 -0
  176. package/src/notifications/rich-draft.ts +107 -0
  177. package/src/notifications/rich-render.ts +142 -0
  178. package/src/notifications/telegram-daemon-control.ts +5 -3
  179. package/src/notifications/telegram-daemon.ts +606 -98
  180. package/src/notifications/telegram-reference.ts +17 -16
  181. package/src/notifications/threaded-render.ts +45 -2
  182. package/src/prompts/agents/architect.md +19 -10
  183. package/src/prompts/agents/critic.md +16 -18
  184. package/src/prompts/agents/executor.md +14 -11
  185. package/src/prompts/agents/planner.md +18 -13
  186. package/src/prompts/ci-green-request.md +3 -8
  187. package/src/prompts/goals/goal-continuation.md +0 -5
  188. package/src/prompts/goals/goal-mode-active.md +0 -5
  189. package/src/prompts/system/agent-creation-architect.md +2 -2
  190. package/src/prompts/system/plan-mode-active.md +1 -1
  191. package/src/prompts/system/plan-mode-subagent.md +3 -3
  192. package/src/prompts/system/project-prompt.md +0 -13
  193. package/src/prompts/system/subagent-system-prompt.md +3 -2
  194. package/src/prompts/system/system-prompt.md +29 -17
  195. package/src/prompts/system/volatile-project-context.md +10 -0
  196. package/src/prompts/system/web-search.md +4 -7
  197. package/src/prompts/tools/apply-patch.md +6 -0
  198. package/src/prompts/tools/ast-edit.md +1 -0
  199. package/src/prompts/tools/bash.md +1 -1
  200. package/src/prompts/tools/bisect.md +22 -0
  201. package/src/prompts/tools/browser.md +1 -1
  202. package/src/prompts/tools/computer.md +2 -2
  203. package/src/prompts/tools/cron.md +0 -2
  204. package/src/prompts/tools/eval.md +2 -2
  205. package/src/prompts/tools/find.md +1 -1
  206. package/src/prompts/tools/github.md +6 -6
  207. package/src/prompts/tools/goal.md +1 -3
  208. package/src/prompts/tools/image-gen.md +12 -5
  209. package/src/prompts/tools/irc.md +5 -4
  210. package/src/prompts/tools/job.md +2 -2
  211. package/src/prompts/tools/lsp.md +1 -1
  212. package/src/prompts/tools/monitor.md +2 -2
  213. package/src/prompts/tools/patch.md +2 -2
  214. package/src/prompts/tools/replace.md +2 -21
  215. package/src/prompts/tools/rewind.md +1 -1
  216. package/src/prompts/tools/search-tool-bm25.md +6 -11
  217. package/src/prompts/tools/search.md +1 -1
  218. package/src/prompts/tools/skill.md +3 -2
  219. package/src/prompts/tools/ssh.md +2 -1
  220. package/src/prompts/tools/subagent.md +2 -2
  221. package/src/prompts/tools/task.md +4 -5
  222. package/src/prompts/tools/todo-write.md +1 -1
  223. package/src/prompts/tools/vim.md +1 -0
  224. package/src/prompts/tools/web-search.md +2 -0
  225. package/src/prompts/tools/write.md +8 -2
  226. package/src/runtime-api-key.ts +16 -0
  227. package/src/sdk.ts +88 -36
  228. package/src/session/agent-session.ts +1013 -141
  229. package/src/session/agent-storage.ts +4 -0
  230. package/src/session/file-mention-pruning.ts +64 -0
  231. package/src/session/messages.ts +4 -0
  232. package/src/session/session-manager.ts +103 -4
  233. package/src/skc-runtime/deep-interview-ambiguity.ts +206 -0
  234. package/src/skc-runtime/deep-interview-recorder.ts +62 -8
  235. package/src/skc-runtime/deep-interview-state.ts +44 -2
  236. package/src/skc-runtime/session-state-sidecar.ts +263 -20
  237. package/src/skc-runtime/state-runtime.ts +12 -2
  238. package/src/skc-runtime/state-writer.ts +15 -0
  239. package/src/skc-runtime/tmux-common.ts +1 -0
  240. package/src/skc-runtime/ultragoal-guard.ts +55 -66
  241. package/src/skc-runtime/ultragoal-receipt-freshness.ts +315 -0
  242. package/src/skc-runtime/ultragoal-runtime.ts +1672 -159
  243. package/src/skc-runtime/workflow-manifest.generated.json +113 -2
  244. package/src/skc-runtime/workflow-manifest.ts +74 -1
  245. package/src/skill-state/active-state.ts +141 -5
  246. package/src/skill-state/deep-interview-mutation-guard.ts +67 -3
  247. package/src/slash-commands/builtin-registry.ts +296 -72
  248. package/src/system-prompt.ts +57 -16
  249. package/src/task/executor.ts +132 -24
  250. package/src/task/fork-context-advisory.ts +2 -7
  251. package/src/task/fork-context-budget.ts +9 -0
  252. package/src/task/index.ts +13 -6
  253. package/src/task/output-manager.ts +20 -8
  254. package/src/task/receipt.ts +26 -7
  255. package/src/task/token-log.test.ts +188 -0
  256. package/src/task/token-log.ts +179 -0
  257. package/src/tools/bisect.ts +367 -0
  258. package/src/tools/computer.ts +246 -43
  259. package/src/tools/index.ts +3 -0
  260. package/src/tools/output-meta.ts +116 -4
  261. package/src/tools/search.ts +8 -2
  262. package/src/tools/skill.ts +15 -0
  263. package/src/tools/telegram-send.ts +6 -0
  264. package/src/tools/ultragoal-ask-guard.ts +39 -5
  265. package/src/utils/file-mentions.ts +39 -7
  266. package/src/utils/git.ts +73 -0
  267. package/src/utils/pasted-image-path.ts +170 -0
  268. package/src/web/scrapers/youtube.ts +24 -12
  269. package/src/workflow/workflow-intent-diff.ts +29 -6
  270. package/src/workflow/workflow-intent-report-consumer.ts +221 -0
  271. package/src/workflow/workflow-intent-report-schema.ts +129 -0
  272. package/src/workflow/workflow-intent-report.ts +135 -0
@@ -31,6 +31,13 @@ export interface DeepInterviewEstablishedFact {
31
31
  dimension?: string;
32
32
  evidence?: string;
33
33
  disputed: boolean;
34
+ /**
35
+ * Resolution pointer for a disputed fact: the id of the fact that replaced it
36
+ * after the user confirmed a pivot. A disputed fact without `superseded_by`
37
+ * keeps the deterministic ambiguity floor elevated; setting it releases the
38
+ * pressure while preserving the contradicted fact for audit.
39
+ */
40
+ superseded_by?: string;
34
41
  }
35
42
 
36
43
  export interface DeepInterviewTriggerMetadata {
@@ -66,7 +73,12 @@ export interface DeepInterviewRoundRecord {
66
73
  answered_at: string;
67
74
  scored_at?: string;
68
75
  scores?: Record<string, number>;
76
+ /** Effective ambiguity after the deterministic floor clamp (`max(reported, floor)`). */
69
77
  ambiguity?: number;
78
+ /** Original LLM-reported ambiguity, preserved for audit when the floor clamped it. */
79
+ reported_ambiguity?: number;
80
+ /** Deterministic floor in effect when this round was scored, when it clamped. */
81
+ ambiguity_floor?: number;
70
82
  triggers?: DeepInterviewTriggerMetadata[];
71
83
  }
72
84
 
@@ -125,6 +137,7 @@ const TRANSCRIPT_STATE_FIELDS = [
125
137
  "rounds",
126
138
  "established_facts",
127
139
  "current_ambiguity",
140
+ "ambiguity_floor",
128
141
  "topology",
129
142
  "ontology_snapshots",
130
143
  "auto_researched_rounds",
@@ -150,13 +163,39 @@ const HOISTED_STATE_FIELDS = [
150
163
  "threshold_source",
151
164
  ] as const;
152
165
 
166
+ /**
167
+ * Envelope-reserved keys that are never legitimate interview `state` fields.
168
+ *
169
+ * A malformed write that wraps a whole envelope under `state`
170
+ * (`skc state deep-interview write --input '{"state": <envelope>}'`) leaks these
171
+ * into the nested state. Because normalization otherwise preserves unknown
172
+ * nested fields, they would accrete a recursive `state.state` chain (plus stale
173
+ * `receipt`/`skill`/`version`/... duplicates) that no later merge or write ever
174
+ * removes, permanently corrupting the shape. They are stripped from `state` on
175
+ * normalize so the canonical envelope self-heals on the next write.
176
+ */
177
+ const ENVELOPE_RESERVED_STATE_KEYS = [
178
+ "state",
179
+ "receipt",
180
+ "skill",
181
+ "version",
182
+ "updated_at",
183
+ "active",
184
+ "current_phase",
185
+ "state_revision",
186
+ "session_id",
187
+ ] as const;
188
+
153
189
  /**
154
190
  * Canonicalize a deep-interview envelope: interview data nested under `state`,
155
191
  * legacy flattened fields hoisted in losslessly, transcript duplicates removed
156
192
  * from the top level, and `rounds`/`established_facts` guaranteed to be arrays.
157
193
  *
158
- * Idempotent: a canonical envelope is returned unchanged in shape. Never deletes
159
- * unknown envelope or nested fields, and never mutates the input.
194
+ * Idempotent: a canonical envelope is returned unchanged in shape. Preserves all
195
+ * unknown envelope and nested fields except the envelope-reserved keys that leak
196
+ * into `state` (see `ENVELOPE_RESERVED_STATE_KEYS`), which are stripped so a
197
+ * malformed envelope-in-state write cannot permanently nest state. Never mutates
198
+ * the input.
160
199
  */
161
200
  export function normalizeDeepInterviewEnvelope(value: unknown): DeepInterviewStateEnvelope {
162
201
  const envelope: DeepInterviewStateEnvelope = isPlainObject(value) ? { ...value } : {};
@@ -169,6 +208,9 @@ export function normalizeDeepInterviewEnvelope(value: unknown): DeepInterviewSta
169
208
  for (const field of HOISTED_STATE_FIELDS) {
170
209
  if (inner[field] === undefined && envelope[field] !== undefined) inner[field] = envelope[field];
171
210
  }
211
+ for (const field of ENVELOPE_RESERVED_STATE_KEYS) {
212
+ if (field in inner) delete inner[field];
213
+ }
172
214
 
173
215
  if (!Array.isArray(inner.rounds)) inner.rounds = [];
174
216
  if (!Array.isArray(inner.established_facts)) inner.established_facts = [];
@@ -3,15 +3,32 @@ import * as fs from "node:fs/promises";
3
3
  import * as path from "node:path";
4
4
  import type { AssistantMessage } from "@sayknow-cli/ai";
5
5
  import { logger, postmortem } from "@sayknow-cli/utils";
6
+ import { sessionRuntimeDir } from "./session-layout";
6
7
 
7
8
  export const SKC_COORDINATOR_SESSION_STATE_FILE_ENV = "SKC_COORDINATOR_SESSION_STATE_FILE";
8
9
  export const SKC_COORDINATOR_SESSION_ID_ENV = "SKC_COORDINATOR_SESSION_ID";
9
10
  export const SKC_COORDINATOR_SESSION_BRANCH_ENV = "SKC_COORDINATOR_SESSION_BRANCH";
11
+ const SKC_SESSION_PROMPT_ACCEPTED_JSON_ENV = "SKC_SESSION_PROMPT_ACCEPTED_JSON";
12
+ const SKC_SESSION_WORKTREE_BASELINE_DIRTY_ENV = "SKC_SESSION_WORKTREE_BASELINE_DIRTY";
10
13
 
11
14
  export type RuntimeState = "ready_for_input" | "running" | "needs_user_input" | "completed" | "errored";
12
15
 
13
16
  type FinalResponseSource = "agent_end" | "launch_error";
14
17
  const MAX_PUBLIC_ERROR_MESSAGE_LENGTH = 2000;
18
+ const HEARTBEAT_MS = 1000;
19
+
20
+ type LastPayloadCacheEntry = { mtimeMs: number; size: number; payload: Record<string, unknown> };
21
+ const lastPayloadByStateFile = new Map<string, LastPayloadCacheEntry>();
22
+
23
+ /** Test-only counters for runtime sidecar hot-path assertions. */
24
+ export const __sessionStateSidecarPerfCounters = {
25
+ persistFromEventCalls: 0,
26
+ reset(): void {
27
+ this.persistFromEventCalls = 0;
28
+ },
29
+ };
30
+
31
+ const lastWrittenPayloadByStateFile = new Map<string, Record<string, unknown>>();
15
32
 
16
33
  interface RuntimeStateEvent {
17
34
  type: string;
@@ -31,6 +48,7 @@ interface RuntimeStateSidecarPayload {
31
48
  state?: unknown;
32
49
  ready_for_input?: unknown;
33
50
  cwd?: unknown;
51
+ workdir?: unknown;
34
52
  session_file?: unknown;
35
53
  final_response?: { source?: unknown };
36
54
  }
@@ -52,6 +70,11 @@ function sameResolvedPath(left: string, right: string): boolean {
52
70
  return path.resolve(left) === path.resolve(right);
53
71
  }
54
72
 
73
+ function isCoordinatorOwnedStateFile(stateFile: string): boolean {
74
+ const coordinatorStateFile = process.env[SKC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim();
75
+ return !!coordinatorStateFile && sameResolvedPath(coordinatorStateFile, stateFile);
76
+ }
77
+
55
78
  export async function readTerminalRuntimeStateMarker(input: {
56
79
  stateFile?: string | null;
57
80
  sessionId?: string | null;
@@ -124,7 +147,7 @@ function finalResponseForEvent(event: RuntimeStateEvent): {
124
147
  };
125
148
  }
126
149
 
127
- function stateForEvent(event: RuntimeStateEvent): RuntimeState | null {
150
+ export function stateForEvent(event: RuntimeStateEvent): RuntimeState | null {
128
151
  if (event.type === "agent_start" || event.type === "turn_start") return "running";
129
152
  if (event.type === "agent_end") {
130
153
  const assistant = lastAssistant(event.messages);
@@ -134,6 +157,10 @@ function stateForEvent(event: RuntimeStateEvent): RuntimeState | null {
134
157
  return null;
135
158
  }
136
159
 
160
+ export function eventAffectsCoordinatorRuntimeState(event: RuntimeStateEvent): boolean {
161
+ return stateForEvent(event) !== null;
162
+ }
163
+
137
164
  function readPreviousPayload(stateFile: string): Record<string, unknown> {
138
165
  try {
139
166
  return JSON.parse(fsSync.readFileSync(stateFile, "utf8")) as Record<string, unknown>;
@@ -142,12 +169,92 @@ function readPreviousPayload(stateFile: string): Record<string, unknown> {
142
169
  }
143
170
  }
144
171
 
145
- function shouldPreserveTerminalPayload(previous: RuntimeStateSidecarPayload): boolean {
172
+ async function readPreviousPayloadForEvent(stateFile: string): Promise<Record<string, unknown>> {
173
+ if (!isCoordinatorOwnedStateFile(stateFile)) {
174
+ const cachedWritten = lastWrittenPayloadByStateFile.get(stateFile);
175
+ if (cachedWritten) return cachedWritten;
176
+ }
177
+ let stat: Awaited<ReturnType<typeof fs.stat>>;
178
+ try {
179
+ stat = await fs.stat(stateFile);
180
+ } catch {
181
+ lastPayloadByStateFile.delete(stateFile);
182
+ return {};
183
+ }
184
+ const cached = lastPayloadByStateFile.get(stateFile);
185
+ if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) return cached.payload;
186
+ try {
187
+ const payload = JSON.parse(await Bun.file(stateFile).text()) as Record<string, unknown>;
188
+ lastPayloadByStateFile.set(stateFile, { mtimeMs: stat.mtimeMs, size: stat.size, payload });
189
+ return payload;
190
+ } catch {
191
+ lastPayloadByStateFile.delete(stateFile);
192
+ return {};
193
+ }
194
+ }
195
+
196
+ function withoutUpdatedAt(payload: Record<string, unknown>): Record<string, unknown> {
197
+ const { updated_at: _updatedAt, ...rest } = payload;
198
+ return rest;
199
+ }
200
+
201
+ function shouldSkipRuntimeStateWrite(
202
+ previous: Record<string, unknown>,
203
+ payload: Record<string, unknown>,
204
+ nowMs: number,
205
+ ): boolean {
206
+ if (payload.state === "completed" || payload.state === "errored") return false;
207
+ if (previous.state !== payload.state) return false;
208
+ if (previous.state !== "running" || payload.state !== "running") return false;
209
+ if (JSON.stringify(withoutUpdatedAt(previous)) !== JSON.stringify(withoutUpdatedAt(payload))) return false;
210
+ const previousUpdatedAt = typeof previous.updated_at === "string" ? Date.parse(previous.updated_at) : NaN;
211
+ if (!Number.isFinite(previousUpdatedAt)) return false;
212
+ return nowMs - previousUpdatedAt < HEARTBEAT_MS;
213
+ }
214
+
215
+ function rememberWrittenPayload(stateFile: string, payload: Record<string, unknown>): void {
216
+ lastWrittenPayloadByStateFile.set(stateFile, payload);
217
+ try {
218
+ const stat = fsSync.statSync(stateFile);
219
+ lastPayloadByStateFile.set(stateFile, { mtimeMs: stat.mtimeMs, size: stat.size, payload });
220
+ } catch {
221
+ lastPayloadByStateFile.delete(stateFile);
222
+ }
223
+ }
224
+ function shouldPreserveTerminalPayload(
225
+ previous: RuntimeStateSidecarPayload,
226
+ input: { sessionId: string; cwd: string; sessionFile?: string | null },
227
+ ): boolean {
146
228
  if (previous.state !== "completed" && previous.state !== "errored") return false;
147
229
  const source = previous.final_response?.source;
148
- return source === "agent_end" || source === "launch_error";
230
+ if (source !== "agent_end" && source !== "launch_error") return false;
231
+ if (typeof previous.session_id === "string" && previous.session_id !== input.sessionId) return false;
232
+ if (typeof previous.cwd === "string" && !sameResolvedPath(previous.cwd, input.cwd)) return false;
233
+ if (typeof previous.workdir === "string" && !sameResolvedPath(previous.workdir, input.cwd)) return false;
234
+ if (
235
+ input.sessionFile &&
236
+ typeof previous.session_file === "string" &&
237
+ !sameResolvedPath(previous.session_file, input.sessionFile)
238
+ ) {
239
+ return false;
240
+ }
241
+ return true;
149
242
  }
150
243
 
244
+ function cachedTerminalPayload(
245
+ stateFile: string,
246
+ input: { sessionId: string; cwd: string; sessionFile?: string | null },
247
+ ): RuntimeStateSidecarPayload | null {
248
+ const cached = lastWrittenPayloadByStateFile.get(stateFile) as RuntimeStateSidecarPayload | undefined;
249
+ return cached && shouldPreserveTerminalPayload(cached, input) ? cached : null;
250
+ }
251
+
252
+ function runtimeStateFileForContext(context: RuntimeStateContext): string | null {
253
+ const explicit = process.env[SKC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim();
254
+ if (explicit) return explicit;
255
+ if (!context.sessionId.trim()) return null;
256
+ return path.join(sessionRuntimeDir(context.cwd, context.sessionId), "runtime-state.json");
257
+ }
151
258
  function branchForContext(context: RuntimeStateContext): string | null {
152
259
  return context.branch ?? (process.env[SKC_COORDINATOR_SESSION_BRANCH_ENV]?.trim() || null);
153
260
  }
@@ -160,10 +267,11 @@ function basePayload(input: {
160
267
  source: string;
161
268
  event: string;
162
269
  reason: string | null;
270
+ sessionId: string;
163
271
  }): Record<string, unknown> {
164
272
  return {
165
273
  schema_version: 1,
166
- session_id: process.env[SKC_COORDINATOR_SESSION_ID_ENV]?.trim() || input.context.sessionId,
274
+ session_id: input.sessionId,
167
275
  state: input.state,
168
276
  ready_for_input: input.state === "completed" || input.state === "ready_for_input",
169
277
  updated_at: input.now,
@@ -179,6 +287,56 @@ function basePayload(input: {
179
287
  session_file: input.context.sessionFile ?? null,
180
288
  };
181
289
  }
290
+ function booleanFromUnknown(value: unknown): boolean | null {
291
+ if (typeof value === "boolean") return value;
292
+ if (typeof value === "string") {
293
+ const normalized = value.trim().toLowerCase();
294
+ if (normalized === "true") return true;
295
+ if (normalized === "false") return false;
296
+ }
297
+ return null;
298
+ }
299
+
300
+ function promptAcceptedFromEnv(): boolean {
301
+ const promptAcceptedJson = process.env[SKC_SESSION_PROMPT_ACCEPTED_JSON_ENV]?.trim();
302
+ if (!promptAcceptedJson) return false;
303
+ try {
304
+ return fsSync.statSync(promptAcceptedJson).size > 0;
305
+ } catch {
306
+ return false;
307
+ }
308
+ }
309
+
310
+ function readJsonFileSync(file: string): Record<string, unknown> | null {
311
+ try {
312
+ return JSON.parse(fsSync.readFileSync(file, "utf8")) as Record<string, unknown>;
313
+ } catch {
314
+ return null;
315
+ }
316
+ }
317
+
318
+ function worktreeBaselineDirtyFromEnvOrMarker(): boolean | null {
319
+ const promptAcceptedJson = process.env[SKC_SESSION_PROMPT_ACCEPTED_JSON_ENV]?.trim();
320
+ if (promptAcceptedJson) {
321
+ const promptAccepted = readJsonFileSync(promptAcceptedJson);
322
+ const promptBaseline = booleanFromUnknown(promptAccepted?.worktreeBaselineDirty);
323
+ if (promptBaseline !== null) return promptBaseline;
324
+ }
325
+ const envValue = booleanFromUnknown(process.env[SKC_SESSION_WORKTREE_BASELINE_DIRTY_ENV]);
326
+ if (envValue !== null) return envValue;
327
+ return null;
328
+ }
329
+
330
+ function observedRecoverableWorktreeChanges(cwd: string): boolean {
331
+ if (!cwd.trim()) return false;
332
+ try {
333
+ const proc = Bun.spawnSync(["git", "status", "--porcelain"], { cwd, stdout: "pipe", stderr: "pipe" });
334
+ return proc.exitCode === 0 && proc.stdout.byteLength > 0;
335
+ } catch {
336
+ return false;
337
+ }
338
+ }
339
+
182
340
  function publicSafeErrorMessage(message: string): string {
183
341
  const normalized = message.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, " ").trim();
184
342
  if (normalized.length <= MAX_PUBLIC_ERROR_MESSAGE_LENGTH) return normalized;
@@ -193,32 +351,75 @@ function numericProcessExitCode(defaultCode: number | null): number | null {
193
351
  return typeof process.exitCode === "number" ? process.exitCode : defaultCode;
194
352
  }
195
353
 
196
- function postmortemExitDetails(reason: postmortem.Reason): {
354
+ function postmortemExitDetails(
355
+ reason: postmortem.Reason,
356
+ previous: RuntimeStateSidecarPayload,
357
+ cwd: string,
358
+ ): {
197
359
  state: RuntimeState;
198
360
  reason: string;
199
361
  exitKind: string;
200
362
  exitCode: number | null;
201
363
  signal: string | null;
202
364
  error?: { code: string; message: string; recoverable: true };
365
+ recovery?: { action: string; reason: string };
366
+ promptAccepted: boolean;
367
+ observedRecoverableWorktreeChanges: boolean;
368
+ worktreeBaselineDirty: boolean | null;
369
+ worktreeChangedSinceBaseline: boolean;
203
370
  } {
371
+ const promptAccepted = promptAcceptedFromEnv();
372
+ const observedChanges = observedRecoverableWorktreeChanges(typeof previous.cwd === "string" ? previous.cwd : cwd);
373
+ const worktreeBaselineDirty = worktreeBaselineDirtyFromEnvOrMarker();
374
+ const worktreeChangedSinceBaseline = worktreeBaselineDirty === false && observedChanges;
204
375
  if (reason === postmortem.Reason.EXIT || reason === postmortem.Reason.MANUAL) {
205
376
  const exitCode = numericProcessExitCode(0) ?? 0;
206
- const state: RuntimeState = exitCode === 0 ? "completed" : "errored";
377
+ const exitedBeforeTerminalState =
378
+ exitCode === 0 && reason === postmortem.Reason.EXIT && previous.state === "running";
379
+ const state: RuntimeState = exitCode === 0 && !exitedBeforeTerminalState ? "completed" : "errored";
380
+ const exitReason = exitedBeforeTerminalState
381
+ ? "process_exit_before_terminal_state"
382
+ : reason === postmortem.Reason.EXIT
383
+ ? "process_exit"
384
+ : "manual_cleanup";
385
+ let classifiedReason = exitReason;
386
+ if (exitedBeforeTerminalState) {
387
+ if (!promptAccepted) classifiedReason = "process_exit_before_prompt_acceptance";
388
+ else if (worktreeChangedSinceBaseline)
389
+ classifiedReason = "accepted_prompt_observed_recoverable_worktree_changes";
390
+ else if (observedChanges)
391
+ classifiedReason = "accepted_prompt_dirty_worktree_observed_without_new_change_proof";
392
+ else classifiedReason = "accepted_prompt_no_useful_output";
393
+ }
207
394
  return {
208
395
  state,
209
- reason: reason === postmortem.Reason.EXIT ? "process_exit" : "manual_cleanup",
396
+ reason: classifiedReason,
210
397
  exitKind: reason,
211
398
  exitCode,
212
399
  signal: null,
213
400
  ...(state === "errored"
214
401
  ? {
215
402
  error: {
216
- code: "process_exit",
217
- message: publicSafeErrorMessage(`SKC process exited with code ${exitCode}`),
403
+ code: classifiedReason,
404
+ message: publicSafeErrorMessage(
405
+ exitedBeforeTerminalState
406
+ ? "SKC process exited before emitting terminal agent state"
407
+ : `SKC process exited with code ${exitCode}`,
408
+ ),
218
409
  recoverable: true,
219
410
  },
411
+ recovery: {
412
+ action: "recover_or_resume_session",
413
+ reason: exitedBeforeTerminalState
414
+ ? "previous runtime state was non-terminal; preserve the worktree and inspect the session before retrying"
415
+ : "process exited with a non-zero status",
416
+ },
220
417
  }
221
418
  : {}),
419
+ promptAccepted,
420
+ observedRecoverableWorktreeChanges: observedChanges,
421
+ worktreeBaselineDirty,
422
+ worktreeChangedSinceBaseline,
222
423
  };
223
424
  }
224
425
  const signalByReason: Partial<Record<postmortem.Reason, string>> = {
@@ -233,32 +434,53 @@ function postmortemExitDetails(reason: postmortem.Reason): {
233
434
  exitCode: numericProcessExitCode(null),
234
435
  signal: signalByReason[reason] ?? null,
235
436
  error: { code: reason, message: errorMessageForPostmortem(reason), recoverable: true },
437
+ recovery: { action: "recover_or_resume_session", reason: "process cleanup ran before terminal agent state" },
438
+ promptAccepted,
439
+ observedRecoverableWorktreeChanges: observedChanges,
440
+ worktreeBaselineDirty,
441
+ worktreeChangedSinceBaseline,
236
442
  };
237
443
  }
238
444
 
239
445
  function writeStateFileSync(stateFile: string, payload: Record<string, unknown>): void {
240
446
  fsSync.mkdirSync(path.dirname(stateFile), { recursive: true });
241
- fsSync.writeFileSync(stateFile, `${JSON.stringify(payload, null, 2)}\n`);
447
+ fsSync.writeFileSync(stateFile, `${JSON.stringify(payload)}\n`);
448
+ rememberWrittenPayload(stateFile, payload);
242
449
  }
243
450
 
244
451
  async function writeStateFile(stateFile: string, payload: Record<string, unknown>): Promise<void> {
245
452
  await fs.mkdir(path.dirname(stateFile), { recursive: true });
246
- await Bun.write(stateFile, `${JSON.stringify(payload, null, 2)}\n`);
453
+ await Bun.write(stateFile, `${JSON.stringify(payload)}\n`);
454
+ rememberWrittenPayload(stateFile, payload);
247
455
  }
248
456
 
249
457
  export async function persistCoordinatorRuntimeStateFromEvent(
250
458
  event: RuntimeStateEvent,
251
459
  context: RuntimeStateContext,
252
460
  ): Promise<void> {
253
- const stateFile = process.env[SKC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim();
461
+ __sessionStateSidecarPerfCounters.persistFromEventCalls += 1;
462
+ const stateFile = runtimeStateFileForContext(context);
254
463
  if (!stateFile) return;
255
464
  const state = stateForEvent(event);
256
465
  if (!state) return;
257
- const now = new Date().toISOString();
258
- const previous = readPreviousPayload(stateFile);
466
+ const nowMs = Date.now();
467
+ const now = new Date(nowMs).toISOString();
468
+ const previous = await readPreviousPayloadForEvent(stateFile);
259
469
  const finalResponse = finalResponseForEvent(event);
470
+ const sessionId = process.env[SKC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim()
471
+ ? process.env[SKC_COORDINATOR_SESSION_ID_ENV]?.trim() || context.sessionId
472
+ : context.sessionId;
260
473
  const payload = {
261
- ...basePayload({ context, previous, state, now, source: "agent_session_event", event: event.type, reason: null }),
474
+ ...basePayload({
475
+ context,
476
+ previous,
477
+ state,
478
+ now,
479
+ source: "agent_session_event",
480
+ event: event.type,
481
+ reason: null,
482
+ sessionId,
483
+ }),
262
484
  ...(state === "completed" || state === "errored" ? { ended_at: now } : {}),
263
485
  ...(finalResponse ? { final_response: finalResponse } : {}),
264
486
  ...(state === "errored"
@@ -271,7 +493,9 @@ export async function persistCoordinatorRuntimeStateFromEvent(
271
493
  }
272
494
  : {}),
273
495
  };
496
+ if (state === "completed" || state === "errored") rememberWrittenPayload(stateFile, payload);
274
497
  try {
498
+ if (shouldSkipRuntimeStateWrite(previous, payload, nowMs)) return;
275
499
  await writeStateFile(stateFile, payload);
276
500
  } catch (error) {
277
501
  logger.warn("Failed to persist coordinator runtime state", { error: String(error), stateFile });
@@ -282,12 +506,24 @@ export function persistCoordinatorRuntimeStateFromPostmortem(
282
506
  reason: postmortem.Reason,
283
507
  context: RuntimeStateContext,
284
508
  ): void {
285
- const stateFile = process.env[SKC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim();
509
+ const stateFile = runtimeStateFileForContext(context);
286
510
  if (!stateFile) return;
287
- const previous = readPreviousPayload(stateFile);
288
- if (shouldPreserveTerminalPayload(previous as RuntimeStateSidecarPayload)) return;
511
+ const sessionId = process.env[SKC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim()
512
+ ? process.env[SKC_COORDINATOR_SESSION_ID_ENV]?.trim() || context.sessionId
513
+ : context.sessionId;
514
+ const preserveInput = { sessionId, cwd: context.cwd, sessionFile: context.sessionFile };
515
+ const cachedTerminal = cachedTerminalPayload(stateFile, preserveInput);
516
+ const previous: Record<string, unknown> = cachedTerminal
517
+ ? (cachedTerminal as unknown as Record<string, unknown>)
518
+ : readPreviousPayload(stateFile);
519
+ if (cachedTerminal || shouldPreserveTerminalPayload(previous as RuntimeStateSidecarPayload, preserveInput)) return;
520
+ const previousForDetails: RuntimeStateSidecarPayload =
521
+ (previous as RuntimeStateSidecarPayload).state === "completed" ||
522
+ (previous as RuntimeStateSidecarPayload).state === "errored"
523
+ ? { ...(previous as RuntimeStateSidecarPayload), state: "running" }
524
+ : (previous as RuntimeStateSidecarPayload);
289
525
  const now = new Date().toISOString();
290
- const details = postmortemExitDetails(reason);
526
+ const details = postmortemExitDetails(reason, previousForDetails, context.cwd);
291
527
  const payload = {
292
528
  ...basePayload({
293
529
  context,
@@ -297,6 +533,7 @@ export function persistCoordinatorRuntimeStateFromPostmortem(
297
533
  source: "process_postmortem",
298
534
  event: "process_exit",
299
535
  reason: details.reason,
536
+ sessionId,
300
537
  }),
301
538
  ended_at: now,
302
539
  detected_at: now,
@@ -304,6 +541,12 @@ export function persistCoordinatorRuntimeStateFromPostmortem(
304
541
  exit_code: details.exitCode,
305
542
  signal: details.signal,
306
543
  ...(details.error ? { error: details.error } : {}),
544
+ ...(details.recovery ? { recovery: details.recovery } : {}),
545
+ previous_runtime_state: typeof previous.state === "string" ? previous.state : null,
546
+ prompt_accepted: details.promptAccepted,
547
+ observed_recoverable_worktree_changes: details.observedRecoverableWorktreeChanges,
548
+ worktree_baseline_dirty: details.worktreeBaselineDirty,
549
+ worktree_changed_since_baseline: details.worktreeChangedSinceBaseline,
307
550
  };
308
551
  try {
309
552
  writeStateFileSync(stateFile, payload);
@@ -313,7 +556,7 @@ export function persistCoordinatorRuntimeStateFromPostmortem(
313
556
  }
314
557
 
315
558
  export function registerCoordinatorRuntimeStateFinalizer(context: RuntimeStateContext): () => void {
316
- if (!process.env[SKC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim()) return () => {};
559
+ if (!runtimeStateFileForContext(context)) return () => {};
317
560
  return postmortem.register("coordinator-runtime-state", reason => {
318
561
  persistCoordinatorRuntimeStateFromPostmortem(reason, context);
319
562
  });
@@ -26,6 +26,7 @@ import {
26
26
  type WorkflowStateReceipt,
27
27
  } from "../skill-state/workflow-state-contract";
28
28
  import { renderCliWriteReceipt } from "./cli-write-receipt";
29
+ import { applyAmbiguityFloorToEnvelope } from "./deep-interview-ambiguity";
29
30
  import { mergeDeepInterviewEnvelope, normalizeDeepInterviewEnvelope } from "./deep-interview-state";
30
31
  import { activeSnapshotPath, auditPath, modeStatePath, sessionStateDir } from "./session-layout";
31
32
  import {
@@ -1158,7 +1159,12 @@ export async function reconcileWorkflowSkillState(options: {
1158
1159
 
1159
1160
  const merged =
1160
1161
  mode === "deep-interview"
1161
- ? (mergeDeepInterviewEnvelope(existingPayload, payload) as Record<string, unknown>)
1162
+ ? // Enforce the deterministic ambiguity floor on every reconcile so a
1163
+ // self-reported score can never undercut persisted contradiction evidence.
1164
+ (applyAmbiguityFloorToEnvelope(mergeDeepInterviewEnvelope(existingPayload, payload)).envelope as Record<
1165
+ string,
1166
+ unknown
1167
+ >)
1162
1168
  : mergeWithNullDelete(existingPayload, payload);
1163
1169
  merged.skill = mode;
1164
1170
  merged.current_phase = trimmedPhase;
@@ -1355,7 +1361,11 @@ async function handleWrite(
1355
1361
  if (mode === "deep-interview") {
1356
1362
  // Deep-interview keeps interview data nested under `state` and merges rounds
1357
1363
  // losslessly by durable key; never flatten or delete `state` (that drops recorder history).
1358
- merged = mergeDeepInterviewEnvelope(existingPayload, payload, { replace: hasFlag(args, "--replace") });
1364
+ // The deterministic ambiguity floor is applied after the merge so a reported
1365
+ // score written through the CLI can never undercut persisted contradiction evidence.
1366
+ merged = applyAmbiguityFloorToEnvelope(
1367
+ mergeDeepInterviewEnvelope(existingPayload, payload, { replace: hasFlag(args, "--replace") }),
1368
+ ).envelope;
1359
1369
  } else if (hasFlag(args, "--replace")) {
1360
1370
  merged = { ...payload };
1361
1371
  } else {
@@ -304,6 +304,18 @@ function activeStateDir(cwd: string, sessionScope?: string | ActiveSessionScope)
304
304
  return layoutActiveStateDir(cwd, requireSessionId(sessionScope, "activeStateDir"));
305
305
  }
306
306
 
307
+ type ActiveStateCacheInvalidator = (cwd?: string, sessionId?: string) => void;
308
+ var activeStateCacheInvalidator: ActiveStateCacheInvalidator | undefined;
309
+
310
+ export function setActiveStateCacheInvalidator(invalidator: ActiveStateCacheInvalidator): void {
311
+ activeStateCacheInvalidator = invalidator;
312
+ }
313
+
314
+ function invalidateActiveStateCacheForScope(cwd: string, sessionScope?: string | ActiveSessionScope): void {
315
+ const sessionId = typeof sessionScope === "string" ? sessionScope : sessionScope?.sessionId;
316
+ activeStateCacheInvalidator?.(cwd, sessionId);
317
+ }
318
+
307
319
  function activeSnapshotPath(cwd: string, sessionScope?: string | ActiveSessionScope): string {
308
320
  return layoutActiveSnapshotPath(cwd, requireSessionId(sessionScope, "activeSnapshotPath"));
309
321
  }
@@ -979,6 +991,7 @@ export async function writeActiveEntry(
979
991
  persistedSourceRevision(entry) || persistedSourceRevision(await readJsonIfPresent(filePath)) + 1,
980
992
  },
981
993
  );
994
+ invalidateActiveStateCacheForScope(cwd, sessionScope);
982
995
  return filePath;
983
996
  }
984
997
 
@@ -1003,6 +1016,7 @@ export async function removeActiveEntry(
1003
1016
  }
1004
1017
  const deleted = await atomicRemove(filePath);
1005
1018
  if (deleted) await maybeAudit(filePath, options);
1019
+ if (deleted) invalidateActiveStateCacheForScope(cwd, sessionScope);
1006
1020
  return { path: filePath, deleted };
1007
1021
  },
1008
1022
  options?.lock,
@@ -1049,6 +1063,7 @@ export async function rebuildActiveSnapshot(
1049
1063
  ...entries.map(entry => persistedSourceRevision(entry)),
1050
1064
  ),
1051
1065
  });
1066
+ invalidateActiveStateCacheForScope(cwd, sessionScope);
1052
1067
  return snapshotPath;
1053
1068
  }
1054
1069
 
@@ -4,6 +4,7 @@ import { resolveSkcTmuxBinary } from "./psmux-detect";
4
4
  export const SKC_DEFAULT_TMUX_SESSION = "sayknow_cli";
5
5
  export const SKC_TMUX_SESSION_PREFIX = `${SKC_DEFAULT_TMUX_SESSION}_`;
6
6
  export const SKC_TMUX_COMMAND_ENV = "SKC_TMUX_COMMAND";
7
+ export const SKC_TMUX_ACTIVE_SESSION_ENV = "SKC_TMUX_ACTIVE_SESSION";
7
8
  export const SKC_TMUX_PROFILE_ENV = "SKC_TMUX_PROFILE";
8
9
  export const SKC_TMUX_MOUSE_ENV = "SKC_MOUSE";
9
10
  export const SKC_TMUX_PROFILE_OPTION = "@skc-profile";