@sayknow-cli/coding-agent 0.3.12 → 0.3.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. package/dist/types/autoresearch/tools/init-experiment.d.ts +1 -1
  2. package/dist/types/autoresearch/tools/log-experiment.d.ts +2 -2
  3. package/dist/types/cli/classify-install-target.d.ts +0 -10
  4. package/dist/types/cli/daemon-cli.d.ts +2 -0
  5. package/dist/types/cli/initial-message.d.ts +1 -1
  6. package/dist/types/cli/malloc-env-guard.d.ts +65 -0
  7. package/dist/types/cli/malloc-env-guard.test.d.ts +1 -0
  8. package/dist/types/cli/notify-cli.d.ts +3 -1
  9. package/dist/types/cli/session-picker.d.ts +4 -3
  10. package/dist/types/cli/update-cli.d.ts +22 -3
  11. package/dist/types/cli.d.ts +5 -0
  12. package/dist/types/commands/daemon.d.ts +6 -2
  13. package/dist/types/commands/launch.d.ts +1 -0
  14. package/dist/types/commands/notify.d.ts +6 -0
  15. package/dist/types/commands/web-search.d.ts +1 -1
  16. package/dist/types/commit/agentic/tools/propose-changelog.d.ts +4 -4
  17. package/dist/types/commit/agentic/tools/propose-commit.d.ts +9 -9
  18. package/dist/types/commit/agentic/tools/schemas.d.ts +9 -9
  19. package/dist/types/commit/agentic/tools/split-commit.d.ts +9 -9
  20. package/dist/types/commit/analysis/conventional.d.ts +1 -1
  21. package/dist/types/commit/analysis/summary.d.ts +1 -1
  22. package/dist/types/commit/changelog/generate.d.ts +3 -3
  23. package/dist/types/commit/changelog/index.d.ts +2 -2
  24. package/dist/types/commit/map-reduce/map-phase.d.ts +1 -1
  25. package/dist/types/commit/map-reduce/reduce-phase.d.ts +1 -1
  26. package/dist/types/commit/shared-llm.d.ts +9 -9
  27. package/dist/types/config/keybindings.d.ts +134 -129
  28. package/dist/types/config/model-profile-activation.d.ts +1 -0
  29. package/dist/types/config/model-registry.d.ts +35 -35
  30. package/dist/types/config/models-config-schema.d.ts +111 -111
  31. package/dist/types/config/settings-schema.d.ts +56 -16
  32. package/dist/types/config/settings.d.ts +2 -0
  33. package/dist/types/coordinator/contract.d.ts +1 -1
  34. package/dist/types/coordinator-mcp/model-preset.d.ts +45 -0
  35. package/dist/types/coordinator-mcp/policy.d.ts +3 -0
  36. package/dist/types/coordinator-mcp/server.d.ts +79 -6
  37. package/dist/types/coordinator-mcp/session-reaper.d.ts +53 -0
  38. package/dist/types/daemon/control-types.d.ts +10 -0
  39. package/dist/types/daemon/operator-contract.d.ts +47 -0
  40. package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/models/catalog.d.ts +4 -0
  41. package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/shared/errors.d.ts +12 -12
  42. package/dist/types/discovery/helpers.d.ts +1 -1
  43. package/dist/types/discovery/substitute-plugin-root.d.ts +0 -4
  44. package/dist/types/edit/modes/patch.d.ts +2 -2
  45. package/dist/types/extensibility/extensions/types.d.ts +3 -1
  46. package/dist/types/goals/tools/goal-tool.d.ts +6 -6
  47. package/dist/types/i18n/messages/en.d.ts +8 -8
  48. package/dist/types/internal-urls/local-protocol.d.ts +13 -9
  49. package/dist/types/lsp/index.d.ts +9 -9
  50. package/dist/types/lsp/types.d.ts +9 -9
  51. package/dist/types/main.d.ts +66 -2
  52. package/dist/types/modes/acp/acp-agent.d.ts +19 -2
  53. package/dist/types/modes/acp/acp-mode.d.ts +12 -0
  54. package/dist/types/modes/components/assistant-message.d.ts +1 -1
  55. package/dist/types/modes/components/custom-editor.d.ts +2 -0
  56. package/dist/types/modes/components/hook-selector.d.ts +5 -0
  57. package/dist/types/modes/components/irc-sidebar.d.ts +32 -0
  58. package/dist/types/modes/components/pet-selector.d.ts +11 -0
  59. package/dist/types/modes/components/sayknow-pet-widget.d.ts +59 -0
  60. package/dist/types/modes/components/session-selector.d.ts +22 -9
  61. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  62. package/dist/types/modes/components/status-line/types.d.ts +1 -1
  63. package/dist/types/modes/components/status-line.d.ts +0 -10
  64. package/dist/types/modes/components/tool-execution.d.ts +1 -0
  65. package/dist/types/modes/components/user-message.d.ts +2 -1
  66. package/dist/types/modes/controllers/event-controller.d.ts +4 -0
  67. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  68. package/dist/types/modes/interactive-mode.d.ts +24 -2
  69. package/dist/types/modes/irc-observation-ledger.d.ts +22 -0
  70. package/dist/types/modes/print-mode.d.ts +3 -4
  71. package/dist/types/modes/rpc/rpc-client.d.ts +7 -1
  72. package/dist/types/modes/rpc/rpc-mode.d.ts +2 -1
  73. package/dist/types/modes/rpc/rpc-types.d.ts +17 -1
  74. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +8 -0
  75. package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +5 -0
  76. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +45 -0
  77. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +42 -2
  78. package/dist/types/modes/theme/defaults/index.d.ts +568 -568
  79. package/dist/types/modes/types.d.ts +28 -2
  80. package/dist/types/modes/utils/abort-message.d.ts +1 -1
  81. package/dist/types/modes/utils/context-usage.d.ts +4 -19
  82. package/dist/types/modes/utils/irc-message.d.ts +38 -0
  83. package/dist/types/modes/utils/ui-helpers.d.ts +13 -4
  84. package/dist/types/notifications/config.d.ts +34 -0
  85. package/dist/types/notifications/daemon-paths.d.ts +7 -0
  86. package/dist/types/notifications/index.d.ts +27 -0
  87. package/dist/types/notifications/lifecycle-control-runtime.d.ts +12 -4
  88. package/dist/types/notifications/notification-service.d.ts +144 -0
  89. package/dist/types/notifications/rate-limit-pool.d.ts +27 -9
  90. package/dist/types/notifications/telegram-daemon-control.d.ts +6 -0
  91. package/dist/types/notifications/telegram-daemon.d.ts +130 -3
  92. package/dist/types/notifications/telegram-reference.d.ts +19 -2
  93. package/dist/types/notifications/topic-registry.d.ts +24 -4
  94. package/dist/types/registry/agent-registry.d.ts +2 -0
  95. package/dist/types/rlm/index.d.ts +1 -1
  96. package/dist/types/sdk.d.ts +2 -0
  97. package/dist/types/secrets/obfuscator.d.ts +1 -1
  98. package/dist/types/session/agent-session.d.ts +40 -3
  99. package/dist/types/session/cache-economics.d.ts +79 -0
  100. package/dist/types/session/context-estimation.d.ts +31 -0
  101. package/dist/types/session/provider-safety-stop.d.ts +5 -0
  102. package/dist/types/session/session-manager.d.ts +124 -4
  103. package/dist/types/session/session-storage.d.ts +155 -13
  104. package/dist/types/setup/credential-auto-import.d.ts +2 -2
  105. package/dist/types/skc-runtime/session-state-sidecar.d.ts +34 -3
  106. package/dist/types/skc-runtime/state-schema.d.ts +19 -19
  107. package/dist/types/skc-runtime/tmux-common.d.ts +6 -0
  108. package/dist/types/skc-runtime/tmux-owner-isolation-cli.d.ts +5 -0
  109. package/dist/types/skc-runtime/tmux-owner-isolation.d.ts +309 -0
  110. package/dist/types/skc-runtime/tmux-sessions.d.ts +49 -17
  111. package/dist/types/task/agents.d.ts +2 -2
  112. package/dist/types/task/executor.d.ts +3 -0
  113. package/dist/types/task/receipt.d.ts +3 -1
  114. package/dist/types/task/types.d.ts +35 -27
  115. package/dist/types/tools/ask-answer-registry.d.ts +5 -0
  116. package/dist/types/tools/bash.d.ts +1 -1
  117. package/dist/types/tools/browser/tab-supervisor.d.ts +6 -0
  118. package/dist/types/tools/browser.d.ts +28 -28
  119. package/dist/types/tools/computer.d.ts +20 -20
  120. package/dist/types/tools/cron.d.ts +2 -2
  121. package/dist/types/tools/debug.d.ts +40 -40
  122. package/dist/types/tools/gh.d.ts +10 -10
  123. package/dist/types/tools/image-gen.d.ts +2 -2
  124. package/dist/types/tools/index.d.ts +58 -6
  125. package/dist/types/tools/monitor.d.ts +6 -6
  126. package/dist/types/tools/skill-discovery.d.ts +4 -4
  127. package/dist/types/tools/subagent.d.ts +12 -12
  128. package/dist/types/tools/telegram-send.d.ts +7 -0
  129. package/dist/types/tools/todo-write.d.ts +8 -8
  130. package/dist/types/utils/external-editor.d.ts +2 -0
  131. package/dist/types/web/insane/bridge.d.ts +9 -9
  132. package/dist/types/web/search/index.d.ts +4 -4
  133. package/package.json +8 -8
  134. package/scripts/g004-tmux-smoke.ts +196 -36
  135. package/src/cli/daemon-cli.ts +19 -29
  136. package/src/cli/malloc-env-guard.test.ts +104 -0
  137. package/src/cli/malloc-env-guard.ts +149 -0
  138. package/src/cli/notify-cli.ts +78 -14
  139. package/src/cli/session-picker.ts +23 -40
  140. package/src/cli/update-cli.ts +56 -37
  141. package/src/cli.ts +51 -10
  142. package/src/commands/daemon.ts +19 -5
  143. package/src/commands/harness.ts +515 -47
  144. package/src/commands/launch.ts +11 -3
  145. package/src/commands/notify.ts +5 -1
  146. package/src/commands/session.ts +6 -1
  147. package/src/config/keybindings.ts +5 -0
  148. package/src/config/model-profile-activation.ts +1 -1
  149. package/src/config/model-profiles.ts +14 -14
  150. package/src/config/settings-schema.ts +41 -1
  151. package/src/config/settings.ts +96 -45
  152. package/src/coordinator/contract.ts +1 -0
  153. package/src/coordinator-mcp/model-preset.ts +100 -0
  154. package/src/coordinator-mcp/policy.ts +30 -0
  155. package/src/coordinator-mcp/server.ts +2958 -430
  156. package/src/coordinator-mcp/session-reaper.ts +129 -0
  157. package/src/daemon/control-types.ts +11 -0
  158. package/src/daemon/operator-contract.ts +121 -0
  159. package/src/defaults/skc/extensions/grok-cli-vendor/src/models/catalog.ts +57 -17
  160. package/src/defaults/skc/extensions/grok-cli-vendor/src/payload/sanitize.ts +15 -2
  161. package/src/defaults/skc/extensions/grok-cli-vendor/src/provider/register.ts +5 -1
  162. package/src/extensibility/extensions/runner.ts +1 -1
  163. package/src/extensibility/extensions/types.ts +3 -1
  164. package/src/internal-urls/docs-index.generated.ts +10 -9
  165. package/src/internal-urls/local-protocol.ts +29 -10
  166. package/src/main.ts +301 -80
  167. package/src/modes/acp/acp-agent.ts +730 -100
  168. package/src/modes/acp/acp-mode.ts +29 -2
  169. package/src/modes/components/assistant-message.ts +32 -3
  170. package/src/modes/components/bash-execution.ts +43 -13
  171. package/src/modes/components/custom-editor.ts +5 -0
  172. package/src/modes/components/footer.ts +3 -3
  173. package/src/modes/components/hook-selector.ts +12 -0
  174. package/src/modes/components/irc-sidebar.ts +206 -0
  175. package/src/modes/components/pet-selector.ts +55 -0
  176. package/src/modes/components/sayknow-pet-widget.ts +360 -0
  177. package/src/modes/components/session-selector.ts +222 -221
  178. package/src/modes/components/settings-selector.ts +10 -0
  179. package/src/modes/components/status-line/segments.ts +14 -7
  180. package/src/modes/components/status-line/types.ts +1 -1
  181. package/src/modes/components/status-line.ts +4 -171
  182. package/src/modes/components/tool-execution.ts +47 -6
  183. package/src/modes/components/user-message.ts +26 -9
  184. package/src/modes/controllers/command-controller.ts +28 -2
  185. package/src/modes/controllers/event-controller.ts +159 -36
  186. package/src/modes/controllers/extension-ui-controller.ts +37 -18
  187. package/src/modes/controllers/input-controller.ts +8 -2
  188. package/src/modes/controllers/selector-controller.ts +62 -15
  189. package/src/modes/interactive-mode.ts +199 -7
  190. package/src/modes/irc-observation-ledger.ts +127 -0
  191. package/src/modes/print-mode.ts +228 -92
  192. package/src/modes/rpc/rpc-client.ts +58 -1
  193. package/src/modes/rpc/rpc-mode.ts +286 -85
  194. package/src/modes/rpc/rpc-types.ts +15 -1
  195. package/src/modes/shared/agent-wire/command-dispatch.ts +33 -1
  196. package/src/modes/shared/agent-wire/command-validation.ts +10 -0
  197. package/src/modes/shared/agent-wire/deep-interview-gate.ts +45 -2
  198. package/src/modes/shared/agent-wire/scopes.ts +1 -0
  199. package/src/modes/shared/agent-wire/unattended-session.ts +288 -1
  200. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +130 -20
  201. package/src/modes/types.ts +34 -2
  202. package/src/modes/utils/context-usage.ts +56 -92
  203. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  204. package/src/modes/utils/irc-message.ts +138 -0
  205. package/src/modes/utils/ui-helpers.ts +216 -50
  206. package/src/notifications/config.ts +44 -0
  207. package/src/notifications/daemon-paths.ts +8 -0
  208. package/src/notifications/index.ts +535 -75
  209. package/src/notifications/lifecycle-control-runtime.ts +930 -77
  210. package/src/notifications/notification-service.ts +710 -0
  211. package/src/notifications/rate-limit-pool.ts +46 -12
  212. package/src/notifications/telegram-daemon-control.ts +27 -2
  213. package/src/notifications/telegram-daemon.ts +635 -94
  214. package/src/notifications/telegram-reference.ts +73 -13
  215. package/src/notifications/topic-registry.ts +86 -9
  216. package/src/prompts/system/irc-peer-roster.md +1 -0
  217. package/src/prompts/system/subagent-system-prompt.md +1 -0
  218. package/src/prompts/system/system-prompt.md +1 -0
  219. package/src/prompts/tools/irc.md +1 -1
  220. package/src/registry/agent-registry.ts +3 -0
  221. package/src/sdk.ts +52 -1
  222. package/src/secrets/obfuscator.ts +4 -2
  223. package/src/session/agent-session.ts +670 -217
  224. package/src/session/cache-economics.ts +275 -0
  225. package/src/session/context-estimation.ts +91 -0
  226. package/src/session/provider-safety-stop.ts +9 -0
  227. package/src/session/session-dump-format.ts +6 -0
  228. package/src/session/session-manager.ts +809 -39
  229. package/src/session/session-storage.ts +582 -38
  230. package/src/skc-runtime/session-state-sidecar.ts +756 -135
  231. package/src/skc-runtime/tmux-common.ts +26 -7
  232. package/src/skc-runtime/tmux-gc.ts +52 -9
  233. package/src/skc-runtime/tmux-owner-isolation-cli.ts +185 -0
  234. package/src/skc-runtime/tmux-owner-isolation.ts +2060 -0
  235. package/src/skc-runtime/tmux-sessions.ts +871 -68
  236. package/src/slash-commands/builtin-registry.ts +153 -9
  237. package/src/slash-commands/helpers/context-report.ts +26 -4
  238. package/src/task/executor.ts +12 -3
  239. package/src/task/index.ts +8 -0
  240. package/src/task/receipt.ts +7 -2
  241. package/src/task/types.ts +26 -0
  242. package/src/tools/ask-answer-registry.ts +25 -0
  243. package/src/tools/browser/tab-supervisor.ts +63 -6
  244. package/src/tools/browser.ts +8 -3
  245. package/src/tools/index.ts +81 -6
  246. package/src/tools/sqlite-reader.ts +8 -4
  247. package/src/tui/output-block.ts +18 -6
  248. package/src/utils/external-editor.ts +6 -1
