@sentropic/h2a-runtime 0.96.1 → 0.96.2
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 +9 -2
- package/dist/agent-launch-args.d.ts.map +1 -1
- package/dist/agent-launch-args.js +48 -2
- package/dist/agent-launch-args.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +468 -390
- package/dist/index.js.map +1 -1
- package/dist/relaunch.d.ts +23 -0
- package/dist/relaunch.d.ts.map +1 -1
- package/dist/relaunch.js +123 -0
- package/dist/relaunch.js.map +1 -1
- package/dist/restore.d.ts +3 -2
- package/dist/restore.d.ts.map +1 -1
- package/dist/restore.js +26 -5
- package/dist/restore.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -23,9 +23,9 @@ import { attachNativeSession, driveNativeInstruction, killNativeSessionIfIncarna
|
|
|
23
23
|
import { executeNativeRestart, NativeRestartError, } from "./native-restart.js";
|
|
24
24
|
import { migrateTmuxNames } from "./tmux-name-migration.js";
|
|
25
25
|
import { projectStatusForH2a } from "./status-projection.js";
|
|
26
|
-
import { buildAgentLaunchArgs, isAgentLaunchEffort, isAgentLaunchProfile, } from "./agent-launch-args.js";
|
|
26
|
+
import { buildAgentLaunchArgs, buildAgentLaunchStdin, isAgentLaunchEffort, isAgentLaunchProfile, } from "./agent-launch-args.js";
|
|
27
27
|
import { deriveSessionClass } from "./session-class.js";
|
|
28
|
-
import { isRelaunchKillable, planRelaunch, relaunchContinuationPrompt, wakeRelaunchedSession, } from "./relaunch.js";
|
|
28
|
+
import { isRelaunchKillable, planRelaunch, relaunchContinuationPrompt, tryAcquireResumeLaunchClaim, wakeRelaunchedSession, } from "./relaunch.js";
|
|
29
29
|
import { isHumanFacingSession, legacySessionEvidence, readConversationCustomTitle, readLastLayout, reconcileRunConvIds, restore as restoreLayout, } from "./restore.js";
|
|
30
30
|
import { getLayoutConfig } from "./config.js";
|
|
31
31
|
import { isManagedLocalKind, advanceJob, coerceRegistryTool, enroll, enrollFromRun, isLive, listJobs, listLocalForLs, loadRegistry, loadRegistryWithDiagnostics, probeNativeSession, probeTmuxSession, registryEntriesForNativeTarget, resolveLocalTmuxSessionForName, resolveManagedHost, tryClaimSlot, unreadableRegistryRowsForTarget, withRegistryLock, resolveRegistryPath, } from "./registry.js";
|
|
@@ -68,6 +68,7 @@ import { acquireLease, createLineage, handoffLease, isIncarnationSuspended, list
|
|
|
68
68
|
import { checkReadiness } from "./readiness.js";
|
|
69
69
|
import { createInterface } from "node:readline";
|
|
70
70
|
const H2A_RUN_API_VERSION = "h2a.run/v1";
|
|
71
|
+
const STRUCTURED_LAUNCH_PHASE_PREFIX = "[h2a] h2a.run.phase/v1 ";
|
|
71
72
|
const H2A_RUNTIME_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
72
73
|
import { CLI_PROFILES, gatewayModeForProfile, profileUsesLlmMeshGateway, } from "./protocol-local.js";
|
|
73
74
|
import { enrollViaFacade, formatLlmMeshAccountError, formatLlmMeshAccountList, listAccountsViaFacade, readLlmMeshConfig, startGateway, stopGateway, readGatewayPid, llmMeshLogPath, replaceAnthropicGatewayEnvironment, removeAccountViaFacade, acquireLlmMeshSessionEnv, updateLlmMeshRoutingConfig, } from "./llm-mesh.js";
|
|
@@ -77,7 +78,7 @@ export const packageName = "@sentropic/h2a-runtime";
|
|
|
77
78
|
export const H2A_RUNTIME_CLI_API_VERSION = 1;
|
|
78
79
|
export { run } from "./run.js";
|
|
79
80
|
export { executeNativeRestart, nativeMcpRelaunchInstruction, NativeRestartError, } from "./native-restart.js";
|
|
80
|
-
export { AGENT_LAUNCH_EFFORTS, AGENT_LAUNCH_PROFILES, AGENT_LAUNCH_PROMPT_MAX_BYTES, assertAgentLaunchModel, assertAgentLaunchPrompt, buildAgentLaunchArgs, isAgentLaunchEffort, isAgentLaunchProfile, } from "./agent-launch-args.js";
|
|
81
|
+
export { AGENT_LAUNCH_EFFORTS, AGENT_LAUNCH_PROFILES, AGENT_LAUNCH_PROMPT_MAX_BYTES, assertAgentLaunchModel, assertAgentLaunchPrompt, buildAgentLaunchArgs, buildAgentLaunchStdin, isAgentLaunchEffort, isAgentLaunchProfile, } from "./agent-launch-args.js";
|
|
81
82
|
export { attach, createRemoteSession, getRemoteSession, listRemoteSessions, refreshRemoteSession, renameRemoteSession, stopRemoteSession, } from "./attach.js";
|
|
82
83
|
export { inspectProfileAuth } from "./auth-diagnostics.js";
|
|
83
84
|
export { AuthRefreshError, ensureProfileAuthFresh } from "./auth-refresh.js";
|
|
@@ -3883,7 +3884,7 @@ export async function main(argv) {
|
|
|
3883
3884
|
});
|
|
3884
3885
|
program
|
|
3885
3886
|
.command("run <profile> [path]")
|
|
3886
|
-
.description("Start a LOCAL session in tmux (claude/codex/…) in <path> (default: cwd), then attach this terminal by default. h2a applies its embedded scroll-safe tmux profile at launch; no ~/.tmux.conf is required. Manage it with `h2a ls`, `h2a attach <slug>`, and `h2a stop <slug>`. Detach with Ctrl-b d; the session keeps running.")
|
|
3887
|
+
.description("Start a LOCAL session in tmux (claude/codex/agy/…) in <path> (default: cwd), then attach this terminal by default. h2a applies its embedded scroll-safe tmux profile at launch; no ~/.tmux.conf is required. Manage it with `h2a ls`, `h2a attach <slug>`, and `h2a stop <slug>`. Detach with Ctrl-b d; the session keeps running.")
|
|
3887
3888
|
.option("--no-attach", "start detached and print the attach command instead (for scripts / fan-out orchestration)")
|
|
3888
3889
|
.option("--no-attached-terminal", "disable the persistent headless tmux client (legacy compatibility; cannot be used with --prompt-stdin)")
|
|
3889
3890
|
.option("--tmux", "host the session in tmux instead of the native PTY host (native is the default; H2A_SESSION_HOST=tmux flips the default fleet-wide)")
|
|
@@ -3893,10 +3894,11 @@ export async function main(argv) {
|
|
|
3893
3894
|
.option("--count <n>", "fan out N parallel agents (named <base>#1…#N) — run more than the per-project layout cap of claude/codex sessions")
|
|
3894
3895
|
.option("--h2a", 'also start the h2a MCP server in a side tmux window "h2a" (launcher contract: agent reachable/wakeable via ~/h2a-workspace/.h2a); config key `h2a: {enabled, command}` makes it the default')
|
|
3895
3896
|
.option("--no-h2a", "do not start the h2a MCP side window, overriding config")
|
|
3896
|
-
.option("--prompt-stdin", "read the initial Claude/Codex prompt from stdin (keeps it out of process argv)")
|
|
3897
|
-
.option("--
|
|
3898
|
-
.option("--
|
|
3899
|
-
.option("--
|
|
3897
|
+
.option("--prompt-stdin", "read the initial Claude/Codex/AGY prompt from stdin (keeps it out of process argv)")
|
|
3898
|
+
.option("--agent <agent>", "AGY agent override (for example: stp)")
|
|
3899
|
+
.option("--model <model>", "Claude/Codex/AGY model override")
|
|
3900
|
+
.option("--effort <level>", "reasoning effort: low|medium|high|xhigh (AGY supports low|medium|high)")
|
|
3901
|
+
.option("--headless", "run once and exit (Claude -p / Codex exec / AGY --print), recording output under .h2a/runs/<name>")
|
|
3900
3902
|
.option("--background", "classify this detached session as a background worker excluded from human restore")
|
|
3901
3903
|
.option("--json", "emit one machine-readable h2a.run.result object")
|
|
3902
3904
|
.option("--llm-gateway", "launch the CLI through the local llm-mesh gateway")
|
|
@@ -3905,13 +3907,14 @@ export async function main(argv) {
|
|
|
3905
3907
|
.option("--no-gw", "alias for --no-llm-gateway")
|
|
3906
3908
|
.action(async (profile, path, opts) => {
|
|
3907
3909
|
const structuredLaunch = opts.promptStdin === true ||
|
|
3910
|
+
opts.agent !== undefined ||
|
|
3908
3911
|
opts.model !== undefined ||
|
|
3909
3912
|
opts.effort !== undefined ||
|
|
3910
3913
|
opts.headless === true ||
|
|
3911
3914
|
opts.background === true ||
|
|
3912
3915
|
opts.json === true;
|
|
3913
3916
|
if (structuredLaunch && !isAgentLaunchProfile(profile)) {
|
|
3914
|
-
process.stderr.write(`[h2a] structured run supports only claude|codex (got "${profile}")\n`);
|
|
3917
|
+
process.stderr.write(`[h2a] structured run supports only claude|codex|agy (got "${profile}")\n`);
|
|
3915
3918
|
process.exitCode = 2;
|
|
3916
3919
|
return;
|
|
3917
3920
|
}
|
|
@@ -3920,6 +3923,16 @@ export async function main(argv) {
|
|
|
3920
3923
|
process.exitCode = 2;
|
|
3921
3924
|
return;
|
|
3922
3925
|
}
|
|
3926
|
+
if (profile === "agy" && opts.effort === "xhigh") {
|
|
3927
|
+
process.stderr.write("[h2a] AGY --effort must be low|medium|high\n");
|
|
3928
|
+
process.exitCode = 2;
|
|
3929
|
+
return;
|
|
3930
|
+
}
|
|
3931
|
+
if (opts.agent !== undefined && profile !== "agy") {
|
|
3932
|
+
process.stderr.write("[h2a] --agent is supported only for the AGY profile\n");
|
|
3933
|
+
process.exitCode = 2;
|
|
3934
|
+
return;
|
|
3935
|
+
}
|
|
3923
3936
|
if (opts.json && !opts.background) {
|
|
3924
3937
|
process.stderr.write("[h2a] --json launch requires --background (MCP launches are detached)\n");
|
|
3925
3938
|
process.exitCode = 2;
|
|
@@ -3945,6 +3958,26 @@ export async function main(argv) {
|
|
|
3945
3958
|
process.exitCode = 2;
|
|
3946
3959
|
return;
|
|
3947
3960
|
}
|
|
3961
|
+
let gatewayMode;
|
|
3962
|
+
try {
|
|
3963
|
+
gatewayMode = gatewayModeFromOptions(opts);
|
|
3964
|
+
}
|
|
3965
|
+
catch (error) {
|
|
3966
|
+
process.stderr.write(`[h2a] ${error.message}.\n`);
|
|
3967
|
+
process.exitCode = 1;
|
|
3968
|
+
return;
|
|
3969
|
+
}
|
|
3970
|
+
if (gatewayMode === "gateway" && !profileUsesLlmMeshGateway(profile)) {
|
|
3971
|
+
process.stderr.write(`[h2a] --llm-gateway/--gw is unsupported for ${profile}; this gateway is Anthropic-compatible and only Claude profiles consume it.\n`);
|
|
3972
|
+
process.exitCode = 2;
|
|
3973
|
+
return;
|
|
3974
|
+
}
|
|
3975
|
+
if (opts.json && opts.name) {
|
|
3976
|
+
process.stderr.write(`${STRUCTURED_LAUNCH_PHASE_PREFIX}${JSON.stringify({
|
|
3977
|
+
launchId: opts.name,
|
|
3978
|
+
phase: "pre-creation",
|
|
3979
|
+
})}\n`);
|
|
3980
|
+
}
|
|
3948
3981
|
const sessionHost = resolveSessionHostKind(opts);
|
|
3949
3982
|
if (sessionHost === "local-tmux") {
|
|
3950
3983
|
if (!tmuxAvailable()) {
|
|
@@ -4018,436 +4051,481 @@ export async function main(argv) {
|
|
|
4018
4051
|
const labels = count > 1
|
|
4019
4052
|
? fanoutLabels(opts.name ?? basename(cwd), count)
|
|
4020
4053
|
: [opts.name];
|
|
4021
|
-
const
|
|
4022
|
-
?
|
|
4023
|
-
:
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
// probe as "no session" would let `run --tmux --resume` start a tmux
|
|
4027
|
-
// twin over a live-but-unprovable native writer (sol-F3): the same
|
|
4028
|
-
// fail-closed rule as resolveManagedHost's rule 4 applies — refuse
|
|
4029
|
-
// to launch on an unprovable host state.
|
|
4030
|
-
const nativeExistence = labels
|
|
4031
|
-
.map((label) => slugify(label ?? cwd))
|
|
4032
|
-
.map((slug) => ({
|
|
4033
|
-
slug,
|
|
4034
|
-
probe: probeNativeSession(localSessionName(slug)),
|
|
4035
|
-
}));
|
|
4036
|
-
const unknownNativeSlugs = nativeExistence
|
|
4037
|
-
.filter((probed) => probed.probe === "unknown")
|
|
4038
|
-
.map((probed) => probed.slug);
|
|
4039
|
-
if (unknownNativeSlugs.length > 0) {
|
|
4040
|
-
for (const slug of unknownNativeSlugs) {
|
|
4041
|
-
process.stderr.write(`[h2a] cannot start ${slug}: native host state is unknown (the probe failed) — a probe failure is never proof of absence; refusing to risk a second writer (fail closed).\n`);
|
|
4042
|
-
}
|
|
4043
|
-
process.exitCode = 1;
|
|
4044
|
-
return;
|
|
4045
|
-
}
|
|
4046
|
-
const existingNativeSlugs = nativeExistence
|
|
4047
|
-
.filter((probed) => probed.probe === "live")
|
|
4048
|
-
.map((probed) => probed.slug);
|
|
4049
|
-
const existingLocalSessions = [
|
|
4050
|
-
...new Set([...existingTmuxSlugs, ...existingNativeSlugs]),
|
|
4051
|
-
];
|
|
4052
|
-
if (existingLocalSessions.length > 0) {
|
|
4053
|
-
for (const slug of existingLocalSessions) {
|
|
4054
|
-
process.stderr.write(`[h2a] local session ${slug} already exists; no new ${profile} was started.\n`);
|
|
4055
|
-
process.stderr.write(`[h2a] attach: h2a attach ${slug}\n`);
|
|
4056
|
-
process.stderr.write(`[h2a] stop first: h2a stop ${slug} --reason restart\n`);
|
|
4057
|
-
}
|
|
4058
|
-
process.exitCode = 1;
|
|
4059
|
-
return;
|
|
4060
|
-
}
|
|
4061
|
-
if (!existsSync(cwd) || !statSync(cwd).isDirectory()) {
|
|
4062
|
-
process.stderr.write(`[h2a] workspace is not an existing directory: ${cwd}\n`);
|
|
4063
|
-
process.exitCode = 2;
|
|
4054
|
+
const resumeClaim = opts.resume
|
|
4055
|
+
? tryAcquireResumeLaunchClaim(opts.resume)
|
|
4056
|
+
: undefined;
|
|
4057
|
+
if (opts.resume && !resumeClaim) {
|
|
4058
|
+
process.stderr.write(`[h2a] conversation ${opts.resume} is already being resumed; duplicate launch request skipped.\n`);
|
|
4064
4059
|
return;
|
|
4065
4060
|
}
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4061
|
+
try {
|
|
4062
|
+
const reservedTmuxSlugs = tmuxAvailable()
|
|
4063
|
+
? existingLocalSessionSlugs(labels, cwd)
|
|
4064
|
+
: [];
|
|
4065
|
+
// The inventory and this decision are separate reads: a structured
|
|
4066
|
+
// launch can leave a name visible just before its last tmux process
|
|
4067
|
+
// exits. Re-probe every canonical/legacy spelling and reclaim the slug
|
|
4068
|
+
// only when ALL probes positively report death. A live or unknown
|
|
4069
|
+
// spelling remains reserved, so probe failure can never authorize a
|
|
4070
|
+
// second writer.
|
|
4071
|
+
const tmuxExistence = reservedTmuxSlugs.map((slug) => {
|
|
4072
|
+
const probes = managedSessionCandidates(slug).map((name) => probeTmuxSession(name));
|
|
4073
|
+
const probe = probes.includes("live")
|
|
4074
|
+
? "live"
|
|
4075
|
+
: probes.includes("unknown")
|
|
4076
|
+
? "unknown"
|
|
4077
|
+
: "dead";
|
|
4078
|
+
return { slug, probe };
|
|
4081
4079
|
});
|
|
4082
|
-
|
|
4080
|
+
const unknownTmuxSlugs = tmuxExistence
|
|
4081
|
+
.filter((probed) => probed.probe === "unknown")
|
|
4082
|
+
.map((probed) => probed.slug);
|
|
4083
|
+
if (unknownTmuxSlugs.length > 0) {
|
|
4084
|
+
for (const slug of unknownTmuxSlugs) {
|
|
4085
|
+
process.stderr.write(`[h2a] cannot start ${slug}: tmux host state is unknown (the probe failed) — refusing to reclaim an unproven session name.\n`);
|
|
4086
|
+
}
|
|
4083
4087
|
process.exitCode = 1;
|
|
4084
4088
|
return;
|
|
4085
4089
|
}
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
// so that hook cannot reclassify it as a human session.
|
|
4108
|
-
const sessionClass = deriveSessionClass({
|
|
4109
|
-
background: opts.background === true,
|
|
4110
|
-
headless: opts.headless === true,
|
|
4111
|
-
humanTerminal: opts.attachedTerminal !== false,
|
|
4112
|
-
});
|
|
4113
|
-
let activeGateway;
|
|
4114
|
-
const h2a = getH2aConfig();
|
|
4115
|
-
const h2aSidecar = opts.h2a ?? h2a.enabled;
|
|
4116
|
-
if (opts.headless && h2aSidecar) {
|
|
4117
|
-
process.stderr.write("[h2a] --headless requires --no-h2a (a sidecar would keep the run-once session alive)\n");
|
|
4118
|
-
process.exitCode = 2;
|
|
4119
|
-
return;
|
|
4120
|
-
}
|
|
4121
|
-
if (opts.headless && !opts.name) {
|
|
4122
|
-
process.stderr.write("[h2a] --headless requires --name\n");
|
|
4123
|
-
process.exitCode = 2;
|
|
4124
|
-
return;
|
|
4125
|
-
}
|
|
4126
|
-
const centralMcp = await prepareCentralMcpForLaunch({ root: cwd, profile, cwd });
|
|
4127
|
-
if (centralMcp?.status === "degraded") {
|
|
4128
|
-
process.stderr.write(`[h2a] central MCP unavailable, falling back to per-session sidecar: ${centralMcp.reason}\n`);
|
|
4129
|
-
}
|
|
4130
|
-
const started = [];
|
|
4131
|
-
for (const label of labels) {
|
|
4132
|
-
const clientSessionId = localSessionName(slugify(label ?? cwd));
|
|
4133
|
-
try {
|
|
4134
|
-
if (structuredLaunch) {
|
|
4135
|
-
activeGateway = await prepareStructuredGateway(launchGatewayMode, (selectedMode) => injectLlmMeshGatewayEnv(selectedMode, selectedMode !== "gateway", clientSessionId));
|
|
4136
|
-
}
|
|
4137
|
-
else {
|
|
4138
|
-
activeGateway = await injectLlmMeshGatewayEnv(launchGatewayMode, true, clientSessionId);
|
|
4090
|
+
const existingTmuxSlugs = tmuxExistence
|
|
4091
|
+
.filter((probed) => probed.probe === "live")
|
|
4092
|
+
.map((probed) => probed.slug);
|
|
4093
|
+
// Native existence is THREE-state (the resolver's probe vocabulary):
|
|
4094
|
+
// a probe FAILURE is "unknown", never absence. Treating a thrown
|
|
4095
|
+
// probe as "no session" would let `run --tmux --resume` start a tmux
|
|
4096
|
+
// twin over a live-but-unprovable native writer (sol-F3): the same
|
|
4097
|
+
// fail-closed rule as resolveManagedHost's rule 4 applies — refuse
|
|
4098
|
+
// to launch on an unprovable host state.
|
|
4099
|
+
const nativeExistence = labels
|
|
4100
|
+
.map((label) => slugify(label ?? cwd))
|
|
4101
|
+
.map((slug) => ({
|
|
4102
|
+
slug,
|
|
4103
|
+
probe: probeNativeSession(localSessionName(slug)),
|
|
4104
|
+
}));
|
|
4105
|
+
const unknownNativeSlugs = nativeExistence
|
|
4106
|
+
.filter((probed) => probed.probe === "unknown")
|
|
4107
|
+
.map((probed) => probed.slug);
|
|
4108
|
+
if (unknownNativeSlugs.length > 0) {
|
|
4109
|
+
for (const slug of unknownNativeSlugs) {
|
|
4110
|
+
process.stderr.write(`[h2a] cannot start ${slug}: native host state is unknown (the probe failed) — a probe failure is never proof of absence; refusing to risk a second writer (fail closed).\n`);
|
|
4139
4111
|
}
|
|
4140
|
-
}
|
|
4141
|
-
catch (error) {
|
|
4142
|
-
process.stderr.write(`[h2a] ${error.message}\n`);
|
|
4143
4112
|
process.exitCode = 1;
|
|
4144
4113
|
return;
|
|
4145
4114
|
}
|
|
4146
|
-
const
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
...(opts.resume !== undefined
|
|
4161
|
-
? { resumeId: opts.resume }
|
|
4162
|
-
: {}),
|
|
4163
|
-
...(opts.headless ? { headless: true } : {}),
|
|
4164
|
-
...(useBare ? { bare: true } : {}),
|
|
4165
|
-
})
|
|
4166
|
-
: opts.resume
|
|
4167
|
-
? localResumeArgs(profile, opts.resume, { bare: useBare })
|
|
4168
|
-
: localStartArgs(profile, { bare: useBare });
|
|
4115
|
+
const existingNativeSlugs = nativeExistence
|
|
4116
|
+
.filter((probed) => probed.probe === "live")
|
|
4117
|
+
.map((probed) => probed.slug);
|
|
4118
|
+
const existingLocalSessions = [
|
|
4119
|
+
...new Set([...existingTmuxSlugs, ...existingNativeSlugs]),
|
|
4120
|
+
];
|
|
4121
|
+
if (existingLocalSessions.length > 0) {
|
|
4122
|
+
for (const slug of existingLocalSessions) {
|
|
4123
|
+
process.stderr.write(`[h2a] local session ${slug} already exists; no new ${profile} was started.\n`);
|
|
4124
|
+
process.stderr.write(`[h2a] attach: h2a attach ${slug}\n`);
|
|
4125
|
+
process.stderr.write(`[h2a] stop first: h2a stop ${slug} --reason restart\n`);
|
|
4126
|
+
}
|
|
4127
|
+
process.exitCode = 1;
|
|
4128
|
+
return;
|
|
4169
4129
|
}
|
|
4170
|
-
|
|
4171
|
-
process.stderr.write(`[h2a] ${
|
|
4130
|
+
if (!existsSync(cwd) || !statSync(cwd).isDirectory()) {
|
|
4131
|
+
process.stderr.write(`[h2a] workspace is not an existing directory: ${cwd}\n`);
|
|
4172
4132
|
process.exitCode = 2;
|
|
4173
4133
|
return;
|
|
4174
4134
|
}
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
return;
|
|
4135
|
+
let initialPrompt;
|
|
4136
|
+
if (opts.promptStdin) {
|
|
4137
|
+
initialPrompt = readFileSync(0, "utf8");
|
|
4179
4138
|
}
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
if (
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
else {
|
|
4196
|
-
({ name, slug, agentPane, promptFile } = startHeadlessSession(profile, command, cwd, args, resultJson, outputLog, label, getTmuxProfileConfig().profile, initialPrompt, structuredLaunch, sessionClass));
|
|
4139
|
+
// Single-writer guard: refuse to resume a conversation another live
|
|
4140
|
+
// session (local registry / remote pod) is already writing.
|
|
4141
|
+
if (opts.resume) {
|
|
4142
|
+
const ok = await guardConvWriters({
|
|
4143
|
+
convId: opts.resume,
|
|
4144
|
+
cwd,
|
|
4145
|
+
...(opts.force ? { force: true } : {}),
|
|
4146
|
+
fetchRemoteSessions: async () => {
|
|
4147
|
+
const url = getConfiguredRemoteOptional();
|
|
4148
|
+
return url ? await listRemoteSessions(url) : [];
|
|
4149
|
+
},
|
|
4150
|
+
});
|
|
4151
|
+
if (!ok) {
|
|
4152
|
+
process.exitCode = 1;
|
|
4153
|
+
return;
|
|
4197
4154
|
}
|
|
4198
4155
|
}
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
...(structuredLaunch ? { refuseExisting: true } : {}),
|
|
4219
|
-
attachedTerminal: opts.attachedTerminal !== false,
|
|
4220
|
-
sessionClass,
|
|
4221
|
-
}));
|
|
4156
|
+
// By default launches are direct. --llm-gateway/--gw opts into the
|
|
4157
|
+
// local gateway for any profile that consumes Anthropic-compatible env.
|
|
4158
|
+
const launchGatewayMode = gatewayModeForProfile(profile, gatewayMode);
|
|
4159
|
+
const command = localCliCommand(profile);
|
|
4160
|
+
// A detached/background or run-once launch is a worker, even when its
|
|
4161
|
+
// agent later emits a Claude SessionStart hook. Stamp this through tmux
|
|
4162
|
+
// so that hook cannot reclassify it as a human session.
|
|
4163
|
+
const sessionClass = deriveSessionClass({
|
|
4164
|
+
background: opts.background === true,
|
|
4165
|
+
headless: opts.headless === true,
|
|
4166
|
+
humanTerminal: opts.attachedTerminal !== false,
|
|
4167
|
+
});
|
|
4168
|
+
let activeGateway;
|
|
4169
|
+
const h2a = getH2aConfig();
|
|
4170
|
+
const h2aSidecar = opts.h2a ?? h2a.enabled;
|
|
4171
|
+
if (opts.headless && h2aSidecar) {
|
|
4172
|
+
process.stderr.write("[h2a] --headless requires --no-h2a (a sidecar would keep the run-once session alive)\n");
|
|
4173
|
+
process.exitCode = 2;
|
|
4174
|
+
return;
|
|
4222
4175
|
}
|
|
4223
|
-
if (
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
// `name` is the session THIS command just created above, on the
|
|
4227
|
-
// host it chose. Re-resolving could return missing/unknown and
|
|
4228
|
-
// refuse — leaking the partial session it exists to remove.
|
|
4229
|
-
killLocalSession(name);
|
|
4230
|
-
process.stderr.write(`[h2a] could not capture the exact agent pane for ${slug}; the partial session was stopped\n`);
|
|
4231
|
-
process.exitCode = 1;
|
|
4176
|
+
if (opts.headless && !opts.name) {
|
|
4177
|
+
process.stderr.write("[h2a] --headless requires --name\n");
|
|
4178
|
+
process.exitCode = 2;
|
|
4232
4179
|
return;
|
|
4233
4180
|
}
|
|
4234
|
-
|
|
4235
|
-
if (
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4181
|
+
const centralMcp = await prepareCentralMcpForLaunch({ root: cwd, profile, cwd });
|
|
4182
|
+
if (centralMcp?.status === "degraded") {
|
|
4183
|
+
process.stderr.write(`[h2a] central MCP unavailable, falling back to per-session sidecar: ${centralMcp.reason}\n`);
|
|
4184
|
+
}
|
|
4185
|
+
const started = [];
|
|
4186
|
+
for (const label of labels) {
|
|
4187
|
+
const clientSessionId = localSessionName(slugify(label ?? cwd));
|
|
4188
|
+
try {
|
|
4189
|
+
if (structuredLaunch) {
|
|
4190
|
+
activeGateway = await prepareStructuredGateway(launchGatewayMode, (selectedMode) => injectLlmMeshGatewayEnv(selectedMode, selectedMode !== "gateway", clientSessionId));
|
|
4191
|
+
}
|
|
4192
|
+
else {
|
|
4193
|
+
activeGateway = await injectLlmMeshGatewayEnv(launchGatewayMode, true, clientSessionId);
|
|
4194
|
+
}
|
|
4195
|
+
}
|
|
4196
|
+
catch (error) {
|
|
4197
|
+
process.stderr.write(`[h2a] ${error.message}\n`);
|
|
4246
4198
|
process.exitCode = 1;
|
|
4247
4199
|
return;
|
|
4248
4200
|
}
|
|
4249
|
-
|
|
4250
|
-
|
|
4201
|
+
const useBare = shouldUseClaudeBare(profile);
|
|
4202
|
+
let args;
|
|
4203
|
+
try {
|
|
4204
|
+
args =
|
|
4205
|
+
structuredLaunch && isAgentLaunchProfile(profile)
|
|
4206
|
+
? buildAgentLaunchArgs({
|
|
4207
|
+
profile,
|
|
4208
|
+
...(initialPrompt !== undefined
|
|
4209
|
+
? { prompt: initialPrompt }
|
|
4210
|
+
: {}),
|
|
4211
|
+
...(opts.agent !== undefined ? { agent: opts.agent } : {}),
|
|
4212
|
+
...(opts.model !== undefined ? { model: opts.model } : {}),
|
|
4213
|
+
...(opts.effort !== undefined
|
|
4214
|
+
? { effort: opts.effort }
|
|
4215
|
+
: {}),
|
|
4216
|
+
...(opts.resume !== undefined
|
|
4217
|
+
? { resumeId: opts.resume }
|
|
4218
|
+
: {}),
|
|
4219
|
+
...(opts.headless ? { headless: true } : {}),
|
|
4220
|
+
...(useBare ? { bare: true } : {}),
|
|
4221
|
+
})
|
|
4222
|
+
: opts.resume
|
|
4223
|
+
? localResumeArgs(profile, opts.resume, { bare: useBare })
|
|
4224
|
+
: localStartArgs(profile, { bare: useBare });
|
|
4251
4225
|
}
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4226
|
+
catch (error) {
|
|
4227
|
+
process.stderr.write(`[h2a] ${error.message}\n`);
|
|
4228
|
+
process.exitCode = 2;
|
|
4229
|
+
return;
|
|
4230
|
+
}
|
|
4231
|
+
if (opts.resume && args.length === 0) {
|
|
4232
|
+
process.stderr.write(`[h2a] profile "${profile}" has no verified local resume argv; start it without -r/--resume\n`);
|
|
4233
|
+
process.exitCode = 1;
|
|
4234
|
+
return;
|
|
4235
|
+
}
|
|
4236
|
+
let name;
|
|
4237
|
+
let slug;
|
|
4238
|
+
let outputLog;
|
|
4239
|
+
let resultJson;
|
|
4240
|
+
let agentPane;
|
|
4241
|
+
let promptFile;
|
|
4242
|
+
let promptDelivery;
|
|
4243
|
+
if (opts.json && opts.name) {
|
|
4244
|
+
process.stderr.write(`${STRUCTURED_LAUNCH_PHASE_PREFIX}${JSON.stringify({
|
|
4245
|
+
launchId: opts.name,
|
|
4246
|
+
phase: "creation-attempted",
|
|
4247
|
+
})}\n`);
|
|
4248
|
+
}
|
|
4249
|
+
if (opts.headless) {
|
|
4250
|
+
const runDir = join(cwd, ".h2a", "runs", label);
|
|
4251
|
+
mkdirSync(runDir, { recursive: true });
|
|
4252
|
+
outputLog = join(runDir, "output.log");
|
|
4253
|
+
resultJson = join(runDir, "result.json");
|
|
4254
|
+
const headlessPromptInput = structuredLaunch && isAgentLaunchProfile(profile)
|
|
4255
|
+
? buildAgentLaunchStdin({
|
|
4256
|
+
profile,
|
|
4257
|
+
...(initialPrompt !== undefined
|
|
4258
|
+
? { prompt: initialPrompt }
|
|
4259
|
+
: {}),
|
|
4260
|
+
headless: true,
|
|
4261
|
+
})
|
|
4262
|
+
: initialPrompt;
|
|
4263
|
+
if (sessionHost === "native") {
|
|
4264
|
+
({ name, slug, promptFile } = startNativeHeadlessSession(profile, command, cwd, args, resultJson, outputLog, label, headlessPromptInput, structuredLaunch, sessionClass));
|
|
4257
4265
|
}
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
h2aSidecarStarted = false;
|
|
4266
|
+
else {
|
|
4267
|
+
({ name, slug, agentPane, promptFile } = startHeadlessSession(profile, command, cwd, args, resultJson, outputLog, label, getTmuxProfileConfig().profile, headlessPromptInput, structuredLaunch, sessionClass));
|
|
4261
4268
|
}
|
|
4262
4269
|
}
|
|
4270
|
+
else if (sessionHost === "native") {
|
|
4271
|
+
({ name, slug } = startNativeSession(profile, command, cwd, args, label, {
|
|
4272
|
+
...(opts.resume !== undefined ? { resumeId: opts.resume } : {}),
|
|
4273
|
+
...(initialPrompt !== undefined
|
|
4274
|
+
? { terminateOnAgentExit: true }
|
|
4275
|
+
: {}),
|
|
4276
|
+
...(structuredLaunch ? { refuseExisting: true } : {}),
|
|
4277
|
+
sessionClass,
|
|
4278
|
+
}));
|
|
4279
|
+
}
|
|
4263
4280
|
else {
|
|
4264
|
-
|
|
4281
|
+
({ name, slug, agentPane } = startLocalSession(profile, command, cwd, args, label, getTmuxProfileConfig().profile, {
|
|
4282
|
+
...(opts.resume !== undefined
|
|
4283
|
+
? { resumeId: opts.resume }
|
|
4284
|
+
: {}),
|
|
4285
|
+
...(h2aSidecar ? { h2aCommand: h2a.command } : {}),
|
|
4286
|
+
...(initialPrompt !== undefined
|
|
4287
|
+
? { terminateOnAgentExit: true }
|
|
4288
|
+
: {}),
|
|
4289
|
+
...(structuredLaunch ? { refuseExisting: true } : {}),
|
|
4290
|
+
attachedTerminal: opts.attachedTerminal !== false,
|
|
4291
|
+
sessionClass,
|
|
4292
|
+
}));
|
|
4265
4293
|
}
|
|
4266
|
-
if (
|
|
4294
|
+
if (structuredLaunch && sessionHost !== "native" && !agentPane) {
|
|
4267
4295
|
cleanupHeadlessPromptFile(promptFile);
|
|
4268
4296
|
// OWN-CLEANUP, deliberately NOT resolver-derived (F1 sorting):
|
|
4269
|
-
//
|
|
4270
|
-
// it chose
|
|
4297
|
+
// `name` is the session THIS command just created above, on the
|
|
4298
|
+
// host it chose. Re-resolving could return missing/unknown and
|
|
4299
|
+
// refuse — leaking the partial session it exists to remove.
|
|
4271
4300
|
killLocalSession(name);
|
|
4272
|
-
process.stderr.write(`[h2a]
|
|
4301
|
+
process.stderr.write(`[h2a] could not capture the exact agent pane for ${slug}; the partial session was stopped\n`);
|
|
4273
4302
|
process.exitCode = 1;
|
|
4274
4303
|
return;
|
|
4275
4304
|
}
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
if (!opts.headless && initialPrompt !== undefined) {
|
|
4281
|
-
// NEVER paste-and-hope: `tmux new-session -d` returns before the
|
|
4282
|
-
// agent TUI draws anything, and a prompt sent into that window is
|
|
4283
|
-
// echoed by the raw terminal and then discarded. Delivery is
|
|
4284
|
-
// OBSERVED — brief seen in a real composer, then real work — because
|
|
4285
|
-
// reporting "started" over a lost brief is what left a lane inert
|
|
4286
|
-
// for 33 minutes on 2026-07-28.
|
|
4287
|
-
promptDelivery = sessionHost === "native"
|
|
4288
|
-
? deliverInitialPrompt(name, initialPrompt, nativePromptDeliveryDeps(sleepSync))
|
|
4289
|
-
: deliverInitialPrompt(agentPane, initialPrompt, {
|
|
4290
|
-
capturePane: capturePaneVisible,
|
|
4291
|
-
clearComposer: clearPaneComposer,
|
|
4292
|
-
pasteBlock: pasteLiteralBlock,
|
|
4293
|
-
submit: submitPane,
|
|
4294
|
-
cpuMs: paneTreeCpuMs,
|
|
4295
|
-
sleep: sleepSync,
|
|
4296
|
-
now: () => Date.now(),
|
|
4305
|
+
let h2aSidecarStarted = false;
|
|
4306
|
+
if (h2aSidecar && sessionHost === "native") {
|
|
4307
|
+
h2aSidecarStarted = startNativeH2aSidecar(name, cwd, h2a.command, {
|
|
4308
|
+
...(structuredLaunch ? { verified: true } : {}),
|
|
4297
4309
|
});
|
|
4298
|
-
|
|
4310
|
+
if (!h2aSidecarStarted && structuredLaunch) {
|
|
4311
|
+
cleanupHeadlessPromptFile(promptFile);
|
|
4312
|
+
// OWN-CLEANUP, deliberately NOT resolver-derived (F1 sorting):
|
|
4313
|
+
// this kills the session THIS command just created on the host
|
|
4314
|
+
// it chose; the resolver could refuse and leak the partial.
|
|
4315
|
+
killNativeSessionTree(name);
|
|
4316
|
+
process.stderr.write(`[h2a] required h2a sidecar failed for ${slug}; the partial session was stopped\n`);
|
|
4317
|
+
process.exitCode = 1;
|
|
4318
|
+
return;
|
|
4319
|
+
}
|
|
4320
|
+
if (h2aSidecarStarted) {
|
|
4321
|
+
process.stderr.write(`[h2a] h2a sidecar session started for ${slug} (${h2a.command})\n`);
|
|
4322
|
+
}
|
|
4323
|
+
}
|
|
4324
|
+
else if (h2aSidecar) {
|
|
4325
|
+
if (structuredLaunch) {
|
|
4326
|
+
try {
|
|
4327
|
+
h2aSidecarStarted = Boolean(await startH2aWindowVerified(name, cwd, h2a.command, agentPane));
|
|
4328
|
+
}
|
|
4329
|
+
catch (error) {
|
|
4330
|
+
process.stderr.write(`[h2a] required h2a sidecar verification failed: ${error.message}\n`);
|
|
4331
|
+
h2aSidecarStarted = false;
|
|
4332
|
+
}
|
|
4333
|
+
}
|
|
4334
|
+
else {
|
|
4335
|
+
h2aSidecarStarted = startH2aWindow(name, cwd, h2a.command);
|
|
4336
|
+
}
|
|
4337
|
+
if (!h2aSidecarStarted && structuredLaunch) {
|
|
4338
|
+
cleanupHeadlessPromptFile(promptFile);
|
|
4339
|
+
// OWN-CLEANUP, deliberately NOT resolver-derived (F1 sorting):
|
|
4340
|
+
// this kills the session THIS command just created on the host
|
|
4341
|
+
// it chose; the resolver could refuse and leak the partial.
|
|
4342
|
+
killLocalSession(name);
|
|
4343
|
+
process.stderr.write(`[h2a] required h2a sidecar failed for ${slug}; the partial session was stopped\n`);
|
|
4344
|
+
process.exitCode = 1;
|
|
4345
|
+
return;
|
|
4346
|
+
}
|
|
4347
|
+
if (h2aSidecarStarted) {
|
|
4348
|
+
process.stderr.write(`[h2a] h2a window started in ${slug} (${h2a.command})\n`);
|
|
4349
|
+
}
|
|
4350
|
+
}
|
|
4351
|
+
if (!opts.headless && initialPrompt !== undefined) {
|
|
4352
|
+
// NEVER paste-and-hope: `tmux new-session -d` returns before the
|
|
4353
|
+
// agent TUI draws anything, and a prompt sent into that window is
|
|
4354
|
+
// echoed by the raw terminal and then discarded. Delivery is
|
|
4355
|
+
// OBSERVED — brief seen in a real composer, then real work — because
|
|
4356
|
+
// reporting "started" over a lost brief is what left a lane inert
|
|
4357
|
+
// for 33 minutes on 2026-07-28.
|
|
4358
|
+
promptDelivery = sessionHost === "native"
|
|
4359
|
+
? deliverInitialPrompt(name, initialPrompt, nativePromptDeliveryDeps(sleepSync))
|
|
4360
|
+
: deliverInitialPrompt(agentPane, initialPrompt, {
|
|
4361
|
+
capturePane: capturePaneVisible,
|
|
4362
|
+
clearComposer: clearPaneComposer,
|
|
4363
|
+
pasteBlock: pasteLiteralBlock,
|
|
4364
|
+
submit: submitPane,
|
|
4365
|
+
cpuMs: paneTreeCpuMs,
|
|
4366
|
+
sleep: sleepSync,
|
|
4367
|
+
now: () => Date.now(),
|
|
4368
|
+
});
|
|
4369
|
+
if (promptDelivery.state !== "working") {
|
|
4370
|
+
cleanupHeadlessPromptFile(promptFile);
|
|
4371
|
+
// Only claim the session is gone when the kill actually reported
|
|
4372
|
+
// success. "nothing is left running" over an unchecked call is the
|
|
4373
|
+
// same wider-than-its-evidence claim this change exists to remove,
|
|
4374
|
+
// and a surviving partial session is precisely the idle lane the
|
|
4375
|
+
// operator was just told could not happen.
|
|
4376
|
+
// OWN-CLEANUP, deliberately NOT resolver-derived (F1 sorting):
|
|
4377
|
+
// `name`/`sessionHost` are the session and host THIS command
|
|
4378
|
+
// just created; re-resolving could refuse and leak the partial.
|
|
4379
|
+
const stopped = sessionHost === "native"
|
|
4380
|
+
? killNativeSessionTree(name)
|
|
4381
|
+
: killLocalSession(name);
|
|
4382
|
+
const detail = promptDelivery.state === "submitted-idle"
|
|
4383
|
+
? `the brief was submitted but the agent never started working ` +
|
|
4384
|
+
`(${Math.round(promptDelivery.cpuDeltaMs)}ms of CPU): it is not "started"`
|
|
4385
|
+
: promptDelivery.reason;
|
|
4386
|
+
process.stderr.write(`[h2a] the initial prompt did not reach ${slug}: ${detail}\n` +
|
|
4387
|
+
(promptDelivery.state === "host-modal"
|
|
4388
|
+
? `[h2a] fix: ${promptDelivery.hint}\n`
|
|
4389
|
+
: "") +
|
|
4390
|
+
(stopped
|
|
4391
|
+
? `[h2a] the partial session was stopped, so nothing is left running idle\n`
|
|
4392
|
+
: `[h2a] WARNING: stopping the partial session FAILED — ${name} may still be alive; ` +
|
|
4393
|
+
`list it with h2a ls and stop it with h2a stop ${slug} --reason failed-launch\n`) +
|
|
4394
|
+
(promptDelivery.state !== "submitted-idle" &&
|
|
4395
|
+
promptDelivery.capture
|
|
4396
|
+
? `[h2a] last screen:\n${promptDelivery.capture}\n`
|
|
4397
|
+
: ""));
|
|
4398
|
+
process.exitCode = 1;
|
|
4399
|
+
return;
|
|
4400
|
+
}
|
|
4401
|
+
}
|
|
4402
|
+
const pid = sessionHost === "native"
|
|
4403
|
+
? nativeSessionPid(name)
|
|
4404
|
+
: agentPane
|
|
4405
|
+
? localSessionPanePid(agentPane)
|
|
4406
|
+
: undefined;
|
|
4407
|
+
const workerPid = sessionHost === "native"
|
|
4408
|
+
? nativeWorkerPid(name)
|
|
4409
|
+
: agentPane !== undefined
|
|
4410
|
+
? paneWorkerPid(agentPane)
|
|
4411
|
+
: undefined;
|
|
4412
|
+
if (structuredLaunch && pid === undefined) {
|
|
4299
4413
|
cleanupHeadlessPromptFile(promptFile);
|
|
4300
|
-
// Only claim the session is gone when the kill actually reported
|
|
4301
|
-
// success. "nothing is left running" over an unchecked call is the
|
|
4302
|
-
// same wider-than-its-evidence claim this change exists to remove,
|
|
4303
|
-
// and a surviving partial session is precisely the idle lane the
|
|
4304
|
-
// operator was just told could not happen.
|
|
4305
4414
|
// OWN-CLEANUP, deliberately NOT resolver-derived (F1 sorting):
|
|
4306
4415
|
// `name`/`sessionHost` are the session and host THIS command
|
|
4307
4416
|
// just created; re-resolving could refuse and leak the partial.
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
`(${Math.round(promptDelivery.cpuDeltaMs)}ms of CPU): it is not "started"`
|
|
4314
|
-
: promptDelivery.reason;
|
|
4315
|
-
process.stderr.write(`[h2a] the initial prompt did not reach ${slug}: ${detail}\n` +
|
|
4316
|
-
(promptDelivery.state === "host-modal"
|
|
4317
|
-
? `[h2a] fix: ${promptDelivery.hint}\n`
|
|
4318
|
-
: "") +
|
|
4319
|
-
(stopped
|
|
4320
|
-
? `[h2a] the partial session was stopped, so nothing is left running idle\n`
|
|
4321
|
-
: `[h2a] WARNING: stopping the partial session FAILED — ${name} may still be alive; ` +
|
|
4322
|
-
`list it with h2a ls and stop it with h2a stop ${slug} --reason failed-launch\n`) +
|
|
4323
|
-
(promptDelivery.state !== "submitted-idle" &&
|
|
4324
|
-
promptDelivery.capture
|
|
4325
|
-
? `[h2a] last screen:\n${promptDelivery.capture}\n`
|
|
4326
|
-
: ""));
|
|
4417
|
+
if (sessionHost === "native")
|
|
4418
|
+
killNativeSessionTree(name);
|
|
4419
|
+
else
|
|
4420
|
+
killLocalSession(name);
|
|
4421
|
+
process.stderr.write(`[h2a] could not verify the agent pane pid for ${slug}; the partial session was stopped\n`);
|
|
4327
4422
|
process.exitCode = 1;
|
|
4328
4423
|
return;
|
|
4329
4424
|
}
|
|
4425
|
+
// Auto-enroll in the live-session registry (feeds `h2a ls`/`restore`).
|
|
4426
|
+
// Pin the gateway posture ONLY when the user was explicit (--gw/--no-gw);
|
|
4427
|
+
// an "auto" launch stays unpinned so restore follows the live default.
|
|
4428
|
+
enrollFromRun({
|
|
4429
|
+
profile,
|
|
4430
|
+
slug,
|
|
4431
|
+
tmuxSession: name,
|
|
4432
|
+
// The ACTUAL host that just started the terminal — never a default.
|
|
4433
|
+
hostKind: sessionHost === "native" ? "local-native" : "local-tmux",
|
|
4434
|
+
...(pid !== undefined ? { pid } : {}),
|
|
4435
|
+
...(workerPid !== undefined ? { workerPid } : {}),
|
|
4436
|
+
cwd,
|
|
4437
|
+
sessionClass,
|
|
4438
|
+
...(opts.resume !== undefined ? { convId: opts.resume } : {}),
|
|
4439
|
+
...(gatewayMode !== "auto" ? { gatewayMode } : {}),
|
|
4440
|
+
});
|
|
4441
|
+
started.push({
|
|
4442
|
+
name,
|
|
4443
|
+
slug,
|
|
4444
|
+
...(agentPane !== undefined ? { pane: agentPane } : {}),
|
|
4445
|
+
h2aSidecar: h2aSidecarStarted,
|
|
4446
|
+
...(pid !== undefined ? { pid } : {}),
|
|
4447
|
+
...(outputLog !== undefined ? { outputLog } : {}),
|
|
4448
|
+
...(resultJson !== undefined ? { resultJson } : {}),
|
|
4449
|
+
...(promptDelivery !== undefined ? { promptDelivery } : {}),
|
|
4450
|
+
});
|
|
4451
|
+
// The writer is now durable in the registry. End the short critical
|
|
4452
|
+
// section before a foreground attach can keep this command alive.
|
|
4453
|
+
resumeClaim?.release();
|
|
4330
4454
|
}
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
: undefined;
|
|
4336
|
-
const workerPid = sessionHost === "native"
|
|
4337
|
-
? nativeWorkerPid(name)
|
|
4338
|
-
: agentPane !== undefined
|
|
4339
|
-
? paneWorkerPid(agentPane)
|
|
4340
|
-
: undefined;
|
|
4341
|
-
if (structuredLaunch && pid === undefined) {
|
|
4342
|
-
cleanupHeadlessPromptFile(promptFile);
|
|
4343
|
-
// OWN-CLEANUP, deliberately NOT resolver-derived (F1 sorting):
|
|
4344
|
-
// `name`/`sessionHost` are the session and host THIS command
|
|
4345
|
-
// just created; re-resolving could refuse and leak the partial.
|
|
4346
|
-
if (sessionHost === "native")
|
|
4347
|
-
killNativeSessionTree(name);
|
|
4348
|
-
else
|
|
4349
|
-
killLocalSession(name);
|
|
4350
|
-
process.stderr.write(`[h2a] could not verify the agent pane pid for ${slug}; the partial session was stopped\n`);
|
|
4351
|
-
process.exitCode = 1;
|
|
4352
|
-
return;
|
|
4455
|
+
if (count > 1) {
|
|
4456
|
+
process.stderr.write(`[h2a] ${started.length} ${profile} agents started in ${cwd}: ${started.map((s) => s.slug).join(", ")}\n` +
|
|
4457
|
+
`[h2a] attach one with: h2a attach <slug>\n`);
|
|
4458
|
+
return; // never auto-attach a fleet
|
|
4353
4459
|
}
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
id: only.slug,
|
|
4397
|
-
tmuxSession: only.name,
|
|
4398
|
-
host: sessionHost === "native" ? "native" : "tmux",
|
|
4399
|
-
profile,
|
|
4400
|
-
workspace: cwd,
|
|
4401
|
-
mode: opts.headless ? "headless" : "interactive",
|
|
4402
|
-
background: opts.background === true,
|
|
4403
|
-
gateway: activeGateway ? "gateway" : "direct",
|
|
4404
|
-
h2aSidecar: only.h2aSidecar,
|
|
4405
|
-
...(only.pane !== undefined ? { pane: only.pane } : {}),
|
|
4406
|
-
...(only.pid !== undefined ? { pid: only.pid } : {}),
|
|
4407
|
-
// `pid` is the launch WRAPPER. Measured 2026-07-29: it showed 0s
|
|
4408
|
-
// of CPU after 37s while its codex child had burned 13s, so a
|
|
4409
|
-
// liveness check on `pid` alone reports a working agent as dead.
|
|
4410
|
-
// Name the process that actually works, and say it is distinct.
|
|
4411
|
-
...(only.workerPid !== undefined
|
|
4412
|
-
? { workerPid: only.workerPid }
|
|
4460
|
+
const only = started[0];
|
|
4461
|
+
if (opts.json) {
|
|
4462
|
+
process.stdout.write(`${JSON.stringify({
|
|
4463
|
+
kind: "h2a.run.result",
|
|
4464
|
+
version: 1,
|
|
4465
|
+
apiVersion: H2A_RUN_API_VERSION,
|
|
4466
|
+
runtimeVersion: H2A_RUNTIME_VERSION,
|
|
4467
|
+
ok: true,
|
|
4468
|
+
state: "started",
|
|
4469
|
+
session: {
|
|
4470
|
+
id: only.slug,
|
|
4471
|
+
tmuxSession: only.name,
|
|
4472
|
+
host: sessionHost === "native" ? "native" : "tmux",
|
|
4473
|
+
profile,
|
|
4474
|
+
workspace: cwd,
|
|
4475
|
+
mode: opts.headless ? "headless" : "interactive",
|
|
4476
|
+
background: opts.background === true,
|
|
4477
|
+
gateway: activeGateway ? "gateway" : "direct",
|
|
4478
|
+
h2aSidecar: only.h2aSidecar,
|
|
4479
|
+
...(only.pane !== undefined ? { pane: only.pane } : {}),
|
|
4480
|
+
...(only.pid !== undefined ? { pid: only.pid } : {}),
|
|
4481
|
+
// `pid` is the launch WRAPPER. Measured 2026-07-29: it showed 0s
|
|
4482
|
+
// of CPU after 37s while its codex child had burned 13s, so a
|
|
4483
|
+
// liveness check on `pid` alone reports a working agent as dead.
|
|
4484
|
+
// Name the process that actually works, and say it is distinct.
|
|
4485
|
+
...(only.workerPid !== undefined
|
|
4486
|
+
? { workerPid: only.workerPid }
|
|
4487
|
+
: {}),
|
|
4488
|
+
},
|
|
4489
|
+
attach: opts.headless
|
|
4490
|
+
? null
|
|
4491
|
+
: { command: "h2a", args: ["attach", only.slug] },
|
|
4492
|
+
// What was OBSERVED, not what was attempted: the brief was seen in
|
|
4493
|
+
// a real composer, submitted, and the agent's CPU then moved.
|
|
4494
|
+
...(only.promptDelivery?.state === "working"
|
|
4495
|
+
? {
|
|
4496
|
+
prompt: {
|
|
4497
|
+
delivered: true,
|
|
4498
|
+
waitedMs: only.promptDelivery.waitedMs,
|
|
4499
|
+
cpuDeltaMs: Math.round(only.promptDelivery.cpuDeltaMs),
|
|
4500
|
+
},
|
|
4501
|
+
}
|
|
4413
4502
|
: {}),
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
:
|
|
4435
|
-
})}\n`);
|
|
4503
|
+
...(only.outputLog !== undefined
|
|
4504
|
+
? {
|
|
4505
|
+
logs: { path: only.outputLog },
|
|
4506
|
+
result: { path: only.resultJson },
|
|
4507
|
+
}
|
|
4508
|
+
: {}),
|
|
4509
|
+
})}\n`);
|
|
4510
|
+
return;
|
|
4511
|
+
}
|
|
4512
|
+
process.stderr.write(`[h2a] local session ${only.slug} started (${profile}${opts.resume ? ` --resume ${opts.resume}` : ""} in ${cwd})\n`);
|
|
4513
|
+
// Single local runs hand off the terminal by default: there is no value
|
|
4514
|
+
// in making users type a second `h2a attach <slug>` command. Commander
|
|
4515
|
+
// sets opts.attach=false only for --no-attach.
|
|
4516
|
+
if (opts.attach === false) {
|
|
4517
|
+
process.stderr.write(`[h2a] attach with: h2a attach ${only.slug}\n`);
|
|
4518
|
+
return;
|
|
4519
|
+
}
|
|
4520
|
+
process.exitCode =
|
|
4521
|
+
sessionHost === "native"
|
|
4522
|
+
? attachNativeSession(only.name)
|
|
4523
|
+
: attachLocalSession(only.name);
|
|
4436
4524
|
return;
|
|
4437
4525
|
}
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
// in making users type a second `h2a attach <slug>` command. Commander
|
|
4441
|
-
// sets opts.attach=false only for --no-attach.
|
|
4442
|
-
if (opts.attach === false) {
|
|
4443
|
-
process.stderr.write(`[h2a] attach with: h2a attach ${only.slug}\n`);
|
|
4444
|
-
return;
|
|
4526
|
+
finally {
|
|
4527
|
+
resumeClaim?.release();
|
|
4445
4528
|
}
|
|
4446
|
-
process.exitCode =
|
|
4447
|
-
sessionHost === "native"
|
|
4448
|
-
? attachNativeSession(only.name)
|
|
4449
|
-
: attachLocalSession(only.name);
|
|
4450
|
-
return;
|
|
4451
4529
|
});
|
|
4452
4530
|
// ---------------------------------------------------------------------------
|
|
4453
4531
|
// delegate — spawn a cross-type agent (claude/codex/agy) as a LOCAL job (P1)
|