@sentropic/h2a-runtime 0.98.6 → 0.98.7-rc.10

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 (236) hide show
  1. package/dist/agent-launch-args.d.ts.map +1 -1
  2. package/dist/agent-launch-args.js +5 -5
  3. package/dist/agent-launch-args.js.map +1 -1
  4. package/dist/agy-claude.d.ts +42 -0
  5. package/dist/agy-claude.d.ts.map +1 -0
  6. package/dist/agy-claude.js +148 -0
  7. package/dist/agy-claude.js.map +1 -0
  8. package/dist/agy-headless.d.ts +11 -0
  9. package/dist/agy-headless.d.ts.map +1 -0
  10. package/dist/agy-headless.js +104 -0
  11. package/dist/agy-headless.js.map +1 -0
  12. package/dist/attach.d.ts +5 -0
  13. package/dist/attach.d.ts.map +1 -1
  14. package/dist/attach.js +75 -6
  15. package/dist/attach.js.map +1 -1
  16. package/dist/auth-login.d.ts +24 -1
  17. package/dist/auth-login.d.ts.map +1 -1
  18. package/dist/auth-login.js +132 -4
  19. package/dist/auth-login.js.map +1 -1
  20. package/dist/central-mcp.d.ts +34 -0
  21. package/dist/central-mcp.d.ts.map +1 -1
  22. package/dist/central-mcp.js +275 -71
  23. package/dist/central-mcp.js.map +1 -1
  24. package/dist/claude-code-cli-isolation.d.ts +53 -0
  25. package/dist/claude-code-cli-isolation.d.ts.map +1 -0
  26. package/dist/claude-code-cli-isolation.js +277 -0
  27. package/dist/claude-code-cli-isolation.js.map +1 -0
  28. package/dist/claude-code-cli-lifecycle.d.ts +80 -0
  29. package/dist/claude-code-cli-lifecycle.d.ts.map +1 -0
  30. package/dist/claude-code-cli-lifecycle.js +402 -0
  31. package/dist/claude-code-cli-lifecycle.js.map +1 -0
  32. package/dist/claude-code-cli-runner.d.ts +89 -0
  33. package/dist/claude-code-cli-runner.d.ts.map +1 -0
  34. package/dist/claude-code-cli-runner.js +659 -0
  35. package/dist/claude-code-cli-runner.js.map +1 -0
  36. package/dist/claude-seat-input.d.ts +47 -0
  37. package/dist/claude-seat-input.d.ts.map +1 -0
  38. package/dist/claude-seat-input.js +272 -0
  39. package/dist/claude-seat-input.js.map +1 -0
  40. package/dist/claude-seat-mesh-contract.d.ts +88 -0
  41. package/dist/claude-seat-mesh-contract.d.ts.map +1 -0
  42. package/dist/claude-seat-mesh-contract.js +13 -0
  43. package/dist/claude-seat-mesh-contract.js.map +1 -0
  44. package/dist/cli-help-groups.js +1 -1
  45. package/dist/cli-help-groups.js.map +1 -1
  46. package/dist/cli-seat-codecs.d.ts +86 -0
  47. package/dist/cli-seat-codecs.d.ts.map +1 -0
  48. package/dist/cli-seat-codecs.js +255 -0
  49. package/dist/cli-seat-codecs.js.map +1 -0
  50. package/dist/cli-seat-fixtures.d.ts +86 -0
  51. package/dist/cli-seat-fixtures.d.ts.map +1 -0
  52. package/dist/cli-seat-fixtures.js +114 -0
  53. package/dist/cli-seat-fixtures.js.map +1 -0
  54. package/dist/cli-seat-materialization.d.ts +245 -0
  55. package/dist/cli-seat-materialization.d.ts.map +1 -0
  56. package/dist/cli-seat-materialization.js +646 -0
  57. package/dist/cli-seat-materialization.js.map +1 -0
  58. package/dist/conv-guard.d.ts.map +1 -1
  59. package/dist/conv-guard.js +6 -2
  60. package/dist/conv-guard.js.map +1 -1
  61. package/dist/crash-log.d.ts +112 -0
  62. package/dist/crash-log.d.ts.map +1 -0
  63. package/dist/crash-log.js +560 -0
  64. package/dist/crash-log.js.map +1 -0
  65. package/dist/delegate.d.ts +10 -0
  66. package/dist/delegate.d.ts.map +1 -1
  67. package/dist/delegate.js +29 -0
  68. package/dist/delegate.js.map +1 -1
  69. package/dist/file-lock.d.ts +4 -2
  70. package/dist/file-lock.d.ts.map +1 -1
  71. package/dist/file-lock.js +24 -10
  72. package/dist/file-lock.js.map +1 -1
  73. package/dist/gateway-host/claude-seat-client.d.ts +32 -0
  74. package/dist/gateway-host/claude-seat-client.d.ts.map +1 -0
  75. package/dist/gateway-host/claude-seat-client.js +38 -0
  76. package/dist/gateway-host/claude-seat-client.js.map +1 -0
  77. package/dist/gateway-host/config-file.d.ts +2 -0
  78. package/dist/gateway-host/config-file.d.ts.map +1 -1
  79. package/dist/gateway-host/config-file.js +5 -1
  80. package/dist/gateway-host/config-file.js.map +1 -1
  81. package/dist/gateway-host/daemon.d.ts.map +1 -1
  82. package/dist/gateway-host/daemon.js +1 -2
  83. package/dist/gateway-host/daemon.js.map +1 -1
  84. package/dist/gateway-host/host.d.ts +6 -0
  85. package/dist/gateway-host/host.d.ts.map +1 -1
  86. package/dist/gateway-host/host.js +24 -3
  87. package/dist/gateway-host/host.js.map +1 -1
  88. package/dist/gateway-host/live-auth.d.ts +62 -0
  89. package/dist/gateway-host/live-auth.d.ts.map +1 -0
  90. package/dist/gateway-host/live-auth.js +87 -0
  91. package/dist/gateway-host/live-auth.js.map +1 -0
  92. package/dist/gateway-host/live-declarations.d.ts +58 -0
  93. package/dist/gateway-host/live-declarations.d.ts.map +1 -0
  94. package/dist/gateway-host/live-declarations.js +429 -0
  95. package/dist/gateway-host/live-declarations.js.map +1 -0
  96. package/dist/gateway-host/live-disclosure.d.ts +124 -0
  97. package/dist/gateway-host/live-disclosure.d.ts.map +1 -0
  98. package/dist/gateway-host/live-disclosure.js +347 -0
  99. package/dist/gateway-host/live-disclosure.js.map +1 -0
  100. package/dist/gateway-host/live-github.d.ts +11 -0
  101. package/dist/gateway-host/live-github.d.ts.map +1 -0
  102. package/dist/gateway-host/live-github.js +374 -0
  103. package/dist/gateway-host/live-github.js.map +1 -0
  104. package/dist/gateway-host/live-host.d.ts +20 -0
  105. package/dist/gateway-host/live-host.d.ts.map +1 -0
  106. package/dist/gateway-host/live-host.js +14 -0
  107. package/dist/gateway-host/live-host.js.map +1 -0
  108. package/dist/gateway-host/live-object-store.d.ts +35 -0
  109. package/dist/gateway-host/live-object-store.d.ts.map +1 -0
  110. package/dist/gateway-host/live-object-store.js +447 -0
  111. package/dist/gateway-host/live-object-store.js.map +1 -0
  112. package/dist/gateway-host/live-registration.d.ts +162 -0
  113. package/dist/gateway-host/live-registration.d.ts.map +1 -0
  114. package/dist/gateway-host/live-registration.js +455 -0
  115. package/dist/gateway-host/live-registration.js.map +1 -0
  116. package/dist/gateway-model-catalog/model-catalog.d.ts +38 -0
  117. package/dist/gateway-model-catalog/model-catalog.d.ts.map +1 -0
  118. package/dist/gateway-model-catalog/model-catalog.js +167 -0
  119. package/dist/gateway-model-catalog/model-catalog.js.map +1 -0
  120. package/dist/identity-cull/cull.js +1 -1
  121. package/dist/identity-cull/cull.js.map +1 -1
  122. package/dist/identity-cull/identity-fence.d.ts +48 -0
  123. package/dist/identity-cull/identity-fence.d.ts.map +1 -0
  124. package/dist/identity-cull/identity-fence.js +221 -0
  125. package/dist/identity-cull/identity-fence.js.map +1 -0
  126. package/dist/identity-operator-break.d.ts +43 -0
  127. package/dist/identity-operator-break.d.ts.map +1 -0
  128. package/dist/identity-operator-break.js +85 -0
  129. package/dist/identity-operator-break.js.map +1 -0
  130. package/dist/index.d.ts +8 -1
  131. package/dist/index.d.ts.map +1 -1
  132. package/dist/index.js +525 -132
  133. package/dist/index.js.map +1 -1
  134. package/dist/launch-layout.d.ts.map +1 -1
  135. package/dist/launch-layout.js +10 -2
  136. package/dist/launch-layout.js.map +1 -1
  137. package/dist/llm-mesh-accounts.d.ts +128 -6
  138. package/dist/llm-mesh-accounts.d.ts.map +1 -1
  139. package/dist/llm-mesh-accounts.js +370 -14
  140. package/dist/llm-mesh-accounts.js.map +1 -1
  141. package/dist/native-host.d.ts +25 -1
  142. package/dist/native-host.d.ts.map +1 -1
  143. package/dist/native-host.js +211 -28
  144. package/dist/native-host.js.map +1 -1
  145. package/dist/native-restart.d.ts +2 -0
  146. package/dist/native-restart.d.ts.map +1 -1
  147. package/dist/native-restart.js +7 -0
  148. package/dist/native-restart.js.map +1 -1
  149. package/dist/native-terminal/fleet.d.ts +11 -0
  150. package/dist/native-terminal/fleet.d.ts.map +1 -1
  151. package/dist/native-terminal/fleet.js +58 -1
  152. package/dist/native-terminal/fleet.js.map +1 -1
  153. package/dist/native-terminal/host-scope.d.ts +18 -0
  154. package/dist/native-terminal/host-scope.d.ts.map +1 -0
  155. package/dist/native-terminal/host-scope.js +65 -0
  156. package/dist/native-terminal/host-scope.js.map +1 -0
  157. package/dist/native-terminal/host.d.ts +7 -1
  158. package/dist/native-terminal/host.d.ts.map +1 -1
  159. package/dist/native-terminal/host.js +10 -26
  160. package/dist/native-terminal/host.js.map +1 -1
  161. package/dist/native-terminal/journal.d.ts +5 -0
  162. package/dist/native-terminal/journal.d.ts.map +1 -1
  163. package/dist/native-terminal/journal.js +28 -16
  164. package/dist/native-terminal/journal.js.map +1 -1
  165. package/dist/native-terminal/op.d.ts +49 -1
  166. package/dist/native-terminal/op.d.ts.map +1 -1
  167. package/dist/native-terminal/op.js +254 -38
  168. package/dist/native-terminal/op.js.map +1 -1
  169. package/dist/native-terminal/process.d.ts.map +1 -1
  170. package/dist/native-terminal/process.js +10 -1
  171. package/dist/native-terminal/process.js.map +1 -1
  172. package/dist/native-terminal/screen.d.ts.map +1 -1
  173. package/dist/native-terminal/screen.js +1 -1
  174. package/dist/native-terminal/screen.js.map +1 -1
  175. package/dist/native-terminal/server.d.ts +12 -2
  176. package/dist/native-terminal/server.d.ts.map +1 -1
  177. package/dist/native-terminal/server.js +143 -42
  178. package/dist/native-terminal/server.js.map +1 -1
  179. package/dist/native-terminal/session-scope.d.ts +2 -0
  180. package/dist/native-terminal/session-scope.d.ts.map +1 -0
  181. package/dist/native-terminal/session-scope.js +32 -0
  182. package/dist/native-terminal/session-scope.js.map +1 -0
  183. package/dist/proc-identity.d.ts +31 -0
  184. package/dist/proc-identity.d.ts.map +1 -1
  185. package/dist/proc-identity.js +54 -0
  186. package/dist/proc-identity.js.map +1 -1
  187. package/dist/prompt-delivery.d.ts +3 -0
  188. package/dist/prompt-delivery.d.ts.map +1 -1
  189. package/dist/prompt-delivery.js +8 -3
  190. package/dist/prompt-delivery.js.map +1 -1
  191. package/dist/protocol-local.d.ts +16 -0
  192. package/dist/protocol-local.d.ts.map +1 -1
  193. package/dist/protocol-local.js +14 -0
  194. package/dist/protocol-local.js.map +1 -1
  195. package/dist/registry.d.ts +16 -12
  196. package/dist/registry.d.ts.map +1 -1
  197. package/dist/registry.js +39 -5
  198. package/dist/registry.js.map +1 -1
  199. package/dist/relaunch.d.ts +16 -0
  200. package/dist/relaunch.d.ts.map +1 -1
  201. package/dist/relaunch.js +42 -1
  202. package/dist/relaunch.js.map +1 -1
  203. package/dist/restore.d.ts +4 -0
  204. package/dist/restore.d.ts.map +1 -1
  205. package/dist/restore.js +9 -37
  206. package/dist/restore.js.map +1 -1
  207. package/dist/resume-launch-options.d.ts +3 -3
  208. package/dist/resume-launch-options.d.ts.map +1 -1
  209. package/dist/resume-launch-options.js +7 -4
  210. package/dist/resume-launch-options.js.map +1 -1
  211. package/dist/run.d.ts +2 -0
  212. package/dist/run.d.ts.map +1 -1
  213. package/dist/run.js +88 -20
  214. package/dist/run.js.map +1 -1
  215. package/dist/soft-refresh.d.ts.map +1 -1
  216. package/dist/soft-refresh.js +6 -0
  217. package/dist/soft-refresh.js.map +1 -1
  218. package/dist/terminal-reset.d.ts +104 -0
  219. package/dist/terminal-reset.d.ts.map +1 -0
  220. package/dist/terminal-reset.js +229 -0
  221. package/dist/terminal-reset.js.map +1 -0
  222. package/dist/tmux.d.ts +54 -8
  223. package/dist/tmux.d.ts.map +1 -1
  224. package/dist/tmux.js +143 -36
  225. package/dist/tmux.js.map +1 -1
  226. package/dist/vendor/engram-live/contracts.d.ts +825 -0
  227. package/dist/vendor/engram-live/contracts.js +1734 -0
  228. package/dist/vendor/engram-live/integration.d.ts +113 -0
  229. package/dist/vendor/engram-live/integration.js +254 -0
  230. package/dist/vendor/engram-live/pr-sync.d.ts +144 -0
  231. package/dist/vendor/engram-live/pr-sync.js +411 -0
  232. package/dist/vendor/engram-live/registration.d.ts +138 -0
  233. package/dist/vendor/engram-live/registration.js +358 -0
  234. package/dist/vendor/engram-live/retention.d.ts +56 -0
  235. package/dist/vendor/engram-live/retention.js +179 -0
  236. package/package.json +6 -6