@@ -1,14 +1,30 @@
1
+ import { Buffer } from "node:buffer";
2
+ import * as crypto from "node:crypto";
3
+ import * as fsSync from "node:fs";
4
+ import * as fs from "node:fs/promises";
5
+ import * as path from "node:path";
1
6
  import { resolveSkcTmuxBinary } from "./psmux-detect";
7
+ import { tmuxRuntimeSessionPath } from "./session-layout";
8
+ import {
9
+ SKC_COORDINATOR_SESSION_ID_ENV,
10
+ SKC_COORDINATOR_SESSION_STATE_FILE_ENV,
11
+ SKC_TMUX_OWNER_GENERATION_ENV,
12
+ SKC_TMUX_OWNER_SERVER_KEY_ENV,
13
+ SKC_TMUX_OWNER_STATE_DIR_ENV,
14
+ } from "./session-state-sidecar";
2
15
  import {
3
16
  buildSkcTmuxExactOptionTarget,
4
17
  buildSkcTmuxExactSessionTarget,
5
18
  buildSkcTmuxProfileCommands,
6
19
  buildSkcTmuxSessionName,
20
+ buildSkcTmuxSessionSlug,
7
21
  buildSkcTmuxUntaggedSessionError,
8
22
  normalizeTmuxCreatedAt,
9
23
  resolveSkcTmuxCommand,
10
24
  SKC_TMUX_BRANCH_OPTION,
11
25
  SKC_TMUX_BRANCH_SLUG_OPTION,
26
+ SKC_TMUX_OWNER_GENERATION_OPTION,
27
+ SKC_TMUX_OWNER_SERVER_KEY_OPTION,
12
28
  SKC_TMUX_PROFILE_OPTION,
13
29
  SKC_TMUX_PROFILE_VALUE,
14
30
  SKC_TMUX_PROJECT_OPTION,
@@ -16,6 +32,23 @@ import {
16
32
  SKC_TMUX_SESSION_STATE_FILE_OPTION,
17
33
  SKC_TMUX_VERSION_OPTION,
18
34
  } from "./tmux-common";
35
+ import {
36
+ captureOwnerGenerationBaselineSync,
37
+ classifyCgroup,
38
+ closeExactTmuxOwner,
39
+ executeTmuxOwnerIsolationPlanSync,
40
+ isOwnerGenerationBaselineCurrentSync,
41
+ isValidOwnerVerdict,
42
+ lifecyclePaths,
43
+ type OwnerIsolationProbeSync,
44
+ type OwnerVerdict,
45
+ type PlanResponse,
46
+ planTmuxOwnerIsolationSync,
47
+ replaceOwnerGenerationSync,
48
+ type TmuxOwnerIsolationExecutionDependencies,
49
+ type TmuxOwnerIsolationExecutionResult,
50
+ type TmuxServerProof,
51
+ } from "./tmux-owner-isolation";
19
52
 
20
53
  export interface SkcTmuxSessionStatus {
21
54
  name: string;
@@ -30,6 +63,9 @@ export interface SkcTmuxSessionStatus {
30
63
  sessionId?: string;
31
64
  sessionStateFile?: string;
32
65
  version?: string;
66
+ ownerGeneration?: string;
67
+ nativeSessionId?: string;
68
+
33
69
  panePids: number[];
34
70
  profile?: string;
35
71
  }
@@ -42,6 +78,9 @@ export interface SkcTmuxSessionTagsForGc {
42
78
  sessionId?: string;
43
79
  sessionStateFile?: string;
44
80
  version?: string;
81
+ ownerGeneration?: string;
82
+ nativeSessionId?: string;
83
+
45
84
  createdAt?: string;
46
85
  attached?: boolean;
47
86
  panePids?: number[];
@@ -52,19 +91,88 @@ export interface SkcTmuxSessionsForGc {
52
91
  untagged: SkcTmuxSessionStatus[];
53
92
  }
54
93
 
94
+ export interface ProvenTmuxSessionIdentity {
95
+ nativeSessionId: string;
96
+ serverPid: number;
97
+ serverStartTime: string;
98
+ }
99
+
100
+ export interface ExpectedSkcTmuxSessionIdentity {
101
+ nativeSessionId: string;
102
+ ownerGeneration: string;
103
+ sessionId: string;
104
+ sessionStateFile: string;
105
+ project: string;
106
+ createdAt: string;
107
+ }
108
+
109
+ export interface ExactOwnerIdentity {
110
+ sessionId: string;
111
+ stateDir: string;
112
+ socketKey: string;
113
+ generation: string;
114
+ pid: number;
115
+ startTime: string;
116
+ }
117
+
118
+ export interface ForceCloseOwnerDependencies {
119
+ resolveOwner(sessionName: string, env: NodeJS.ProcessEnv): Promise<ExactOwnerIdentity>;
120
+ signalTerm(pid: number): void;
121
+ readProcessStartTime(pid: number): Promise<string | null>;
122
+ cleanupSession(sessionTarget: string, env: NodeJS.ProcessEnv): void;
123
+ now(): Date;
124
+ sleep(ms: number): Promise<void>;
125
+ listPanePids(sessionName: string, env: NodeJS.ProcessEnv): number[];
126
+ }
127
+
128
+ const FORCE_CLOSE_VERDICT_TIMEOUT_MS = 5_000;
129
+ const FORCE_CLOSE_VERDICT_POLL_MS = 50;
130
+
131
+ export type CreateOwnerIsolationTestDependencies = {
132
+ probe?: Partial<OwnerIsolationProbeSync>;
133
+ execute?: (plan: PlanResponse, deps: TmuxOwnerIsolationExecutionDependencies) => TmuxOwnerIsolationExecutionResult;
134
+ };
135
+
136
+ let createOwnerIsolationTestDependencies: CreateOwnerIsolationTestDependencies | null = null;
137
+ let mutationServerProofTestDependency: ((tmuxCommand: string, env: NodeJS.ProcessEnv) => unknown) | null = null;
138
+
139
+ /** @internal Test-only seam; production create always uses live fail-closed probes. */
140
+ export function __setCreateOwnerIsolationForTests(dependencies: CreateOwnerIsolationTestDependencies | null): void {
141
+ createOwnerIsolationTestDependencies = dependencies;
142
+ }
143
+
144
+ /** @internal Test-only seam; production mutations always use live fail-closed proofs. */
145
+ export function __setMutationServerProofForTests(
146
+ dependency: ((tmuxCommand: string, env: NodeJS.ProcessEnv) => unknown) | null,
147
+ ): void {
148
+ mutationServerProofTestDependency = dependency;
149
+ }
150
+
55
151
  function runTmux(args: string[], env: NodeJS.ProcessEnv = process.env): string {
56
152
  const tmuxCommand = resolveSkcTmuxCommand(env);
57
- const result = Bun.spawnSync([tmuxCommand, ...args], { stdout: "pipe", stderr: "pipe", env });
153
+ const result = Bun.spawnSync([tmuxCommand, ...args], {
154
+ stdout: "pipe",
155
+ stderr: "pipe",
156
+ env,
157
+ });
58
158
  if (result.exitCode === 0) return result.stdout.toString();
59
159
  throw new Error(result.stderr.toString().trim() || `tmux ${args.join(" ")} failed`);
60
160
  }
161
+ function normalizeExactTmuxTarget(sessionTarget: string, env: NodeJS.ProcessEnv, kind: "session" | "option"): string {
162
+ if (sessionTarget.startsWith("$")) return sessionTarget;
163
+ return kind === "option"
164
+ ? buildSkcTmuxExactOptionTarget(sessionTarget, { env })
165
+ : buildSkcTmuxExactSessionTarget(sessionTarget, { env });
166
+ }
61
167
 
62
- function tryKillSession(sessionName: string, env: NodeJS.ProcessEnv): void {
63
- try {
64
- runTmux(["kill-session", "-t", buildSkcTmuxExactSessionTarget(sessionName, { env })], env);
65
- } catch {
66
- // Best-effort cleanup only; preserve the original create/tag failure.
67
- }
168
+ function readExactSessionPanePids(sessionName: string, env: NodeJS.ProcessEnv): number[] {
169
+ return runTmux(
170
+ ["list-panes", "-s", "-t", normalizeExactTmuxTarget(sessionName, env, "session"), "-F", "#{pane_pid}"],
171
+ env,
172
+ )
173
+ .split("\n")
174
+ .map(value => Number.parseInt(value.trim(), 10))
175
+ .filter(pid => Number.isSafeInteger(pid) && pid > 0);
68
176
  }
69
177
 
70
178
  function parseBooleanFlag(value: string | undefined): boolean {
@@ -91,8 +199,11 @@ function parseSessionLine(line: string): SkcTmuxSessionStatus | null {
91
199
  project = "",
92
200
  sessionId = "",
93
201
  sessionStateFile = "",
202
+ ownerGeneration = "",
94
203
  version = "",
204
+ nativeSessionId = "",
95
205
  ] = line.split("\t");
206
+
96
207
  if (!name) return null;
97
208
  return {
98
209
  name,
@@ -112,6 +223,8 @@ function parseSessionLine(line: string): SkcTmuxSessionStatus | null {
112
223
  sessionId: sessionId || undefined,
113
224
  sessionStateFile: sessionStateFile || undefined,
114
225
  version: version || undefined,
226
+ ownerGeneration: ownerGeneration || undefined,
227
+ nativeSessionId: nativeSessionId || undefined,
115
228
  };
116
229
  }
117
230
 
@@ -148,7 +261,8 @@ function runListSessions(format: string, env: NodeJS.ProcessEnv = process.env):
148
261
  if (!match) return line;
149
262
  const [, name, windows, created] = match;
150
263
  const createdEpoch = String(Math.floor(new Date(`${created} UTC`).getTime() / 1000) || 0);
151
- return [name, windows, "0", createdEpoch, "", "", "0", "", "", "", "", "", "", ""].join("\t");
264
+
265
+ return [name, windows, "0", createdEpoch, "", "", "0", "", "", "", "", "", "", "", "", ""].join("\t");
152
266
  });
153
267
  }
154
268
  }
@@ -157,7 +271,8 @@ function runListSessions(format: string, env: NodeJS.ProcessEnv = process.env):
157
271
 
158
272
  function listSessionLines(env: NodeJS.ProcessEnv = process.env): string[] {
159
273
  return runListSessions(
160
- `#{session_name}\t#{session_windows}\t#{session_attached}\t#{session_created}\t#{${SKC_TMUX_PROFILE_OPTION}}\t#{session_key_table}\t#{session_panes}\t#{pane_pid}\t#{${SKC_TMUX_BRANCH_OPTION}}\t#{${SKC_TMUX_BRANCH_SLUG_OPTION}}\t#{${SKC_TMUX_PROJECT_OPTION}}\t#{${SKC_TMUX_SESSION_ID_OPTION}}\t#{${SKC_TMUX_SESSION_STATE_FILE_OPTION}}\t#{${SKC_TMUX_VERSION_OPTION}}`,
274
+ `#{session_name}\t#{session_windows}\t#{session_attached}\t#{session_created}\t#{${SKC_TMUX_PROFILE_OPTION}}\t#{session_key_table}\t#{session_panes}\t#{pane_pid}\t#{${SKC_TMUX_BRANCH_OPTION}}\t#{${SKC_TMUX_BRANCH_SLUG_OPTION}}\t#{${SKC_TMUX_PROJECT_OPTION}}\t#{${SKC_TMUX_SESSION_ID_OPTION}}\t#{${SKC_TMUX_SESSION_STATE_FILE_OPTION}}\t#{${SKC_TMUX_OWNER_GENERATION_OPTION}}\t#{${SKC_TMUX_VERSION_OPTION}}\t#{session_id}`,
275
+
161
276
  env,
162
277
  );
163
278
  }
@@ -242,40 +357,443 @@ export function statusSkcTmuxSession(sessionName: string, env: NodeJS.ProcessEnv
242
357
  export function createSkcTmuxSession(env: NodeJS.ProcessEnv = process.env): SkcTmuxSessionStatus {
243
358
  const tmuxCommand = resolveSkcTmuxCommand(env);
244
359
  const sessionName = buildSkcTmuxSessionName(env);
245
- // Build a shell-bootstrap command appropriate for the host shell. Psmux on
246
- // Windows runs the new-session command through PowerShell, so we use the
247
- // $env:VAR = ... assignment form there. POSIX keeps the historical exec
248
- // env form so the launched skc inherits SKC_TMUX_LAUNCHED without leaking
249
- // into the parent tmux server.
250
- const platform = process.platform;
251
- const command = platform === "win32" ? "$env:SKC_TMUX_LAUNCHED = '1'; skc" : "exec env SKC_TMUX_LAUNCHED=1 skc";
252
- const created = Bun.spawnSync([tmuxCommand, "new-session", "-d", "-s", sessionName, command], {
360
+ const cwd = process.cwd();
361
+ const sessionId = env[SKC_COORDINATOR_SESSION_ID_ENV]?.trim() || sessionName;
362
+ const stateFile =
363
+ env[SKC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim() ||
364
+ tmuxRuntimeSessionPath(cwd, env.SKC_SESSION_ID?.trim() || sessionId, buildSkcTmuxSessionSlug(sessionName));
365
+ const stateDir = path.dirname(stateFile);
366
+ const generation = crypto.randomUUID();
367
+ const childEnvironment: Record<string, string> = {
368
+ SKC_TMUX_LAUNCHED: "1",
369
+ [SKC_TMUX_OWNER_GENERATION_ENV]: generation,
370
+ [SKC_TMUX_OWNER_STATE_DIR_ENV]: stateDir,
371
+ [SKC_TMUX_OWNER_SERVER_KEY_ENV]: tmuxCommand,
372
+ [SKC_COORDINATOR_SESSION_ID_ENV]: sessionId,
373
+ [SKC_COORDINATOR_SESSION_STATE_FILE_ENV]: stateFile,
374
+ };
375
+ const shellQuote = (value: string): string => `'${value.replace(/'/g, `'\\''`)}'`;
376
+ const powershellQuote = (value: string): string => `'${value.replace(/'/g, "''")}'`;
377
+ const command =
378
+ process.platform === "win32"
379
+ ? `${Object.entries(childEnvironment)
380
+ .map(([name, value]) => `$env:${name} = ${powershellQuote(value)}`)
381
+ .join("; ")}; skc`
382
+ : `exec env ${Object.entries(childEnvironment)
383
+ .map(([name, value]) => `${name}=${shellQuote(value)}`)
384
+ .join(" ")} skc`;
385
+ const tmuxArgv = [
386
+ tmuxCommand,
387
+ "new-session",
388
+ "-d",
389
+ "-s",
390
+ sessionName,
391
+ ...(process.platform === "win32" ? [] : ["-P", "-F", "#{session_id}"]),
392
+ command,
393
+ ];
394
+ function probeTmuxServer(tmuxCommand: string, env: NodeJS.ProcessEnv): TmuxServerProof {
395
+ if (process.platform !== "linux") {
396
+ return {
397
+ state: "safe",
398
+ pid: 1,
399
+ startTime: "not-applicable",
400
+ cgroup: { classification: "not_applicable" },
401
+ };
402
+ }
403
+ const result = Bun.spawnSync([tmuxCommand, "display-message", "-p", "#{pid}"], {
404
+ stdout: "pipe",
405
+ stderr: "pipe",
406
+ env,
407
+ });
408
+ if (result.exitCode !== 0) {
409
+ const diagnostic = result.stderr.toString();
410
+ return /no server running|failed to connect|error connecting/.test(diagnostic)
411
+ ? { state: "absent" }
412
+ : { state: "unverifiable" };
413
+ }
414
+ const pid = Number(result.stdout.toString().trim());
415
+ if (!Number.isSafeInteger(pid) || pid <= 0) return { state: "unverifiable" };
416
+ try {
417
+ const stat = fsSync.readFileSync(`/proc/${pid}/stat`, "utf8");
418
+ const startTime = stat
419
+ .slice(stat.lastIndexOf(")") + 2)
420
+ .trim()
421
+ .split(/\s+/)[19];
422
+ if (!startTime) return { state: "unverifiable" };
423
+ const cgroup = classifyCgroup({
424
+ platform: process.platform,
425
+ cgroupText: fsSync.readFileSync(`/proc/${pid}/cgroup`, "utf8"),
426
+ });
427
+ return {
428
+ state:
429
+ cgroup.classification === "safe"
430
+ ? "safe"
431
+ : cgroup.classification === "unsafe_service"
432
+ ? "unsafe"
433
+ : "unverifiable",
434
+ pid,
435
+ startTime,
436
+ cgroup,
437
+ };
438
+ } catch {
439
+ return { state: "unverifiable" };
440
+ }
441
+ }
442
+
443
+ const probeServer = () => probeTmuxServer(tmuxCommand, env);
444
+
445
+ const testOwnerProbe = createOwnerIsolationTestDependencies?.probe;
446
+ const ownerProbe: OwnerIsolationProbeSync = {
447
+ readCallerCgroup:
448
+ testOwnerProbe?.readCallerCgroup ??
449
+ (() => {
450
+ try {
451
+ return fsSync.readFileSync("/proc/self/cgroup", "utf8");
452
+ } catch {
453
+ return null;
454
+ }
455
+ }),
456
+ probeServer: testOwnerProbe?.probeServer ?? probeServer,
457
+ recordAttempt:
458
+ testOwnerProbe?.recordAttempt ??
459
+ (({ attempt }) => {
460
+ const root = lifecyclePaths(stateDir, sessionId, generation).root;
461
+ const attemptFile = path.join(root, `attempt-${attempt.token}.json`);
462
+ fsSync.mkdirSync(root, { recursive: true, mode: 0o700 });
463
+ const descriptor = fsSync.openSync(attemptFile, "wx", 0o600);
464
+ try {
465
+ fsSync.writeFileSync(
466
+ descriptor,
467
+ `${JSON.stringify({
468
+ schema_version: 1,
469
+ generation,
470
+ session_id: sessionId,
471
+ ...attempt,
472
+ created_at: new Date().toISOString(),
473
+ })}\n`,
474
+ );
475
+ fsSync.fsyncSync(descriptor);
476
+ } finally {
477
+ fsSync.closeSync(descriptor);
478
+ }
479
+ const directory = fsSync.openSync(root, "r");
480
+ try {
481
+ fsSync.fsyncSync(directory);
482
+ } finally {
483
+ fsSync.closeSync(directory);
484
+ }
485
+ }),
486
+ };
487
+ if (resolveSkcTmuxBinary({ env }).isPsmux)
488
+ throw new Error("skc_tmux_owner_isolation_native_session_identity_unavailable");
489
+
490
+ const baseline = captureOwnerGenerationBaselineSync(stateDir, sessionId);
491
+ const ownerPlan = planTmuxOwnerIsolationSync(
492
+ {
493
+ schema_version: 1,
494
+ op: "plan",
495
+ platform: process.platform,
496
+ session_id: sessionId,
497
+ owner_generation: generation,
498
+ baseline,
499
+ cwd,
500
+ state_dir: stateDir,
501
+ socket_key: tmuxCommand,
502
+ tmux_argv: tmuxArgv,
503
+ },
504
+ ownerProbe,
505
+ );
506
+ if (!ownerPlan.ok) throw new Error(`skc_tmux_owner_isolation_${ownerPlan.code}:${ownerPlan.diagnostic}`);
507
+
508
+ const outcome = (createOwnerIsolationTestDependencies?.execute ?? executeTmuxOwnerIsolationPlanSync)(ownerPlan, {
509
+ socketKey: tmuxCommand,
510
+ spawn: (argv, stdinLine) => {
511
+ const result = stdinLine
512
+ ? Bun.spawnSync(argv, {
513
+ stdout: "pipe",
514
+ stderr: "pipe",
515
+ stdin: Buffer.from(stdinLine),
516
+ env,
517
+ })
518
+ : Bun.spawnSync(argv, { stdout: "pipe", stderr: "pipe", env });
519
+ return { exitCode: result.exitCode, stdout: result.stdout.toString() };
520
+ },
521
+ probeServer: ownerProbe.probeServer,
522
+ isCurrentGeneration: () => isOwnerGenerationBaselineCurrentSync(stateDir, sessionId, baseline),
523
+ cleanupSpawned: ({ execution, nativeSessionId, server }) => {
524
+ if (!server.pid || !server.startTime) throw new Error("skc_tmux_cleanup_target_changed");
525
+ cleanupExactCreatedTmuxSession(
526
+ nativeSessionId,
527
+ execution.attempt_session,
528
+ tmuxCommand,
529
+ env,
530
+ server.pid,
531
+ server.startTime,
532
+ );
533
+ },
534
+ });
535
+ if (!outcome.ok) throw new Error(`skc_tmux_owner_isolation_${outcome.code}:${outcome.diagnostic}`);
536
+
537
+ const nativeSessionId = outcome.native_session_id;
538
+ if (!nativeSessionId) throw new Error("skc_tmux_owner_isolation_native_session_identity_unavailable");
539
+ const server = requireSafeTmuxServerForMutation(tmuxCommand, env);
540
+ if (server.pid !== outcome.server_pid || server.startTime !== outcome.server_start_time)
541
+ throw new Error("skc_tmux_owner_changed_after_create");
542
+ if (!isNativeTmuxSessionBoundToName(nativeSessionId, sessionName, env))
543
+ throw new Error("skc_tmux_owner_changed_after_create");
544
+
545
+ try {
546
+ tagCreatedTmuxSession(
547
+ nativeSessionId,
548
+ sessionName,
549
+ outcome.server_pid,
550
+ env,
551
+ {
552
+ sessionId,
553
+ sessionStateFile: stateFile,
554
+ ownerGeneration: generation,
555
+ ownerServerKey: tmuxCommand,
556
+ version: env.npm_package_version ?? null,
557
+ },
558
+ tmuxCommand,
559
+ );
560
+ } catch (tagError) {
561
+ try {
562
+ cleanupExactCreatedTmuxSession(
563
+ nativeSessionId,
564
+ sessionName,
565
+ tmuxCommand,
566
+ env,
567
+ outcome.server_pid,
568
+ outcome.server_start_time,
569
+ );
570
+ } catch (cleanupError) {
571
+ throw new AggregateError([tagError, cleanupError], "skc_tmux_profile_tag_failed_cleanup_failed");
572
+ }
573
+ throw tagError;
574
+ }
575
+
576
+ if (nativeSessionId) {
577
+ const firstServer = requireSafeTmuxServerForMutation(tmuxCommand, env);
578
+ if (firstServer.pid !== outcome.server_pid || firstServer.startTime !== outcome.server_start_time)
579
+ throw new Error("skc_tmux_owner_changed_after_create");
580
+ const status = statusSkcTmuxSessionByNativeId(nativeSessionId, env);
581
+ const finalServer = requireSafeTmuxServerForMutation(tmuxCommand, env);
582
+ if (finalServer.pid !== firstServer.pid || finalServer.startTime !== firstServer.startTime)
583
+ throw new Error("skc_tmux_owner_changed_after_create");
584
+ try {
585
+ replaceOwnerGenerationSync(stateDir, sessionId, generation, baseline);
586
+ } catch (publicationError) {
587
+ try {
588
+ cleanupExactCreatedTmuxSession(
589
+ nativeSessionId,
590
+ sessionName,
591
+ tmuxCommand,
592
+ env,
593
+ outcome.server_pid,
594
+ outcome.server_start_time,
595
+ );
596
+ } catch (cleanupError) {
597
+ throw new AggregateError(
598
+ [publicationError, cleanupError],
599
+ "skc_tmux_owner_generation_publish_failed_cleanup_failed",
600
+ );
601
+ }
602
+ throw publicationError;
603
+ }
604
+ return status;
605
+ }
606
+ try {
607
+ replaceOwnerGenerationSync(stateDir, sessionId, generation, baseline);
608
+ } catch (publicationError) {
609
+ try {
610
+ cleanupExactCreatedTmuxSession(
611
+ nativeSessionId,
612
+ sessionName,
613
+ tmuxCommand,
614
+ env,
615
+ outcome.server_pid,
616
+ outcome.server_start_time,
617
+ );
618
+ } catch (cleanupError) {
619
+ throw new AggregateError(
620
+ [publicationError, cleanupError],
621
+ "skc_tmux_owner_generation_publish_failed_cleanup_failed",
622
+ );
623
+ }
624
+ throw publicationError;
625
+ }
626
+ return statusSkcTmuxSession(sessionName, env);
627
+ }
628
+
629
+ function statusSkcTmuxSessionByNativeId(nativeSessionId: string, env: NodeJS.ProcessEnv): SkcTmuxSessionStatus {
630
+ const name = runTmux(
631
+ ["display-message", "-p", "-t", normalizeExactTmuxTarget(nativeSessionId, env, "option"), "#{session_name}"],
632
+ env,
633
+ ).trim();
634
+ if (!name || readNativeTmuxSessionId(nativeSessionId, env) !== nativeSessionId)
635
+ throw new Error(`skc_tmux_owner_changed:${nativeSessionId}`);
636
+ return statusSkcTmuxSession(name, env);
637
+ }
638
+
639
+ function tmuxCommandArgument(value: string): string {
640
+ return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("$", "\\$").replaceAll("`", "\\`")}"`;
641
+ }
642
+
643
+ function guardedTmuxSessionPredicate(
644
+ expectedPid: number,
645
+ nativeSessionId: string,
646
+ sessionName: string,
647
+ expectedOwnerGeneration?: string,
648
+ ): string {
649
+ const ownerGenerationPredicate = expectedOwnerGeneration
650
+ ? `#{==:#{${SKC_TMUX_OWNER_GENERATION_OPTION}},${expectedOwnerGeneration}}`
651
+ : "1";
652
+ return `#{&&:#{==:#{pid},${expectedPid}},#{&&:#{==:#{session_id},${nativeSessionId}},#{&&:#{==:#{session_name},${sessionName}},${ownerGenerationPredicate}}}}`;
653
+ }
654
+
655
+ function runGuardedTmuxSessionCommand(
656
+ nativeSessionId: string,
657
+ sessionName: string,
658
+ expectedPid: number,
659
+ env: NodeJS.ProcessEnv,
660
+ thenCommand: string,
661
+ expectedOwnerGeneration?: string,
662
+ ): void {
663
+ const result = runTmux(
664
+ [
665
+ "if-shell",
666
+ "-t",
667
+ normalizeExactTmuxTarget(nativeSessionId, env, "session"),
668
+ "-F",
669
+ guardedTmuxSessionPredicate(expectedPid, nativeSessionId, sessionName, expectedOwnerGeneration),
670
+ `${thenCommand} ; display-message -p __skc_tmux_guarded_mutation_ok__`,
671
+ "display-message -p __skc_tmux_guarded_mutation_refused__",
672
+ ],
673
+ env,
674
+ ).trim();
675
+ if (result !== "__skc_tmux_guarded_mutation_ok__") throw new Error("skc_tmux_cleanup_target_changed");
676
+ }
677
+
678
+ function tagCreatedTmuxSession(
679
+ nativeSessionId: string,
680
+ sessionName: string,
681
+ expectedPid: number,
682
+ env: NodeJS.ProcessEnv,
683
+ metadata: {
684
+ branch?: string | null;
685
+ branchSlug?: string | null;
686
+ project?: string | null;
687
+ sessionId?: string | null;
688
+ sessionStateFile?: string | null;
689
+ ownerGeneration?: string | null;
690
+ ownerServerKey?: string | null;
691
+ version?: string | null;
692
+ },
693
+ tmuxCommand: string,
694
+ ): void {
695
+ const target = `${nativeSessionId}:`;
696
+ const commands = buildSkcTmuxProfileCommands(target, env, metadata, { tmuxCommand })
697
+ .map(command => command.args.map(tmuxCommandArgument).join(" "))
698
+ .join(" ; ");
699
+ runGuardedTmuxSessionCommand(nativeSessionId, sessionName, expectedPid, env, commands);
700
+ }
701
+
702
+ function cleanupExactCreatedTmuxSession(
703
+ nativeSessionId: string,
704
+ sessionName: string,
705
+ tmuxCommand: string,
706
+ env: NodeJS.ProcessEnv,
707
+ expectedPid: number,
708
+ expectedStartTime: string,
709
+ ): void {
710
+ const server = requireSafeTmuxServerForMutation(tmuxCommand, env);
711
+ if (server.pid !== expectedPid || server.startTime !== expectedStartTime)
712
+ throw new Error("skc_tmux_cleanup_target_changed");
713
+ runGuardedTmuxSessionCommand(
714
+ nativeSessionId,
715
+ sessionName,
716
+ expectedPid,
717
+ env,
718
+ `kill-session -t ${tmuxCommandArgument(normalizeExactTmuxTarget(nativeSessionId, env, "session"))}`,
719
+ );
720
+ }
721
+
722
+ function requireSafeTmuxServerForMutation(
723
+ tmuxCommand: string,
724
+ env: NodeJS.ProcessEnv,
725
+ ): { pid: number; startTime: string } {
726
+ if (mutationServerProofTestDependency) {
727
+ const proof = mutationServerProofTestDependency(tmuxCommand, env);
728
+ if (
729
+ proof &&
730
+ typeof proof === "object" &&
731
+ Number.isSafeInteger((proof as { pid?: unknown }).pid) &&
732
+ typeof (proof as { startTime?: unknown }).startTime === "string"
733
+ )
734
+ return proof as { pid: number; startTime: string };
735
+ return { pid: 1, startTime: "test" };
736
+ }
737
+ if (process.platform !== "linux") return { pid: 1, startTime: "not-applicable" };
738
+ const result = Bun.spawnSync([tmuxCommand, "display-message", "-p", "#{pid}"], {
253
739
  stdout: "pipe",
254
740
  stderr: "pipe",
255
741
  env,
256
742
  });
257
- if (created.exitCode !== 0) throw new Error(created.stderr.toString().trim() || "skc_tmux_session_create_failed");
743
+ if (result.exitCode !== 0) throw new Error("skc_tmux_owner_isolation_server_unverifiable");
744
+ const pid = Number(result.stdout.toString().trim());
745
+ if (!Number.isSafeInteger(pid) || pid <= 0) throw new Error("skc_tmux_owner_isolation_server_unverifiable");
258
746
  try {
259
- // psmux 3.3.0 rejects the tmux `=NAME` exact-session prefix for option
260
- // commands, so the target is the bare session name on psmux and the
261
- // window-qualified `=NAME:` on tmux. The ownership-tag round-trip
262
- // (set-option @skc-*) is preserved on both; only the UX profile commands
263
- // (mouse / set-clipboard / mode-style / set-window-option) get filtered
264
- // by buildSkcTmuxProfileCommands when the active binary is psmux.
265
- const target = buildSkcTmuxExactOptionTarget(sessionName, { env });
266
- for (const profileCommand of buildSkcTmuxProfileCommands(target, env, {}, { tmuxCommand })) {
267
- runTmux(profileCommand.args, env);
268
- }
747
+ const stat = fsSync.readFileSync(`/proc/${pid}/stat`, "utf8");
748
+ const startTime = stat
749
+ .slice(stat.lastIndexOf(")") + 2)
750
+ .trim()
751
+ .split(/\s+/)[19];
752
+ const cgroup = classifyCgroup({
753
+ platform: process.platform,
754
+ cgroupText: fsSync.readFileSync(`/proc/${pid}/cgroup`, "utf8"),
755
+ });
756
+ if (!startTime || cgroup.classification !== "safe")
757
+ throw new Error(
758
+ `skc_tmux_owner_isolation_${cgroup.classification === "unsafe_service" ? "server_unsafe" : "server_unverifiable"}`,
759
+ );
760
+ return { pid, startTime };
269
761
  } catch (error) {
270
- tryKillSession(sessionName, env);
271
- throw error;
762
+ if (error instanceof Error && error.message.startsWith("skc_tmux_owner_isolation_")) throw error;
763
+ throw new Error("skc_tmux_owner_isolation_server_unverifiable");
272
764
  }
273
- return statusSkcTmuxSession(sessionName, env);
765
+ }
766
+
767
+ /** Proves a managed reusable name still resolves to one immutable session on one server. */
768
+ export function proveSkcTmuxSessionMutationTarget(
769
+ sessionName: string,
770
+ env: NodeJS.ProcessEnv = process.env,
771
+ ): ProvenTmuxSessionIdentity {
772
+ const session = statusSkcTmuxSession(sessionName, env);
773
+ if (readProfileForExactTarget(session.name, env) !== SKC_TMUX_PROFILE_VALUE)
774
+ throw new Error(`skc_tmux_session_not_managed:${sessionName}`);
775
+ const firstServer = requireSafeTmuxServerForMutation(resolveSkcTmuxCommand(env), env);
776
+ if (resolveSkcTmuxBinary({ env }).isPsmux) throw new Error(`skc_tmux_owner_unverifiable:${sessionName}`);
777
+ const nativeSessionId = readNativeTmuxSessionId(session.name, env);
778
+ if (!nativeSessionId) throw new Error(`skc_tmux_owner_unverifiable:${sessionName}`);
779
+ if (
780
+ readNativeTmuxSessionId(nativeSessionId, env) !== nativeSessionId ||
781
+ readProfileForExactTarget(nativeSessionId, env) !== SKC_TMUX_PROFILE_VALUE
782
+ )
783
+ throw new Error(`skc_tmux_owner_changed:${sessionName}`);
784
+ const finalServer = requireSafeTmuxServerForMutation(resolveSkcTmuxCommand(env), env);
785
+ if (finalServer.pid !== firstServer.pid || finalServer.startTime !== firstServer.startTime)
786
+ throw new Error(`skc_tmux_owner_changed:${sessionName}`);
787
+ return {
788
+ nativeSessionId,
789
+ serverPid: finalServer.pid,
790
+ serverStartTime: finalServer.startTime,
791
+ };
274
792
  }
275
793
 
276
794
  function readProfileForExactTarget(sessionName: string, env: NodeJS.ProcessEnv): string {
277
795
  const raw = runTmux(
278
- ["show-options", "-qv", "-t", buildSkcTmuxExactOptionTarget(sessionName, { env }), SKC_TMUX_PROFILE_OPTION],
796
+ ["show-options", "-qv", "-t", normalizeExactTmuxTarget(sessionName, env, "option"), SKC_TMUX_PROFILE_OPTION],
279
797
  env,
280
798
  ).trim();
281
799
  // tmux returns just the value; psmux returns `key value`. Strip the
@@ -291,7 +809,7 @@ function readProfileForExactTarget(sessionName: string, env: NodeJS.ProcessEnv):
291
809
  function readExactOptionForGc(sessionName: string, option: string, env: NodeJS.ProcessEnv): string | undefined {
292
810
  try {
293
811
  const raw = runTmux(
294
- ["show-options", "-qv", "-t", buildSkcTmuxExactOptionTarget(sessionName, { env }), option],
812
+ ["show-options", "-qv", "-t", normalizeExactTmuxTarget(sessionName, env, "option"), option],
295
813
  env,
296
814
  ).trim();
297
815
  if (!raw) return undefined;
@@ -316,6 +834,38 @@ function readExactOptionForGc(sessionName: string, option: string, env: NodeJS.P
316
834
  }
317
835
  }
318
836
 
837
+ function readNativeTmuxSessionId(sessionTarget: string, env: NodeJS.ProcessEnv): string | undefined {
838
+ if (resolveSkcTmuxBinary({ env }).isPsmux) return undefined;
839
+ try {
840
+ const sessionId = runTmux(
841
+ ["display-message", "-p", "-t", normalizeExactTmuxTarget(sessionTarget, env, "option"), "#{session_id}"],
842
+ env,
843
+ ).trim();
844
+ return sessionId || undefined;
845
+ } catch {
846
+ return undefined;
847
+ }
848
+ }
849
+
850
+ function isNativeTmuxSessionBoundToName(nativeSessionId: string, sessionName: string, env: NodeJS.ProcessEnv): boolean {
851
+ try {
852
+ return (
853
+ runTmux(
854
+ [
855
+ "display-message",
856
+ "-p",
857
+ "-t",
858
+ normalizeExactTmuxTarget(sessionName, env, "option"),
859
+ "#{session_id}\t#{session_name}",
860
+ ],
861
+ env,
862
+ ).trim() === `${nativeSessionId}\t${sessionName}`
863
+ );
864
+ } catch {
865
+ return false;
866
+ }
867
+ }
868
+
319
869
  function hydrateSessionFromExactOptions(session: SkcTmuxSessionStatus, env: NodeJS.ProcessEnv): SkcTmuxSessionStatus {
320
870
  if (session.profile === SKC_TMUX_PROFILE_VALUE) return session;
321
871
  const profile = readExactOptionForGc(session.name, SKC_TMUX_PROFILE_OPTION, env);
@@ -329,6 +879,9 @@ function hydrateSessionFromExactOptions(session: SkcTmuxSessionStatus, env: Node
329
879
  sessionId: session.sessionId ?? readExactOptionForGc(session.name, SKC_TMUX_SESSION_ID_OPTION, env),
330
880
  sessionStateFile:
331
881
  session.sessionStateFile ?? readExactOptionForGc(session.name, SKC_TMUX_SESSION_STATE_FILE_OPTION, env),
882
+ ownerGeneration:
883
+ session.ownerGeneration ?? readExactOptionForGc(session.name, SKC_TMUX_OWNER_GENERATION_OPTION, env),
884
+
332
885
  version: session.version ?? readExactOptionForGc(session.name, SKC_TMUX_VERSION_OPTION, env),
333
886
  };
334
887
  }
@@ -347,71 +900,321 @@ export function readTmuxSessionTagsForGc(
347
900
  sessionId: readExactOptionForGc(sessionName, SKC_TMUX_SESSION_ID_OPTION, env),
348
901
  sessionStateFile: readExactOptionForGc(sessionName, SKC_TMUX_SESSION_STATE_FILE_OPTION, env),
349
902
  version: readExactOptionForGc(sessionName, SKC_TMUX_VERSION_OPTION, env),
903
+ ownerGeneration: readExactOptionForGc(sessionName, SKC_TMUX_OWNER_GENERATION_OPTION, env),
904
+ nativeSessionId: session?.nativeSessionId,
350
905
  createdAt: session?.createdAt,
351
906
  attached: session?.attached,
352
907
  panePids: session?.panePids,
353
908
  };
354
909
  }
355
910
 
356
- export function removeSkcTmuxSession(sessionName: string, env: NodeJS.ProcessEnv = process.env): SkcTmuxSessionStatus {
911
+ export function removeSkcTmuxSession(
912
+ sessionName: string,
913
+ env: NodeJS.ProcessEnv = process.env,
914
+ expectedIdentity?: ExpectedSkcTmuxSessionIdentity,
915
+ ): SkcTmuxSessionStatus {
357
916
  const session = statusSkcTmuxSession(sessionName, env);
358
917
  if (session.attached || session.panePids.length > 0) {
359
918
  throw new Error(`skc_tmux_session_live:${sessionName}`);
360
919
  }
920
+ if (
921
+ expectedIdentity &&
922
+ (session.nativeSessionId !== expectedIdentity.nativeSessionId ||
923
+ session.ownerGeneration !== expectedIdentity.ownerGeneration ||
924
+ session.sessionId !== expectedIdentity.sessionId ||
925
+ session.sessionStateFile !== expectedIdentity.sessionStateFile ||
926
+ session.project !== expectedIdentity.project ||
927
+ session.createdAt !== expectedIdentity.createdAt)
928
+ )
929
+ throw new Error(`skc_tmux_owner_changed:${sessionName}`);
361
930
  if (readProfileForExactTarget(session.name, env) !== SKC_TMUX_PROFILE_VALUE) {
362
931
  throw new Error(`skc_tmux_session_not_managed:${sessionName}`);
363
932
  }
364
- runTmux(["kill-session", "-t", buildSkcTmuxExactSessionTarget(session.name, { env })], env);
933
+ if (resolveSkcTmuxBinary({ env }).isPsmux) throw new Error(`skc_tmux_owner_unverifiable:${sessionName}`);
934
+ const nativeSessionId = readNativeTmuxSessionId(session.name, env);
935
+ if (!nativeSessionId) throw new Error(`skc_tmux_owner_unverifiable:${sessionName}`);
936
+ if (expectedIdentity && nativeSessionId !== expectedIdentity.nativeSessionId)
937
+ throw new Error(`skc_tmux_owner_changed:${sessionName}`);
938
+ if (
939
+ expectedIdentity &&
940
+ readExactOptionForGc(session.name, SKC_TMUX_OWNER_GENERATION_OPTION, env) !== expectedIdentity.ownerGeneration
941
+ )
942
+ throw new Error(`skc_tmux_owner_changed:${sessionName}`);
943
+ const firstServer = requireSafeTmuxServerForMutation(resolveSkcTmuxCommand(env), env);
944
+ if (
945
+ readNativeTmuxSessionId(nativeSessionId, env) !== nativeSessionId ||
946
+ readProfileForExactTarget(nativeSessionId, env) !== SKC_TMUX_PROFILE_VALUE
947
+ )
948
+ throw new Error(`skc_tmux_owner_changed:${sessionName}`);
949
+ const finalServer = requireSafeTmuxServerForMutation(resolveSkcTmuxCommand(env), env);
950
+ if (finalServer.pid !== firstServer.pid || finalServer.startTime !== firstServer.startTime)
951
+ throw new Error(`skc_tmux_owner_changed:${sessionName}`);
952
+ runGuardedTmuxSessionCommand(
953
+ nativeSessionId,
954
+ session.name,
955
+ finalServer.pid,
956
+ env,
957
+ `kill-session -t '${nativeSessionId}'`,
958
+ expectedIdentity?.ownerGeneration,
959
+ );
365
960
  return session;
366
961
  }
367
962
 
963
+ async function readProcessStartTime(pid: number): Promise<string | null> {
964
+ try {
965
+ const stat = await fs.readFile(`/proc/${pid}/stat`, "utf8");
966
+ const close = stat.lastIndexOf(")");
967
+ const fields = stat
968
+ .slice(close + 2)
969
+ .trim()
970
+ .split(/\s+/);
971
+ return fields[19] ?? null;
972
+ } catch {
973
+ return null;
974
+ }
975
+ }
976
+
977
+ async function readCurrentGeneration(stateDir: string, sessionId: string): Promise<string | null> {
978
+ try {
979
+ const value: unknown = JSON.parse(
980
+ await fs.readFile(path.join(stateDir, sessionId, "owner-lifecycle", "generation.json"), "utf8"),
981
+ );
982
+ return typeof value === "object" &&
983
+ value !== null &&
984
+ typeof (value as { generation?: unknown }).generation === "string"
985
+ ? (value as { generation: string }).generation
986
+ : null;
987
+ } catch {
988
+ return null;
989
+ }
990
+ }
991
+
992
+ async function resolveExactOwner(
993
+ sessionName: string,
994
+ env: NodeJS.ProcessEnv,
995
+ exactPanePid: number,
996
+ ): Promise<ExactOwnerIdentity> {
997
+ const session = statusSkcTmuxSession(sessionName, env);
998
+ const sessionId = readExactOptionForGc(session.name, SKC_TMUX_SESSION_ID_OPTION, env);
999
+ const stateFile = readExactOptionForGc(session.name, SKC_TMUX_SESSION_STATE_FILE_OPTION, env);
1000
+ const ownerGeneration = readExactOptionForGc(session.name, SKC_TMUX_OWNER_GENERATION_OPTION, env);
1001
+ const ownerServerKey = readExactOptionForGc(session.name, SKC_TMUX_OWNER_SERVER_KEY_OPTION, env);
1002
+
1003
+ if (!sessionId || !stateFile) throw new Error(`skc_tmux_owner_unverifiable:${sessionName}`);
1004
+ const stateDir = path.dirname(stateFile);
1005
+ const generation = await readCurrentGeneration(stateDir, sessionId);
1006
+ const pid = exactPanePid;
1007
+ const startTime = await readProcessStartTime(pid);
1008
+ if (!generation || !ownerGeneration || ownerGeneration !== generation || !ownerServerKey || !startTime)
1009
+ throw new Error(`skc_tmux_owner_unverifiable:${sessionName}`);
1010
+
1011
+ return {
1012
+ sessionId,
1013
+ stateDir,
1014
+ socketKey: ownerServerKey,
1015
+ generation,
1016
+ pid,
1017
+ startTime,
1018
+ };
1019
+ }
1020
+
1021
+ async function requireUnchangedOwnerForCompatibilityCleanup(
1022
+ sessionName: string,
1023
+ nativeSessionId: string,
1024
+ env: NodeJS.ProcessEnv,
1025
+ identity: ExactOwnerIdentity,
1026
+ initialStateFile: string,
1027
+ initialServer: { pid: number; startTime: string },
1028
+ listPanePids: (sessionName: string, env: NodeJS.ProcessEnv) => number[],
1029
+ readStartTime: (pid: number) => Promise<string | null>,
1030
+ ): Promise<void> {
1031
+ try {
1032
+ const currentNativeSessionId = readNativeTmuxSessionId(nativeSessionId, env);
1033
+ if (!currentNativeSessionId) {
1034
+ if (readNativeTmuxSessionId(sessionName, env)) throw new Error(`skc_tmux_owner_changed:${sessionName}`);
1035
+ return;
1036
+ }
1037
+ const currentServer = requireSafeTmuxServerForMutation(resolveSkcTmuxCommand(env), env);
1038
+ const panePids = listPanePids(nativeSessionId, env);
1039
+ let currentPaneStartTime: string | null;
1040
+ try {
1041
+ currentPaneStartTime = await readStartTime(identity.pid);
1042
+ } catch (error) {
1043
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
1044
+ currentPaneStartTime = null;
1045
+ }
1046
+ const mismatches = [
1047
+ currentServer.pid !== initialServer.pid ? "server_pid" : null,
1048
+ currentServer.startTime !== initialServer.startTime ? "server_start" : null,
1049
+ currentNativeSessionId !== nativeSessionId ? "native_id" : null,
1050
+ readProfileForExactTarget(nativeSessionId, env) !== SKC_TMUX_PROFILE_VALUE ? "profile" : null,
1051
+ panePids.length > 1 ? "pane_count" : null,
1052
+ panePids.length === 1 && panePids[0] !== identity.pid ? "pane_pid" : null,
1053
+ currentPaneStartTime !== null && currentPaneStartTime !== identity.startTime ? "pane_start" : null,
1054
+ readExactOptionForGc(nativeSessionId, SKC_TMUX_SESSION_ID_OPTION, env) !== identity.sessionId
1055
+ ? "session_id"
1056
+ : null,
1057
+ readExactOptionForGc(nativeSessionId, SKC_TMUX_SESSION_STATE_FILE_OPTION, env) !== initialStateFile
1058
+ ? "state_file"
1059
+ : null,
1060
+ readExactOptionForGc(nativeSessionId, SKC_TMUX_OWNER_GENERATION_OPTION, env) !== identity.generation
1061
+ ? "generation"
1062
+ : null,
1063
+ readExactOptionForGc(nativeSessionId, SKC_TMUX_OWNER_SERVER_KEY_OPTION, env) !== identity.socketKey
1064
+ ? "server_key"
1065
+ : null,
1066
+ ].filter((value): value is string => value !== null);
1067
+ if (mismatches.length > 0) throw new Error(`skc_tmux_owner_changed:${sessionName}:${mismatches.join(",")}`);
1068
+ } catch (error) {
1069
+ if (!readNativeTmuxSessionId(nativeSessionId, env)) {
1070
+ if (readNativeTmuxSessionId(sessionName, env)) throw new Error(`skc_tmux_owner_changed:${sessionName}`);
1071
+ return;
1072
+ }
1073
+ if (error instanceof Error && error.message.startsWith(`skc_tmux_owner_changed:${sessionName}`))
1074
+ throw new Error(`skc_tmux_owner_changed:${sessionName}`);
1075
+ throw new Error(`skc_tmux_owner_changed:${sessionName}`);
1076
+ }
1077
+ }
1078
+
1079
+ async function waitForExpectedVerdict(
1080
+ identity: ExactOwnerIdentity,
1081
+ sleep: (ms: number) => Promise<void>,
1082
+ now: () => Date,
1083
+ ): Promise<OwnerVerdict | null> {
1084
+ const deadline = now().getTime() + FORCE_CLOSE_VERDICT_TIMEOUT_MS;
1085
+ const paths = lifecyclePaths(identity.stateDir, identity.sessionId, identity.generation);
1086
+ const verdictFile = paths.verdictFile;
1087
+ const verdictAliasFile = paths.verdictAliasFile;
1088
+ while (now().getTime() <= deadline) {
1089
+ try {
1090
+ const [verdictBody, aliasBody] = await Promise.all([
1091
+ fs.readFile(verdictFile, "utf8"),
1092
+ fs.readFile(verdictAliasFile, "utf8"),
1093
+ ]);
1094
+ const verdict: unknown = JSON.parse(verdictBody);
1095
+ const alias: unknown = JSON.parse(aliasBody);
1096
+ if (
1097
+ isValidOwnerVerdict(verdict) &&
1098
+ typeof alias === "object" &&
1099
+ alias !== null &&
1100
+ Object.keys(alias).length === Object.keys(verdict).length + 1 &&
1101
+ Object.keys(alias).every(key => key === "owner_generation" || Object.hasOwn(verdict, key)) &&
1102
+ (alias as Record<string, unknown>).owner_generation === identity.generation &&
1103
+ Object.entries(verdict).every(
1104
+ ([key, value]) => Object.hasOwn(alias, key) && (alias as Record<string, unknown>)[key] === value,
1105
+ ) &&
1106
+ verdict.generation === identity.generation &&
1107
+ verdict.session_id === identity.sessionId &&
1108
+ verdict.server_key === identity.socketKey &&
1109
+ verdict.signal === "SIGTERM" &&
1110
+ verdict.result === "owner_term_then_session_cleanup" &&
1111
+ verdict.classification === "expected_operator_shutdown"
1112
+ )
1113
+ return verdict;
1114
+ } catch {}
1115
+ await sleep(FORCE_CLOSE_VERDICT_POLL_MS);
1116
+ }
1117
+ return null;
1118
+ }
1119
+
368
1120
  /**
369
- * Force-close a SKC-managed tmux session, even if a live pane is attached.
370
- *
371
- * This is the lifecycle-control counterpart to {@link removeSkcTmuxSession}: it
372
- * intentionally does NOT refuse live/attached panes (hard-kill is the contract),
373
- * but it keeps every safety check so it can only ever kill a genuinely
374
- * SKC-managed session:
375
- * - re-reads the exact tmux profile immediately before kill (never a non-SKC
376
- * session, even one that collides by name);
377
- * - when `expectedSessionId` is given, requires the `@skc-session-id` tag match;
378
- * - when `expectedStateFile` is given, requires the `@skc-session-state-file`
379
- * tag match.
380
- *
381
- * Returns the prior status (for audit). Throws a tagged error otherwise:
382
- * `skc_tmux_session_not_found`, `skc_tmux_session_not_managed`,
383
- * `skc_tmux_session_id_mismatch`, or `skc_tmux_session_state_file_mismatch`.
1121
+ * Requests an exact tagged owner shutdown. Session cleanup is compatibility-only
1122
+ * and follows a validated SIGTERM verdict; it never authorizes the close.
384
1123
  */
385
- export function forceCloseSkcTmuxSession(
1124
+ export async function forceCloseSkcTmuxSession(
386
1125
  sessionName: string,
387
1126
  env: NodeJS.ProcessEnv = process.env,
388
1127
  expectedSessionId?: string,
389
1128
  expectedStateFile?: string,
390
- ): SkcTmuxSessionStatus {
1129
+ deps: Partial<ForceCloseOwnerDependencies> = {},
1130
+ ): Promise<SkcTmuxSessionStatus> {
1131
+ if (resolveSkcTmuxBinary({ env }).isPsmux) throw new Error(`skc_tmux_owner_unverifiable:${sessionName}`);
391
1132
  const session = statusSkcTmuxSession(sessionName, env);
392
- if (readProfileForExactTarget(session.name, env) !== SKC_TMUX_PROFILE_VALUE) {
1133
+ if (readProfileForExactTarget(session.name, env) !== SKC_TMUX_PROFILE_VALUE)
393
1134
  throw new Error(`skc_tmux_session_not_managed:${sessionName}`);
394
- }
395
- if (expectedSessionId !== undefined) {
396
- const actual = readExactOptionForGc(session.name, SKC_TMUX_SESSION_ID_OPTION, env);
397
- if (actual !== expectedSessionId) {
398
- throw new Error(`skc_tmux_session_id_mismatch:${sessionName}`);
399
- }
400
- }
401
- if (expectedStateFile !== undefined) {
402
- const actual = readExactOptionForGc(session.name, SKC_TMUX_SESSION_STATE_FILE_OPTION, env);
403
- if (actual !== expectedStateFile) {
404
- throw new Error(`skc_tmux_session_state_file_mismatch:${sessionName}`);
405
- }
406
- }
407
- // Intentionally NOT refusing live/attached panes — force-close is hard-kill.
408
- runTmux(["kill-session", "-t", buildSkcTmuxExactSessionTarget(session.name, { env })], env);
1135
+ const exactPanePids = (deps.listPanePids ?? readExactSessionPanePids)(session.name, env);
1136
+ if (exactPanePids.length !== 1) throw new Error(`skc_tmux_owner_unverifiable:${sessionName}`);
1137
+ const actualSessionId = readExactOptionForGc(session.name, SKC_TMUX_SESSION_ID_OPTION, env);
1138
+ const actualStateFile = readExactOptionForGc(session.name, SKC_TMUX_SESSION_STATE_FILE_OPTION, env);
1139
+ const actualGeneration = readExactOptionForGc(session.name, SKC_TMUX_OWNER_GENERATION_OPTION, env);
1140
+ const actualServerKey = readExactOptionForGc(session.name, SKC_TMUX_OWNER_SERVER_KEY_OPTION, env);
1141
+
1142
+ if (expectedSessionId !== undefined && actualSessionId !== expectedSessionId)
1143
+ throw new Error(`skc_tmux_session_id_mismatch:${sessionName}`);
1144
+ if (expectedStateFile !== undefined && actualStateFile !== expectedStateFile)
1145
+ throw new Error(`skc_tmux_session_state_file_mismatch:${sessionName}`);
1146
+ if (!actualSessionId || !actualStateFile || !actualGeneration || !actualServerKey)
1147
+ throw new Error(`skc_tmux_owner_unverifiable:${sessionName}`);
1148
+ const nativeSessionId = readNativeTmuxSessionId(session.name, env);
1149
+ if (!nativeSessionId) throw new Error(`skc_tmux_owner_unverifiable:${sessionName}`);
1150
+
1151
+ const resolveOwner =
1152
+ deps.resolveOwner ?? ((name, targetEnv) => resolveExactOwner(name, targetEnv, exactPanePids[0]!));
1153
+ const identity = await resolveOwner(session.name, env);
1154
+ if (identity.pid !== exactPanePids[0]) throw new Error(`skc_tmux_owner_identity_mismatch:${sessionName}`);
1155
+ if (identity.sessionId !== actualSessionId || identity.stateDir !== path.dirname(actualStateFile))
1156
+ throw new Error(`skc_tmux_owner_identity_mismatch:${sessionName}`);
1157
+ if (identity.generation !== actualGeneration) throw new Error(`skc_tmux_owner_generation_mismatch:${sessionName}`);
1158
+ if (identity.socketKey !== actualServerKey) throw new Error(`skc_tmux_owner_server_key_mismatch:${sessionName}`);
1159
+ const initialServer = requireSafeTmuxServerForMutation(resolveSkcTmuxCommand(env), env);
1160
+
1161
+ const currentStartTime = await (deps.readProcessStartTime ?? readProcessStartTime)(identity.pid);
1162
+ if (currentStartTime !== identity.startTime) throw new Error("owner_pid_identity_mismatch");
1163
+ const now = deps.now ?? (() => new Date());
1164
+ const sleep = deps.sleep ?? (ms => Bun.sleep(ms));
1165
+ const dispatchId = crypto.randomUUID();
1166
+ await closeExactTmuxOwner(
1167
+ {
1168
+ stateDir: identity.stateDir,
1169
+ sessionId: identity.sessionId,
1170
+ generation: identity.generation,
1171
+ serverKey: identity.socketKey,
1172
+ pid: identity.pid,
1173
+ startTime: identity.startTime,
1174
+ dispatchId,
1175
+ createdAt: now().toISOString(),
1176
+ expiresAt: new Date(now().getTime() + FORCE_CLOSE_VERDICT_TIMEOUT_MS).toISOString(),
1177
+ },
1178
+ {
1179
+ readStartTime: deps.readProcessStartTime ?? readProcessStartTime,
1180
+ sendSigterm: async pid => {
1181
+ if ((await (deps.readProcessStartTime ?? readProcessStartTime)(pid)) !== identity.startTime)
1182
+ throw new Error("owner_pid_identity_mismatch");
1183
+ (deps.signalTerm ?? (target => process.kill(target, "SIGTERM")))(pid);
1184
+ },
1185
+
1186
+ waitForVerdict: () => waitForExpectedVerdict(identity, sleep, now),
1187
+ cleanupSession: async () => {
1188
+ await requireUnchangedOwnerForCompatibilityCleanup(
1189
+ session.name,
1190
+ nativeSessionId,
1191
+ env,
1192
+ identity,
1193
+ actualStateFile,
1194
+ initialServer,
1195
+ deps.listPanePids ?? readExactSessionPanePids,
1196
+ deps.readProcessStartTime ?? readProcessStartTime,
1197
+ );
1198
+ if (deps.cleanupSession) deps.cleanupSession(nativeSessionId, env);
1199
+ else
1200
+ runGuardedTmuxSessionCommand(
1201
+ nativeSessionId,
1202
+ session.name,
1203
+ initialServer.pid,
1204
+ env,
1205
+ `kill-session -t '${nativeSessionId}'`,
1206
+ );
1207
+ },
1208
+ },
1209
+ );
409
1210
  return session;
410
1211
  }
411
1212
 
412
1213
  export function attachSkcTmuxSession(sessionName: string, env: NodeJS.ProcessEnv = process.env): never {
1214
+ if (resolveSkcTmuxBinary({ env }).isPsmux) throw new Error(`skc_tmux_owner_unverifiable:${sessionName}`);
413
1215
  const session = statusSkcTmuxSession(sessionName, env);
414
1216
  const tmuxCommand = resolveSkcTmuxCommand(env);
1217
+ requireSafeTmuxServerForMutation(tmuxCommand, env);
415
1218
  const result = Bun.spawnSync(
416
1219
  [tmuxCommand, "attach-session", "-t", buildSkcTmuxExactSessionTarget(session.name, { env })],
417
1220
  {