@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.
- package/dist/agent-launch-args.d.ts.map +1 -1
- package/dist/agent-launch-args.js +5 -5
- package/dist/agent-launch-args.js.map +1 -1
- package/dist/agy-claude.d.ts +42 -0
- package/dist/agy-claude.d.ts.map +1 -0
- package/dist/agy-claude.js +148 -0
- package/dist/agy-claude.js.map +1 -0
- package/dist/agy-headless.d.ts +11 -0
- package/dist/agy-headless.d.ts.map +1 -0
- package/dist/agy-headless.js +104 -0
- package/dist/agy-headless.js.map +1 -0
- package/dist/attach.d.ts +5 -0
- package/dist/attach.d.ts.map +1 -1
- package/dist/attach.js +75 -6
- package/dist/attach.js.map +1 -1
- package/dist/auth-login.d.ts +24 -1
- package/dist/auth-login.d.ts.map +1 -1
- package/dist/auth-login.js +132 -4
- package/dist/auth-login.js.map +1 -1
- package/dist/central-mcp.d.ts +34 -0
- package/dist/central-mcp.d.ts.map +1 -1
- package/dist/central-mcp.js +275 -71
- package/dist/central-mcp.js.map +1 -1
- package/dist/claude-code-cli-isolation.d.ts +53 -0
- package/dist/claude-code-cli-isolation.d.ts.map +1 -0
- package/dist/claude-code-cli-isolation.js +277 -0
- package/dist/claude-code-cli-isolation.js.map +1 -0
- package/dist/claude-code-cli-lifecycle.d.ts +80 -0
- package/dist/claude-code-cli-lifecycle.d.ts.map +1 -0
- package/dist/claude-code-cli-lifecycle.js +402 -0
- package/dist/claude-code-cli-lifecycle.js.map +1 -0
- package/dist/claude-code-cli-runner.d.ts +89 -0
- package/dist/claude-code-cli-runner.d.ts.map +1 -0
- package/dist/claude-code-cli-runner.js +659 -0
- package/dist/claude-code-cli-runner.js.map +1 -0
- package/dist/claude-seat-input.d.ts +47 -0
- package/dist/claude-seat-input.d.ts.map +1 -0
- package/dist/claude-seat-input.js +272 -0
- package/dist/claude-seat-input.js.map +1 -0
- package/dist/claude-seat-mesh-contract.d.ts +88 -0
- package/dist/claude-seat-mesh-contract.d.ts.map +1 -0
- package/dist/claude-seat-mesh-contract.js +13 -0
- package/dist/claude-seat-mesh-contract.js.map +1 -0
- package/dist/cli-help-groups.js +1 -1
- package/dist/cli-help-groups.js.map +1 -1
- package/dist/cli-seat-codecs.d.ts +86 -0
- package/dist/cli-seat-codecs.d.ts.map +1 -0
- package/dist/cli-seat-codecs.js +255 -0
- package/dist/cli-seat-codecs.js.map +1 -0
- package/dist/cli-seat-fixtures.d.ts +86 -0
- package/dist/cli-seat-fixtures.d.ts.map +1 -0
- package/dist/cli-seat-fixtures.js +114 -0
- package/dist/cli-seat-fixtures.js.map +1 -0
- package/dist/cli-seat-materialization.d.ts +245 -0
- package/dist/cli-seat-materialization.d.ts.map +1 -0
- package/dist/cli-seat-materialization.js +646 -0
- package/dist/cli-seat-materialization.js.map +1 -0
- package/dist/conv-guard.d.ts.map +1 -1
- package/dist/conv-guard.js +6 -2
- package/dist/conv-guard.js.map +1 -1
- package/dist/crash-log.d.ts +112 -0
- package/dist/crash-log.d.ts.map +1 -0
- package/dist/crash-log.js +560 -0
- package/dist/crash-log.js.map +1 -0
- package/dist/delegate.d.ts +10 -0
- package/dist/delegate.d.ts.map +1 -1
- package/dist/delegate.js +29 -0
- package/dist/delegate.js.map +1 -1
- package/dist/file-lock.d.ts +4 -2
- package/dist/file-lock.d.ts.map +1 -1
- package/dist/file-lock.js +24 -10
- package/dist/file-lock.js.map +1 -1
- package/dist/gateway-host/claude-seat-client.d.ts +32 -0
- package/dist/gateway-host/claude-seat-client.d.ts.map +1 -0
- package/dist/gateway-host/claude-seat-client.js +38 -0
- package/dist/gateway-host/claude-seat-client.js.map +1 -0
- package/dist/gateway-host/config-file.d.ts +2 -0
- package/dist/gateway-host/config-file.d.ts.map +1 -1
- package/dist/gateway-host/config-file.js +5 -1
- package/dist/gateway-host/config-file.js.map +1 -1
- package/dist/gateway-host/daemon.d.ts.map +1 -1
- package/dist/gateway-host/daemon.js +1 -2
- package/dist/gateway-host/daemon.js.map +1 -1
- package/dist/gateway-host/host.d.ts +6 -0
- package/dist/gateway-host/host.d.ts.map +1 -1
- package/dist/gateway-host/host.js +24 -3
- package/dist/gateway-host/host.js.map +1 -1
- package/dist/gateway-host/live-auth.d.ts +62 -0
- package/dist/gateway-host/live-auth.d.ts.map +1 -0
- package/dist/gateway-host/live-auth.js +87 -0
- package/dist/gateway-host/live-auth.js.map +1 -0
- package/dist/gateway-host/live-declarations.d.ts +58 -0
- package/dist/gateway-host/live-declarations.d.ts.map +1 -0
- package/dist/gateway-host/live-declarations.js +429 -0
- package/dist/gateway-host/live-declarations.js.map +1 -0
- package/dist/gateway-host/live-disclosure.d.ts +124 -0
- package/dist/gateway-host/live-disclosure.d.ts.map +1 -0
- package/dist/gateway-host/live-disclosure.js +347 -0
- package/dist/gateway-host/live-disclosure.js.map +1 -0
- package/dist/gateway-host/live-github.d.ts +11 -0
- package/dist/gateway-host/live-github.d.ts.map +1 -0
- package/dist/gateway-host/live-github.js +374 -0
- package/dist/gateway-host/live-github.js.map +1 -0
- package/dist/gateway-host/live-host.d.ts +20 -0
- package/dist/gateway-host/live-host.d.ts.map +1 -0
- package/dist/gateway-host/live-host.js +14 -0
- package/dist/gateway-host/live-host.js.map +1 -0
- package/dist/gateway-host/live-object-store.d.ts +35 -0
- package/dist/gateway-host/live-object-store.d.ts.map +1 -0
- package/dist/gateway-host/live-object-store.js +447 -0
- package/dist/gateway-host/live-object-store.js.map +1 -0
- package/dist/gateway-host/live-registration.d.ts +162 -0
- package/dist/gateway-host/live-registration.d.ts.map +1 -0
- package/dist/gateway-host/live-registration.js +455 -0
- package/dist/gateway-host/live-registration.js.map +1 -0
- package/dist/gateway-model-catalog/model-catalog.d.ts +38 -0
- package/dist/gateway-model-catalog/model-catalog.d.ts.map +1 -0
- package/dist/gateway-model-catalog/model-catalog.js +167 -0
- package/dist/gateway-model-catalog/model-catalog.js.map +1 -0
- package/dist/identity-cull/cull.js +1 -1
- package/dist/identity-cull/cull.js.map +1 -1
- package/dist/identity-cull/identity-fence.d.ts +48 -0
- package/dist/identity-cull/identity-fence.d.ts.map +1 -0
- package/dist/identity-cull/identity-fence.js +221 -0
- package/dist/identity-cull/identity-fence.js.map +1 -0
- package/dist/identity-operator-break.d.ts +43 -0
- package/dist/identity-operator-break.d.ts.map +1 -0
- package/dist/identity-operator-break.js +85 -0
- package/dist/identity-operator-break.js.map +1 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +525 -132
- package/dist/index.js.map +1 -1
- package/dist/launch-layout.d.ts.map +1 -1
- package/dist/launch-layout.js +10 -2
- package/dist/launch-layout.js.map +1 -1
- package/dist/llm-mesh-accounts.d.ts +128 -6
- package/dist/llm-mesh-accounts.d.ts.map +1 -1
- package/dist/llm-mesh-accounts.js +370 -14
- package/dist/llm-mesh-accounts.js.map +1 -1
- package/dist/native-host.d.ts +25 -1
- package/dist/native-host.d.ts.map +1 -1
- package/dist/native-host.js +211 -28
- package/dist/native-host.js.map +1 -1
- package/dist/native-restart.d.ts +2 -0
- package/dist/native-restart.d.ts.map +1 -1
- package/dist/native-restart.js +7 -0
- package/dist/native-restart.js.map +1 -1
- package/dist/native-terminal/fleet.d.ts +11 -0
- package/dist/native-terminal/fleet.d.ts.map +1 -1
- package/dist/native-terminal/fleet.js +58 -1
- package/dist/native-terminal/fleet.js.map +1 -1
- package/dist/native-terminal/host-scope.d.ts +18 -0
- package/dist/native-terminal/host-scope.d.ts.map +1 -0
- package/dist/native-terminal/host-scope.js +65 -0
- package/dist/native-terminal/host-scope.js.map +1 -0
- package/dist/native-terminal/host.d.ts +7 -1
- package/dist/native-terminal/host.d.ts.map +1 -1
- package/dist/native-terminal/host.js +10 -26
- package/dist/native-terminal/host.js.map +1 -1
- package/dist/native-terminal/journal.d.ts +5 -0
- package/dist/native-terminal/journal.d.ts.map +1 -1
- package/dist/native-terminal/journal.js +28 -16
- package/dist/native-terminal/journal.js.map +1 -1
- package/dist/native-terminal/op.d.ts +49 -1
- package/dist/native-terminal/op.d.ts.map +1 -1
- package/dist/native-terminal/op.js +254 -38
- package/dist/native-terminal/op.js.map +1 -1
- package/dist/native-terminal/process.d.ts.map +1 -1
- package/dist/native-terminal/process.js +10 -1
- package/dist/native-terminal/process.js.map +1 -1
- package/dist/native-terminal/screen.d.ts.map +1 -1
- package/dist/native-terminal/screen.js +1 -1
- package/dist/native-terminal/screen.js.map +1 -1
- package/dist/native-terminal/server.d.ts +12 -2
- package/dist/native-terminal/server.d.ts.map +1 -1
- package/dist/native-terminal/server.js +143 -42
- package/dist/native-terminal/server.js.map +1 -1
- package/dist/native-terminal/session-scope.d.ts +2 -0
- package/dist/native-terminal/session-scope.d.ts.map +1 -0
- package/dist/native-terminal/session-scope.js +32 -0
- package/dist/native-terminal/session-scope.js.map +1 -0
- package/dist/proc-identity.d.ts +31 -0
- package/dist/proc-identity.d.ts.map +1 -1
- package/dist/proc-identity.js +54 -0
- package/dist/proc-identity.js.map +1 -1
- package/dist/prompt-delivery.d.ts +3 -0
- package/dist/prompt-delivery.d.ts.map +1 -1
- package/dist/prompt-delivery.js +8 -3
- package/dist/prompt-delivery.js.map +1 -1
- package/dist/protocol-local.d.ts +16 -0
- package/dist/protocol-local.d.ts.map +1 -1
- package/dist/protocol-local.js +14 -0
- package/dist/protocol-local.js.map +1 -1
- package/dist/registry.d.ts +16 -12
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +39 -5
- package/dist/registry.js.map +1 -1
- package/dist/relaunch.d.ts +16 -0
- package/dist/relaunch.d.ts.map +1 -1
- package/dist/relaunch.js +42 -1
- package/dist/relaunch.js.map +1 -1
- package/dist/restore.d.ts +4 -0
- package/dist/restore.d.ts.map +1 -1
- package/dist/restore.js +9 -37
- package/dist/restore.js.map +1 -1
- package/dist/resume-launch-options.d.ts +3 -3
- package/dist/resume-launch-options.d.ts.map +1 -1
- package/dist/resume-launch-options.js +7 -4
- package/dist/resume-launch-options.js.map +1 -1
- package/dist/run.d.ts +2 -0
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +88 -20
- package/dist/run.js.map +1 -1
- package/dist/soft-refresh.d.ts.map +1 -1
- package/dist/soft-refresh.js +6 -0
- package/dist/soft-refresh.js.map +1 -1
- package/dist/terminal-reset.d.ts +104 -0
- package/dist/terminal-reset.d.ts.map +1 -0
- package/dist/terminal-reset.js +229 -0
- package/dist/terminal-reset.js.map +1 -0
- package/dist/tmux.d.ts +54 -8
- package/dist/tmux.d.ts.map +1 -1
- package/dist/tmux.js +143 -36
- package/dist/tmux.js.map +1 -1
- package/dist/vendor/engram-live/contracts.d.ts +825 -0
- package/dist/vendor/engram-live/contracts.js +1734 -0
- package/dist/vendor/engram-live/integration.d.ts +113 -0
- package/dist/vendor/engram-live/integration.js +254 -0
- package/dist/vendor/engram-live/pr-sync.d.ts +144 -0
- package/dist/vendor/engram-live/pr-sync.js +411 -0
- package/dist/vendor/engram-live/registration.d.ts +138 -0
- package/dist/vendor/engram-live/registration.js +358 -0
- package/dist/vendor/engram-live/retention.d.ts +56 -0
- package/dist/vendor/engram-live/retention.js +179 -0
- 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
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
510
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
})
|