@@ -34,16 +34,35 @@ import { dirname } from "node:path";
34
34
  import { fileURLToPath } from "node:url";
35
35
  import { NativeTerminalClient } from "./client.js";
36
36
  import { loadRegistry, registryEntriesForNativeTarget } from "../registry.js";
37
- import { NATIVE_TERMINAL_INTERACTIVE_READ_TIMEOUT_MS, NATIVE_TERMINAL_PROTOCOL_VERSION, NativeTerminalRemoteError, } from "./protocol.js";
37
+ import { NATIVE_TERMINAL_INTERACTIVE_READ_TIMEOUT_MS, NATIVE_TERMINAL_HEALTH_TIMEOUT_MS, NATIVE_TERMINAL_PROTOCOL_VERSION, NativeTerminalRemoteError, } from "./protocol.js";
38
38
  import { NativeTerminalHostSupervisor } from "./supervisor.js";
39
- import { proveNativeTerminalEndpointAbsent } from "./server.js";
39
+ import { proveNativeTerminalEndpointAbsent, withNativeTerminalStartupLock } from "./server.js";
40
40
  import { defaultNativeTerminalSocketPath, knownNativeTerminalSocketPaths, inspectPrivateNativeTerminalSocket, sameNativeTerminalSocket } from "./socket-path.js";
41
- import { collectNativeInventory, resolveNativeOwner } from "./fleet.js";
41
+ import { collectNativeInventory, publicationContenderObserved, readNativeTerminalSuccessor, resolveNativeOwner } from "./fleet.js";
42
42
  import { renderTerminalScreen } from "./screen.js";
43
43
  import { TerminalModeTracker } from "./terminal-modes.js";
44
44
  import { publishAttachReset } from "./attach-recovery.js";
45
45
  import { TerminalResetRelay } from "./reset-relay.js";
46
+ import { appendCrashLog, crashSignalName, installCrashHandlers } from "../crash-log.js";
46
47
  const DETACH_BYTE = 0x1c; // Ctrl-\
48
+ /**
49
+ * Build the crash-log record for an observed session exit. Only observed
50
+ * values are recorded: `exitCode` comes from the session's exit report and
51
+ * `signal` from the stop order (`stopSignal`) or the pty exit signal. Either
52
+ * field is omitted when unknown — never defaulted to 0/1.
53
+ */
54
+ export function sessionExitCrashEntry(state, options) {
55
+ const exitCode = state.exit?.exitCode;
56
+ const signal = state.stopSignal ?? crashSignalName(state.exit?.signal ?? undefined);
57
+ return {
58
+ event: "sessionExit",
59
+ sessionId: options.sessionId,
60
+ ...(options.reason ? { reason: options.reason } : {}),
61
+ ...(options.detail ? { detail: options.detail } : {}),
62
+ ...(typeof exitCode === "number" && Number.isSafeInteger(exitCode) ? { exitCode } : {}),
63
+ ...(signal ? { signal } : {}),
64
+ };
65
+ }
47
66
  function parseArgv(argv) {
48
67
  const op = argv[2];
49
68
  if (!op)
@@ -77,7 +96,7 @@ function required(parsed, name) {
77
96
  throw new Error(`--${name} is required`);
78
97
  return value;
79
98
  }
80
- function emit(payload) {
99
+ function emitResult(payload) {
81
100
  process.stdout.write(`${JSON.stringify(payload)}\n`);
82
101
  }
83
102
  export function stripAnsi(text) {
@@ -104,12 +123,17 @@ export function closeAllNativeTerminalOpClients() {
104
123
  client.close();
105
124
  openClients.clear();
106
125
  }
107
- async function connectExisting(socketPath, tracked = true) {
126
+ async function connectExisting(socketPath, tracked = true, signal) {
127
+ signal?.throwIfAborted();
108
128
  const identity = await inspectPrivateNativeTerminalSocket(socketPath);
129
+ signal?.throwIfAborted();
109
130
  const client = await NativeTerminalClient.connect(socketPath);
110
131
  if (tracked)
111
132
  openClients.add(client);
133
+ const abort = () => client.close();
134
+ signal?.addEventListener("abort", abort, { once: true });
112
135
  try {
136
+ signal?.throwIfAborted();
113
137
  const ping = await client.ping();
114
138
  if (ping.protocolVersion !== NATIVE_TERMINAL_PROTOCOL_VERSION || !ping.generation?.trim() ||
115
139
  !Number.isSafeInteger(ping.hostPid) || ping.hostPid <= 0 ||
@@ -119,8 +143,12 @@ async function connectExisting(socketPath, tracked = true) {
119
143
  }
120
144
  catch (error) {
121
145
  client.close();
146
+ openClients.delete(client);
122
147
  throw error;
123
148
  }
149
+ finally {
150
+ signal?.removeEventListener("abort", abort);
151
+ }
124
152
  return client;
125
153
  }
126
154
  class NativeOwnerError extends Error {
@@ -134,6 +162,34 @@ async function inventoryFor(parsed) {
134
162
  const pinned = parsed.flags.get("socket");
135
163
  return collectNativeInventory(pinned ? [pinned] : knownNativeTerminalSocketPaths(), connectExisting, proveNativeTerminalEndpointAbsent);
136
164
  }
165
+ /** Same three-state proof for in-process callers and the one-shot probe.
166
+ * Own these connections locally: an async caller must close them even when
167
+ * another endpoint makes the inventory incomplete. */
168
+ export async function probeNativeSession(id, socketPath, ensureHost = false) {
169
+ // A run may prepare its host before the read-only guards. Otherwise every
170
+ // relauncher first queues multiple absence proofs, delaying the publisher
171
+ // until those same callers' publication/readiness budgets expire.
172
+ if (ensureHost) {
173
+ const client = socketPath ? await launchClient(socketPath) : (await selectLaunchClient(true)).client;
174
+ client.close();
175
+ openClients.delete(client);
176
+ }
177
+ const inventory = await collectNativeInventory(socketPath ? [socketPath] : knownNativeTerminalSocketPaths(), path => connectExisting(path, false), proveNativeTerminalEndpointAbsent);
178
+ try {
179
+ const owner = resolveNativeOwner(id, inventory);
180
+ if (owner.state === "found")
181
+ return { verdict: owner.session.status === "running" ? "live" : "dead",
182
+ state: { ...owner.session, socketPath: owner.socketPath } };
183
+ if (owner.state === "absent")
184
+ return { verdict: "dead" };
185
+ return { verdict: "unknown", code: owner.state,
186
+ ...(owner.state === "unknown" ? { reason: owner.reason } : { sockets: owner.sockets, reason: "ambiguous-owner" }) };
187
+ }
188
+ finally {
189
+ for (const host of inventory.hosts)
190
+ host.client?.close();
191
+ }
192
+ }
137
193
  async function owningClient(parsed, id) {
138
194
  const owner = resolveNativeOwner(id, await inventoryFor(parsed));
139
195
  if (owner.state !== "found")
@@ -153,31 +209,71 @@ async function selectLaunchClient(fenced) {
153
209
  }
154
210
  const historical = paths[0];
155
211
  const client = await launchClient(historical);
156
- const ping = await client.ping();
157
- if (!fenced || paths.length === 1)
158
- return { client, socketPath: historical };
159
- const compatible = paths[1];
160
- // Initialize the second known endpoint so admission obtains a complete
161
- // inventory even when the historical host already provides the fence.
162
- const second = await launchClient(compatible);
163
- return ping.launchFence === true ? { client, socketPath: historical } : { client: second, socketPath: compatible };
212
+ try {
213
+ const ping = await client.ping();
214
+ if (!fenced || paths.length === 1)
215
+ return { client, socketPath: historical };
216
+ const compatible = paths[1];
217
+ // Initialize the second known endpoint so admission obtains a complete
218
+ // inventory even when the historical host already provides the fence.
219
+ const second = await launchClient(compatible);
220
+ const unused = ping.launchFence === true ? second : client;
221
+ unused.close();
222
+ openClients.delete(unused);
223
+ return ping.launchFence === true ? { client, socketPath: historical } : { client: second, socketPath: compatible };
224
+ }
225
+ catch (error) {
226
+ client.close();
227
+ openClients.delete(client);
228
+ throw error;
229
+ }
164
230
  }
165
231
  async function launchClient(socketPath) {
232
+ let failure;
166
233
  try {
167
234
  return await connectExisting(socketPath);
168
235
  }
169
236
  catch (error) {
170
- let absent = false;
171
- try {
172
- absent = await proveNativeTerminalEndpointAbsent(socketPath, error);
173
- }
174
- catch (proofError) {
175
- error = new Error(`${error instanceof Error ? error.message : String(error)}; absence proof failed: ${proofError instanceof Error ? proofError.message : String(proofError)}`);
237
+ failure = error;
238
+ }
239
+ try {
240
+ return await withNativeTerminalStartupLock(socketPath, () => launchClientUnderLock(socketPath, failure), signal => connectExisting(socketPath, true, signal));
241
+ }
242
+ catch (error) {
243
+ // Election expiry is contention, just like publication-lock expiry. The
244
+ // elected publisher can finish as our acquisition deadline expires. Spend
245
+ // the existing successor-health budget on read-only adoption; never use
246
+ // this failure as an absence proof or permission to spawn another host.
247
+ if (error?.name === "TimeoutError" || publicationContenderObserved(error)) {
248
+ const successorSignal = AbortSignal.timeout(NATIVE_TERMINAL_HEALTH_TIMEOUT_MS);
249
+ try {
250
+ return await readNativeTerminalSuccessor(() => connectExisting(socketPath, true, successorSignal), true);
251
+ }
252
+ catch { /* Preserve the original fail-closed election diagnostic. */ }
176
253
  }
177
- if (absent)
178
- return ensureClient(socketPath);
179
- throw Object.assign(new Error(`${socketPath}: ${error instanceof Error ? error.message : String(error)}; owner death is unproven. Inspect endpoint permissions and the owning process identity, restore probe access, then retry; do not remove a socket or lock while ownership is unproven.`), { socketPath });
254
+ throw error;
255
+ }
256
+ }
257
+ async function launchClientUnderLock(socketPath, error) {
258
+ let absent = false;
259
+ let publisherObserved = false;
260
+ // Launch-only reclamation: inventories never remove the stale inode.
261
+ try {
262
+ absent = await proveNativeTerminalEndpointAbsent(socketPath, error, undefined, undefined, true);
263
+ }
264
+ catch (proofError) {
265
+ publisherObserved = publicationContenderObserved(proofError);
266
+ error = new Error(`${error instanceof Error ? error.message : String(error)}; absence proof failed: ${proofError instanceof Error ? proofError.message : String(proofError)}`);
267
+ }
268
+ if (absent)
269
+ return ensureClient(socketPath);
270
+ // Publication may have completed while the absence proof held/waited for
271
+ // its lock. A fresh handshake can adopt that winner without any takeover.
272
+ try {
273
+ return await readNativeTerminalSuccessor(() => connectExisting(socketPath), publisherObserved);
180
274
  }
275
+ catch { /* Still uncertain: retain the original proof failure below. */ }
276
+ throw Object.assign(new Error(`${socketPath}: ${error instanceof Error ? error.message : String(error)}; owner death is unproven. Inspect endpoint permissions and the owning process identity, restore probe access, then retry; do not remove a socket or lock while ownership is unproven.`), { socketPath });
181
277
  }
182
278
  async function ensureClient(socketPath) {
183
279
  const supervisor = new NativeTerminalHostSupervisor({
@@ -375,6 +471,10 @@ export async function runAttach(id, runtime = defaultAttachRuntime(socketPathFro
375
471
  : "controller released or session replaced";
376
472
  stdout.write(`\r\n[h2a] ${reason}; attach detached\r\n`);
377
473
  exitCode = state.status === "exited" && !state.stopSignal ? state.exit?.exitCode ?? 0 : 0;
474
+ if (state.status === "exited")
475
+ appendCrashLog("attach", sessionExitCrashEntry(state, { sessionId: id, detail: reason }));
476
+ else
477
+ appendCrashLog("attach", { event: "refusal", sessionId: id, reason: "controller-changed", detail: reason });
378
478
  terminalEnded = true;
379
479
  return true;
380
480
  };
@@ -408,6 +508,7 @@ export async function runAttach(id, runtime = defaultAttachRuntime(socketPathFro
408
508
  return;
409
509
  if (error instanceof NativeTerminalRemoteError && error.message === "stale terminal controller lease") {
410
510
  stdout.write("\r\n[h2a] controller revoked by takeover or stop; attach detached\r\n");
511
+ appendCrashLog("attach", { event: "refusal", sessionId: id, reason: "stale-controller-lease" });
411
512
  terminalEnded = true;
412
513
  closeActive();
413
514
  return;
@@ -458,6 +559,7 @@ export async function runAttach(id, runtime = defaultAttachRuntime(socketPathFro
458
559
  if (pendingInputBytes + chunk.length > ATTACH_PENDING_INPUT_MAX_BYTES) {
459
560
  stdout.write("\r\n[h2a] native attach input buffer full during reconnect; detaching " +
460
561
  "without stopping the session\r\n");
562
+ appendCrashLog("attach", { event: "refusal", sessionId: id, reason: "input-buffer-full" });
461
563
  detached = true;
462
564
  closeActive();
463
565
  return;
@@ -506,9 +608,22 @@ export async function runAttach(id, runtime = defaultAttachRuntime(socketPathFro
506
608
  if (isRawCapable)
507
609
  stdin.setRawMode(true);
508
610
  stdin.resume();
509
- const initial = await connectAndAcquire();
510
- if (initial.status === "exited")
611
+ let initial;
612
+ try {
613
+ initial = await connectAndAcquire();
614
+ }
615
+ catch (error) {
616
+ // Attaching to a provably absent session is an observed session end,
617
+ // not a crash without a record. Anything else still throws.
618
+ if (!isDefinitiveNativeTerminalAbsence(error))
619
+ throw error;
620
+ appendCrashLog("attach", sessionExitCrashEntry({ exit: null, stopSignal: null }, { sessionId: id, reason: "definitive-absence", detail: "session absent at attach; exit code unknown" }));
621
+ return 1;
622
+ }
623
+ if (initial.status === "exited") {
624
+ appendCrashLog("attach", sessionExitCrashEntry(initial, { sessionId: id, detail: "session already exited at attach" }));
511
625
  return initial.exit?.exitCode ?? 0;
626
+ }
512
627
  let idlePollMs = ATTACH_IDLE_POLL_MIN_MS;
513
628
  let nextStatePollAt = runtime.now() + ATTACH_STATE_POLL_MS;
514
629
  for (;;) {
@@ -530,6 +645,7 @@ export async function runAttach(id, runtime = defaultAttachRuntime(socketPathFro
530
645
  const state = await connectAndAcquire();
531
646
  if (state.status === "exited") {
532
647
  exitCode = state.exit?.exitCode ?? 0;
648
+ appendCrashLog("attach", sessionExitCrashEntry(state, { sessionId: id, detail: "session exited during reconnect" }));
533
649
  terminalEnded = true;
534
650
  break;
535
651
  }
@@ -543,10 +659,12 @@ export async function runAttach(id, runtime = defaultAttachRuntime(socketPathFro
543
659
  catch (error) {
544
660
  if (error instanceof NativeTerminalRemoteError && error.message.includes("already has a controller")) {
545
661
  stdout.write(`\r\n[h2a] ${error.message}; attach detached\r\n`);
662
+ appendCrashLog("attach", { event: "refusal", sessionId: id, reason: "already-has-controller", detail: error.message });
546
663
  terminalEnded = true;
547
664
  break;
548
665
  }
549
666
  if (isDefinitiveNativeTerminalAbsence(error)) {
667
+ appendCrashLog("attach", sessionExitCrashEntry({ exit: null, stopSignal: null }, { sessionId: id, reason: "definitive-absence", detail: "session proven absent during reconnect; exit code unknown" }));
550
668
  exitCode = 1;
551
669
  terminalEnded = true;
552
670
  break;
@@ -596,6 +714,7 @@ export async function runAttach(id, runtime = defaultAttachRuntime(socketPathFro
596
714
  break;
597
715
  if (state.status === "exited") {
598
716
  exitCode = state.exit?.exitCode ?? 0;
717
+ appendCrashLog("attach", sessionExitCrashEntry(state, { sessionId: id, detail: "session exited during attach" }));
599
718
  break;
600
719
  }
601
720
  }
@@ -605,6 +724,7 @@ export async function runAttach(id, runtime = defaultAttachRuntime(socketPathFro
605
724
  break;
606
725
  if (isDefinitiveNativeTerminalAbsence(error)) {
607
726
  stdout.write("\r\n[h2a] native session is no longer available; attach stopped\r\n");
727
+ appendCrashLog("attach", sessionExitCrashEntry({ exit: null, stopSignal: null }, { sessionId: id, reason: "definitive-absence", detail: "session proven absent during attach; exit code unknown" }));
608
728
  exitCode = 1;
609
729
  break;
610
730
  }
@@ -639,7 +759,13 @@ export async function runAttach(id, runtime = defaultAttachRuntime(socketPathFro
639
759
  }
640
760
  return exitCode;
641
761
  }
642
- export async function runNativeTerminalOp(argv) {
762
+ export async function runNativeTerminalOp(argv, emit = emitResult, signal, deadline = Infinity) {
763
+ const checkDeadline = () => {
764
+ signal?.throwIfAborted();
765
+ if (Date.now() >= deadline)
766
+ throw new Error("native host operation deadline exceeded");
767
+ };
768
+ checkDeadline();
643
769
  const parsed = parseArgv(argv);
644
770
  const socketPath = parsed.flags.get("socket") ?? socketPathFromEnv();
645
771
  switch (parsed.op) {
@@ -684,15 +810,7 @@ export async function runNativeTerminalOp(argv) {
684
810
  // - unknown: an incomplete inventory without a found owner; even
685
811
  // transport errors without an independent owner-death proof.
686
812
  const id = required(parsed, "id");
687
- const owner = resolveNativeOwner(id, await inventoryFor(parsed));
688
- if (owner.state === "found")
689
- emit({ verdict: owner.session.status === "running" ? "live" : "dead",
690
- state: { ...owner.session, socketPath: owner.socketPath } });
691
- else if (owner.state === "absent")
692
- emit({ verdict: "dead" });
693
- else
694
- emit({ verdict: "unknown", code: owner.state,
695
- ...(owner.state === "unknown" ? { reason: owner.reason } : { sockets: owner.sockets, reason: "ambiguous-owner" }) });
813
+ emit(await probeNativeSession(id, parsed.flags.get("socket")));
696
814
  return 0;
697
815
  }
698
816
  case "admit": {
@@ -744,6 +862,88 @@ export async function runNativeTerminalOp(argv) {
744
862
  emit(state);
745
863
  return 0;
746
864
  }
865
+ case "launch": {
866
+ // R3 P1 single-fork fast path: ensure-host + admit + probe + create
867
+ // share ONE client and ONE inventory in ONE Node process. Admission
868
+ // and identity proofs stay under the host lock: an incomplete
869
+ // inventory still refuses, and a fenced create re-checks admission
870
+ // atomically like "create" does. Emits the host identity together
871
+ // with the session state so callers skip their pid re-probes.
872
+ const fenced = parsed.flags.has("incarnation") || parsed.flags.has("fenced");
873
+ const selected = parsed.flags.has("socket")
874
+ ? { client: await connectExisting(socketPath), socketPath }
875
+ : await selectLaunchClient(fenced);
876
+ const client = selected.client;
877
+ const launchSocket = selected.socketPath;
878
+ try {
879
+ checkDeadline();
880
+ const ping = await client.ping();
881
+ if (fenced && ping.launchFence !== true) {
882
+ emit({ code: "native-host-capability-mismatch", socketPath: launchSocket,
883
+ hostPid: ping.hostPid, generation: ping.generation, launchFence: false });
884
+ return 0;
885
+ }
886
+ const inventory = await inventoryFor({ ...parsed, flags: new Map() });
887
+ const id = required(parsed, "id");
888
+ const ids = [id, ...(parsed.flags.has("sidecar") ? [required(parsed, "sidecar")]
889
+ : !id.endsWith(".h2a") ? [`${id}.h2a`] : [])];
890
+ const collision = inventory.sessions.find(session => ids.includes(session.id) &&
891
+ (session.status !== "exited" || session.socketPath !== launchSocket));
892
+ if (collision) {
893
+ emit({ code: "native-name-collision", id: collision.id, socketPath: collision.socketPath,
894
+ hostPid: ping.hostPid, generation: ping.generation, launchFence: ping.launchFence === true });
895
+ return 0;
896
+ }
897
+ if (!inventory.complete) {
898
+ emit({ code: "native-inventory-unknown",
899
+ hosts: inventory.hosts.map(({ socketPath: p, reason }) => ({ socketPath: p, reason })),
900
+ hostPid: ping.hostPid, generation: ping.generation, launchFence: ping.launchFence === true });
901
+ return 0;
902
+ }
903
+ const existing = inventory.sessions.find(session => session.id === id && session.status !== "exited" &&
904
+ session.socketPath === launchSocket);
905
+ if (existing && parsed.flags.get("refuse-existing") !== "true") {
906
+ const { socketPath: _droppedSocket, ...existingRest } = existing;
907
+ emit({ ...existingRest, hostPid: ping.hostPid, socketPath: launchSocket, generation: ping.generation,
908
+ launchFence: ping.launchFence === true, adopted: true });
909
+ return 0;
910
+ }
911
+ if (existing && parsed.flags.get("refuse-existing") === "true") {
912
+ emit({ code: "native-name-collision", id: existing.id, socketPath: launchSocket,
913
+ hostPid: ping.hostPid, generation: ping.generation, launchFence: ping.launchFence === true });
914
+ return 0;
915
+ }
916
+ const cwd = required(parsed, "cwd");
917
+ const cols = Number(parsed.flags.get("cols") ?? 160);
918
+ const rows = Number(parsed.flags.get("rows") ?? 48);
919
+ const envFile = parsed.flags.get("env-file");
920
+ const env = envFile !== undefined
921
+ ? JSON.parse(readFileSync(envFile, "utf8"))
922
+ : Object.fromEntries(Object.entries(process.env).filter((entry) => entry[1] !== undefined));
923
+ if (parsed.command.length === 0)
924
+ throw new Error("launch requires -- command");
925
+ checkDeadline();
926
+ const state = await client.create({
927
+ ...(parsed.flags.has("incarnation") ? { launchFence: {
928
+ generation: required(parsed, "generation"), incarnation: required(parsed, "incarnation"),
929
+ } } : {}),
930
+ id,
931
+ command: parsed.command[0],
932
+ args: parsed.command.slice(1),
933
+ cwd,
934
+ env,
935
+ cols,
936
+ rows,
937
+ });
938
+ const { socketPath: _droppedStateSocket, generation: _droppedStateGeneration, ...stateRest } = state;
939
+ emit({ ...stateRest, hostPid: ping.hostPid, socketPath: launchSocket, generation: ping.generation,
940
+ launchFence: ping.launchFence === true });
941
+ return 0;
942
+ }
943
+ finally {
944
+ client.close();
945
+ }
946
+ }
747
947
  case "drive": {
748
948
  const target = parsed.flags.has("id")
749
949
  ? { state: "found", id: required(parsed, "id") } : nativeDriveSessionId(required(parsed, "target"));
@@ -853,10 +1053,14 @@ export async function runNativeTerminalOp(argv) {
853
1053
  const fenced = parsed.op.endsWith("-if-incarnation");
854
1054
  const generation = fenced ? required(parsed, "generation") : before.generation;
855
1055
  const incarnation = fenced ? required(parsed, "incarnation") : before.incarnation;
856
- if (before.generation !== generation)
1056
+ if (before.generation !== generation) {
1057
+ appendCrashLog("cli", { event: "refusal", sessionId: id, instanceId: generation, reason: "stale-terminal-host-generation" });
857
1058
  throw new Error("stale terminal host generation");
858
- if (before.incarnation !== incarnation)
1059
+ }
1060
+ if (before.incarnation !== incarnation) {
1061
+ appendCrashLog("cli", { event: "refusal", sessionId: id, instanceId: generation, reason: "stale-terminal-session-incarnation" });
859
1062
  throw new Error("stale terminal session incarnation");
1063
+ }
860
1064
  if (before.status !== "exited") {
861
1065
  await client.stopIfIncarnation(id, generation, incarnation, signal);
862
1066
  const deadline = Date.now() + 4_000;
@@ -878,17 +1082,28 @@ export async function runNativeTerminalOp(argv) {
878
1082
  const after = await client.state(id);
879
1083
  client.close();
880
1084
  if (after.generation !== generation || after.incarnation !== incarnation) {
1085
+ appendCrashLog("cli", { event: "refusal", sessionId: id, instanceId: generation, reason: "incarnation-changed-while-stopping" });
881
1086
  throw new Error("native session changed incarnation while stopping; replacement was not stopped");
882
1087
  }
883
1088
  if (after.status !== "exited") {
1089
+ appendCrashLog("cli", { event: "exit", sessionId: id, instanceId: generation, reason: "kill-escalation-failed", detail: `status=${after.status}` });
884
1090
  throw new Error(`native session ${id} pid=${after.pid} did not exit after ${signal} and SIGKILL (status=${after.status})`);
885
1091
  }
1092
+ appendCrashLog("cli", { event: "sessionExit", sessionId: id, instanceId: generation, reason: `killed-${signal}`, signal });
886
1093
  emit(after);
887
1094
  return 0;
888
1095
  }
889
1096
  case "attach": {
890
1097
  const id = required(parsed, "id");
891
- const owner = await owningClient(parsed, id);
1098
+ let owner;
1099
+ try {
1100
+ owner = await owningClient(parsed, id);
1101
+ }
1102
+ catch (error) {
1103
+ const refusal = error instanceof NativeOwnerError ? error.refusal : undefined;
1104
+ appendCrashLog("attach", { event: "refusal", sessionId: id, reason: refusal?.code ?? "owner-unproven", detail: refusal?.reason });
1105
+ throw error;
1106
+ }
892
1107
  owner.client.close();
893
1108
  // Reconnects retain this owner for the entire interactive operation.
894
1109
  return runAttach(id, defaultAttachRuntime(owner.socketPath), { takeover: parsed.flags.get("takeover") === "true" });
@@ -926,12 +1141,13 @@ function isEntryPoint() {
926
1141
  }
927
1142
  }
928
1143
  if (isEntryPoint()) {
1144
+ installCrashHandlers("cli");
929
1145
  runNativeTerminalOp(process.argv)
930
1146
  .then((code) => {
931
1147
  process.exitCode = code;
932
1148
  }, (error) => {
933
1149
  if (error instanceof NativeOwnerError)
934
- emit(error.refusal);
1150
+ emitResult(error.refusal);
935
1151
  process.stderr.write(`[h2a-native-op] ${error instanceof Error ? error.message : String(error)}\n`);
936
1152
  process.exitCode = 1;
937
1153
  })