@sentropic/h2a-runtime 0.85.0 → 0.85.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/attach.js +9 -9
- package/dist/attach.js.map +1 -1
- package/dist/auth-bundle.js +1 -1
- package/dist/auth-bundle.js.map +1 -1
- package/dist/auth-refresh.js +1 -1
- package/dist/auth-refresh.js.map +1 -1
- package/dist/browser-bridge-local.js +1 -1
- package/dist/browser-bridge-local.js.map +1 -1
- package/dist/browser.js +4 -4
- package/dist/browser.js.map +1 -1
- package/dist/conv-guard.js +6 -6
- package/dist/conv-guard.js.map +1 -1
- package/dist/convsync.js +1 -1
- package/dist/convsync.js.map +1 -1
- package/dist/cred-health.js +6 -6
- package/dist/cred-health.js.map +1 -1
- package/dist/delegate.js +1 -1
- package/dist/delegate.js.map +1 -1
- package/dist/forward.js +5 -5
- package/dist/forward.js.map +1 -1
- package/dist/h2a-bridge.js +4 -4
- package/dist/h2a-bridge.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +413 -404
- package/dist/index.js.map +1 -1
- package/dist/interactive-throttle.js +6 -6
- package/dist/interactive-throttle.js.map +1 -1
- package/dist/llm-mesh.js +1 -1
- package/dist/llm-mesh.js.map +1 -1
- package/dist/migrate.js +32 -32
- package/dist/migrate.js.map +1 -1
- package/dist/plugin.js +27 -27
- package/dist/plugin.js.map +1 -1
- package/dist/pod-liveness.js +1 -1
- package/dist/pod-liveness.js.map +1 -1
- package/dist/profile-menu.js +3 -3
- package/dist/profile-menu.js.map +1 -1
- package/dist/registry.d.ts +16 -0
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +32 -0
- package/dist/registry.js.map +1 -1
- package/dist/restore.js +9 -9
- package/dist/restore.js.map +1 -1
- package/dist/skills-sync.js +6 -6
- package/dist/skills-sync.js.map +1 -1
- package/dist/smoke.js +4 -4
- package/dist/smoke.js.map +1 -1
- package/dist/soft-refresh.js +15 -15
- package/dist/soft-refresh.js.map +1 -1
- package/dist/sync.js +1 -1
- package/dist/sync.js.map +1 -1
- package/dist/tmux.d.ts +1 -1
- package/dist/tmux.d.ts.map +1 -1
- package/dist/tmux.js +12 -12
- package/dist/tmux.js.map +1 -1
- package/dist/tunnel.js +2 -2
- package/dist/tunnel.js.map +1 -1
- package/dist/workspace-sync.js +1 -1
- package/dist/workspace-sync.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -18,7 +18,7 @@ import { attachLocalSession, attachPodTmux, capturePane, conductorRunning, curre
|
|
|
18
18
|
import { planRelaunch } from "./relaunch.js";
|
|
19
19
|
import { readLastLayout, restore as restoreLayout, } from "./restore.js";
|
|
20
20
|
import { getLayoutConfig } from "./config.js";
|
|
21
|
-
import { advanceJob, coerceRegistryTool, enroll, enrollFromRun, isLive, listJobs, listLocalForLs, loadRegistry, tryClaimSlot, withRegistryLock, resolveRegistryPath, } from "./registry.js";
|
|
21
|
+
import { advanceJob, coerceRegistryTool, enroll, enrollFromRun, isLive, listJobs, listLocalForLs, loadRegistry, localTmuxSessionForName, tryClaimSlot, withRegistryLock, resolveRegistryPath, } from "./registry.js";
|
|
22
22
|
import { aimdEffectiveCap, assertSafeName, buildDelegateArgs, buildJobRows, buildRemoteDelegate, buildThrottleResumeArgs, canDelegateAtDepth, childDepthEnvValue, clampDepth, clampRemoteDepthBudget, conductorAdvisory, DEFAULT_MAX_CONCURRENT, DEPTH_ENV, inheritedDepthBudget, isDelegateType, isThrottleResumeDue, jitteredBackoffMs, JOB_ID_ENV, jobDir, planNextStarts, planThrottleStep, readJobResult, THROTTLE_MAX_ATTEMPTS, reconcileRemoteJobs, renderJobsTable, resolveJobCwd, runTrackMirror, sweepStaleJobs, trackItemNewArgs, trackItemRealizeArgs, } from "./delegate.js";
|
|
23
23
|
import { detectThrottle, THROTTLE_TAIL_LINES } from "./throttle-signatures.js";
|
|
24
24
|
import { interactiveResumeNudge, planInteractiveResume, } from "./interactive-throttle.js";
|
|
@@ -135,12 +135,12 @@ async function startRemoteFanout(remote, profileName, members, spec) {
|
|
|
135
135
|
].join("\t");
|
|
136
136
|
});
|
|
137
137
|
process.stdout.write(`NAME\tSESSION\tWORKSPACE\tSTATUS\n${rows.join("\n")}\n`);
|
|
138
|
-
process.stderr.write(`[
|
|
138
|
+
process.stderr.write(`[h2a] ${ok.length}/${members.length} remote ${profileName} sessions created on ${remote} (shared RWX, subPath per session)\n`);
|
|
139
139
|
if (ok.length > 0) {
|
|
140
|
-
process.stderr.write(`[
|
|
140
|
+
process.stderr.write(`[h2a] attach one with: h2a attach <session> (list: h2a ls)\n`);
|
|
141
141
|
}
|
|
142
142
|
if (failed.length > 0) {
|
|
143
|
-
process.stderr.write(`[
|
|
143
|
+
process.stderr.write(`[h2a] ${failed.length} session(s) failed to create — re-run --count for the shortfall, or check the control-plane\n`);
|
|
144
144
|
process.exitCode = 1;
|
|
145
145
|
}
|
|
146
146
|
}
|
|
@@ -152,7 +152,7 @@ async function runProfile(profileName, opts, commandArgs = []) {
|
|
|
152
152
|
throw new Error("--count must be a whole number >= 1");
|
|
153
153
|
}
|
|
154
154
|
if (opts.count > 1 && !opts.remote) {
|
|
155
|
-
throw new Error("--count > 1 is a REMOTE fan-out — it needs a configured remote (use `
|
|
155
|
+
throw new Error("--count > 1 is a REMOTE fan-out — it needs a configured remote (use `h2a run --count` for LOCAL tmux fan-out)");
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
158
|
if (opts.remote) {
|
|
@@ -162,7 +162,7 @@ async function runProfile(profileName, opts, commandArgs = []) {
|
|
|
162
162
|
if (opts.authRefresh !== false) {
|
|
163
163
|
const result = await ensureProfileAuthFresh(profileName);
|
|
164
164
|
if (result.checked) {
|
|
165
|
-
process.stderr.write(`[
|
|
165
|
+
process.stderr.write(`[h2a] auth status ok: ${result.command}\n`);
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
168
|
const bundle = await collectProfileAuth(profileName);
|
|
@@ -210,9 +210,9 @@ async function runProfile(profileName, opts, commandArgs = []) {
|
|
|
210
210
|
}
|
|
211
211
|
let archive;
|
|
212
212
|
if (opts.sync) {
|
|
213
|
-
process.stderr.write(`[
|
|
213
|
+
process.stderr.write(`[h2a] packing ${process.cwd()} (respecting .gitignore)\n`);
|
|
214
214
|
archive = await buildWorkspaceArchive(process.cwd());
|
|
215
|
-
process.stderr.write(`[
|
|
215
|
+
process.stderr.write(`[h2a] workspace archive: ${(archive.byteLength / 1024).toFixed(0)} KiB\n`);
|
|
216
216
|
}
|
|
217
217
|
const session = await createRemoteSession(opts.remote, {
|
|
218
218
|
profile: profileName,
|
|
@@ -226,13 +226,13 @@ async function runProfile(profileName, opts, commandArgs = []) {
|
|
|
226
226
|
});
|
|
227
227
|
if (archive) {
|
|
228
228
|
await uploadWorkspaceArchive(opts.remote, session.id, archive);
|
|
229
|
-
process.stderr.write(`[
|
|
229
|
+
process.stderr.write(`[h2a] uploaded workspace to ${opts.remote}/sessions/${session.id}/workspace\n`);
|
|
230
230
|
}
|
|
231
231
|
if (credentials) {
|
|
232
|
-
process.stderr.write(`[
|
|
233
|
-
process.stderr.write(`[
|
|
232
|
+
process.stderr.write(`[h2a] sending ${profileName} creds to ${opts.remote}: ${Object.keys(credentials).join(", ")}\n`);
|
|
233
|
+
process.stderr.write(`[h2a] (use --no-auth to start without credentials)\n`);
|
|
234
234
|
}
|
|
235
|
-
process.stderr.write(`[
|
|
235
|
+
process.stderr.write(`[h2a] attached to ${opts.remote}/sessions/${session.id}\n`);
|
|
236
236
|
const attachSession = await attach({
|
|
237
237
|
baseUrl: opts.remote,
|
|
238
238
|
sessionId: session.id,
|
|
@@ -247,7 +247,7 @@ async function runProfile(profileName, opts, commandArgs = []) {
|
|
|
247
247
|
...(commandArgs.length > 0 ? { startupArgs: commandArgs } : {}),
|
|
248
248
|
};
|
|
249
249
|
const result = await run(runOptions);
|
|
250
|
-
process.stderr.write(`[
|
|
250
|
+
process.stderr.write(`[h2a] session ${result.sessionId} attach at http://127.0.0.1:${result.port}\n`);
|
|
251
251
|
const { exitCode } = await result.exit;
|
|
252
252
|
process.exitCode = exitCode;
|
|
253
253
|
}
|
|
@@ -257,7 +257,7 @@ async function refreshProfileSession(baseUrl, sessionId, opts) {
|
|
|
257
257
|
const requestedProfile = opts.profile ?? remoteProfile;
|
|
258
258
|
if (opts.profile &&
|
|
259
259
|
coerceCliProfileName(opts.profile) !== coerceCliProfileName(remoteProfile)) {
|
|
260
|
-
process.stderr.write(`[
|
|
260
|
+
process.stderr.write(`[h2a] warning: --profile ${opts.profile} does not match the session profile ${remoteProfile}; bundling ${opts.profile} credentials anyway\n`);
|
|
261
261
|
}
|
|
262
262
|
const profileName = coerceCliProfileName(requestedProfile);
|
|
263
263
|
if (!profileName) {
|
|
@@ -268,7 +268,7 @@ async function refreshProfileSession(baseUrl, sessionId, opts) {
|
|
|
268
268
|
if (opts.authRefresh !== false) {
|
|
269
269
|
const result = await ensureProfileAuthFresh(profileName);
|
|
270
270
|
if (result.checked) {
|
|
271
|
-
process.stderr.write(`[
|
|
271
|
+
process.stderr.write(`[h2a] auth status ok: ${result.command}\n`);
|
|
272
272
|
}
|
|
273
273
|
}
|
|
274
274
|
const bundle = await collectProfileAuth(profileName);
|
|
@@ -277,11 +277,11 @@ async function refreshProfileSession(baseUrl, sessionId, opts) {
|
|
|
277
277
|
credentials = bundle;
|
|
278
278
|
}
|
|
279
279
|
if (!credentials || Object.keys(credentials).length === 0) {
|
|
280
|
-
process.stderr.write(`[
|
|
280
|
+
process.stderr.write(`[h2a] no credentials to refresh for session ${sessionId} (${profileName})\n`);
|
|
281
281
|
return;
|
|
282
282
|
}
|
|
283
283
|
const response = await refreshRemoteSession(baseUrl, sessionId, credentials, fetch, opts.name);
|
|
284
|
-
process.stderr.write(`[
|
|
284
|
+
process.stderr.write(`[h2a] refresh ${response.accepted ? "accepted" : "rejected"} for ${response.sessionId}` +
|
|
285
285
|
(opts.name ? ` (renamed → ${opts.name})` : "") +
|
|
286
286
|
"\n");
|
|
287
287
|
}
|
|
@@ -305,12 +305,12 @@ async function pushAllProfiles(baseUrl, sessionId, opts) {
|
|
|
305
305
|
sent.push(profile);
|
|
306
306
|
}
|
|
307
307
|
if (sent.length === 0) {
|
|
308
|
-
process.stderr.write(`[
|
|
308
|
+
process.stderr.write(`[h2a] no local credentials found for any profile; nothing to push\n`);
|
|
309
309
|
return;
|
|
310
310
|
}
|
|
311
|
-
process.stderr.write(`[
|
|
311
|
+
process.stderr.write(`[h2a] sending creds for ${sent.join(", ")} to ${baseUrl}/sessions/${sessionId}: ${Object.keys(merged).join(", ")}\n`);
|
|
312
312
|
const response = await refreshRemoteSession(baseUrl, sessionId, merged);
|
|
313
|
-
process.stderr.write(`[
|
|
313
|
+
process.stderr.write(`[h2a] push ${response.accepted ? "accepted" : "rejected"} for ${response.sessionId}\n`);
|
|
314
314
|
}
|
|
315
315
|
/** Validate `--watch <minutes>`: a whole number of minutes >= 1. */
|
|
316
316
|
export function parseWatchMinutes(raw) {
|
|
@@ -415,12 +415,12 @@ async function preflightOrWarn(profile) {
|
|
|
415
415
|
try {
|
|
416
416
|
const fresh = await ensureProfileAuthFresh(profile);
|
|
417
417
|
if (fresh.checked) {
|
|
418
|
-
process.stderr.write(`[
|
|
418
|
+
process.stderr.write(`[h2a] auth status ok: ${fresh.command}\n`);
|
|
419
419
|
}
|
|
420
420
|
}
|
|
421
421
|
catch (error) {
|
|
422
422
|
const detail = error instanceof Error ? error.message : String(error);
|
|
423
|
-
process.stderr.write(`[
|
|
423
|
+
process.stderr.write(`[h2a] auth preflight failed (${detail.slice(0, 120)}) — pushing current creds anyway\n`);
|
|
424
424
|
}
|
|
425
425
|
}
|
|
426
426
|
/**
|
|
@@ -440,7 +440,7 @@ export async function softRefreshAllSessions(url, opts, hashes) {
|
|
|
440
440
|
process.stderr.write(`${claudeWarn}\n`);
|
|
441
441
|
const sessions = await listRemoteSessions(url);
|
|
442
442
|
if (sessions.length === 0) {
|
|
443
|
-
process.stderr.write("[
|
|
443
|
+
process.stderr.write("[h2a] no live remote sessions to refresh\n");
|
|
444
444
|
return { failed: 0 };
|
|
445
445
|
}
|
|
446
446
|
const preflighted = new Set();
|
|
@@ -492,12 +492,12 @@ export async function softRefreshAllSessions(url, opts, hashes) {
|
|
|
492
492
|
const actions = await probePodCredHealth(s.id);
|
|
493
493
|
for (const a of actions) {
|
|
494
494
|
if (!a.health.ok) {
|
|
495
|
-
process.stderr.write(`[
|
|
495
|
+
process.stderr.write(`[h2a] ${s.id}: ${a.health.reason}${a.pushed ? " — pushed" : ""}\n`);
|
|
496
496
|
}
|
|
497
497
|
}
|
|
498
498
|
}
|
|
499
499
|
catch (probeError) {
|
|
500
|
-
process.stderr.write(`[
|
|
500
|
+
process.stderr.write(`[h2a] ${s.id}: tool health probe skipped: ${String(probeError).slice(0, 120)}\n`);
|
|
501
501
|
}
|
|
502
502
|
// Slice 3: ADDITIONAL trigger — compare the local desired-state manifest
|
|
503
503
|
// hash with the Pod's recorded sidecar; on drift, re-run the EXISTING
|
|
@@ -506,7 +506,7 @@ export async function softRefreshAllSessions(url, opts, hashes) {
|
|
|
506
506
|
reconcileSessionPlugins(s.id, profile);
|
|
507
507
|
}
|
|
508
508
|
catch (driftError) {
|
|
509
|
-
process.stderr.write(`[
|
|
509
|
+
process.stderr.write(`[h2a] ${s.id}: plugin drift reconcile skipped: ${String(driftError).slice(0, 120)}\n`);
|
|
510
510
|
}
|
|
511
511
|
outcomes.push({
|
|
512
512
|
sessionId: s.id,
|
|
@@ -524,7 +524,7 @@ export async function softRefreshAllSessions(url, opts, hashes) {
|
|
|
524
524
|
}
|
|
525
525
|
}
|
|
526
526
|
const failed = outcomes.filter((o) => o.status === "failed").length;
|
|
527
|
-
process.stderr.write(`[
|
|
527
|
+
process.stderr.write(`[h2a] soft refresh recap — ${outcomes.length} session(s), ${failed} failed:\n`);
|
|
528
528
|
for (const o of outcomes) {
|
|
529
529
|
process.stderr.write(` ${o.sessionId} (${o.profile}) ${o.status}${o.detail ? ` — ${o.detail}` : ""}\n`);
|
|
530
530
|
}
|
|
@@ -566,12 +566,12 @@ async function softRefreshOneGated(url, sessionId, opts, hashes) {
|
|
|
566
566
|
const actions = await probePodCredHealth(sessionId);
|
|
567
567
|
for (const a of actions) {
|
|
568
568
|
if (!a.health.ok) {
|
|
569
|
-
process.stderr.write(`[
|
|
569
|
+
process.stderr.write(`[h2a] ${sessionId}: ${a.health.reason}${a.pushed ? " — pushed" : ""}\n`);
|
|
570
570
|
}
|
|
571
571
|
}
|
|
572
572
|
}
|
|
573
573
|
catch (probeError) {
|
|
574
|
-
process.stderr.write(`[
|
|
574
|
+
process.stderr.write(`[h2a] ${sessionId}: tool health probe skipped: ${String(probeError).slice(0, 120)}\n`);
|
|
575
575
|
}
|
|
576
576
|
// Slice 3: ADDITIONAL trigger — plugin/MCP drift reconcile (manifest hash
|
|
577
577
|
// mismatch → re-run the EXISTING buildPodSyncScript). Best-effort.
|
|
@@ -579,13 +579,13 @@ async function softRefreshOneGated(url, sessionId, opts, hashes) {
|
|
|
579
579
|
reconcileSessionPlugins(sessionId, profile);
|
|
580
580
|
}
|
|
581
581
|
catch (driftError) {
|
|
582
|
-
process.stderr.write(`[
|
|
582
|
+
process.stderr.write(`[h2a] ${sessionId}: plugin drift reconcile skipped: ${String(driftError).slice(0, 120)}\n`);
|
|
583
583
|
}
|
|
584
|
-
process.stderr.write(`[
|
|
584
|
+
process.stderr.write(`[h2a] ${sessionId} (${profile}) ${result.changed ? "refreshed" : "unchanged"}\n`);
|
|
585
585
|
return { failed: 0 };
|
|
586
586
|
}
|
|
587
587
|
catch (error) {
|
|
588
|
-
process.stderr.write(`[
|
|
588
|
+
process.stderr.write(`[h2a] ${sessionId} refresh failed: ${(error instanceof Error ? error.message : String(error)).slice(0, 200)}\n`);
|
|
589
589
|
return { failed: 1 };
|
|
590
590
|
}
|
|
591
591
|
}
|
|
@@ -607,16 +607,16 @@ signals = process) {
|
|
|
607
607
|
signals.on("SIGINT", onSigint);
|
|
608
608
|
try {
|
|
609
609
|
while (!stopped) {
|
|
610
|
-
process.stderr.write(`[
|
|
610
|
+
process.stderr.write(`[h2a] refresh pass — ${new Date().toISOString()}\n`);
|
|
611
611
|
try {
|
|
612
612
|
await pass();
|
|
613
613
|
}
|
|
614
614
|
catch (error) {
|
|
615
|
-
process.stderr.write(`[
|
|
615
|
+
process.stderr.write(`[h2a] refresh pass failed: ${(error instanceof Error ? error.message : String(error)).slice(0, 200)}\n`);
|
|
616
616
|
}
|
|
617
617
|
if (stopped)
|
|
618
618
|
break;
|
|
619
|
-
process.stderr.write(`[
|
|
619
|
+
process.stderr.write(`[h2a] next pass in ${minutes} min (Ctrl-C to stop)\n`);
|
|
620
620
|
await new Promise((resolve) => {
|
|
621
621
|
const timer = setTimeout(resolve, minutes * 60_000);
|
|
622
622
|
wake = () => {
|
|
@@ -630,7 +630,7 @@ signals = process) {
|
|
|
630
630
|
finally {
|
|
631
631
|
signals.removeListener("SIGINT", onSigint);
|
|
632
632
|
}
|
|
633
|
-
process.stderr.write("[
|
|
633
|
+
process.stderr.write("[h2a] watch stopped (SIGINT)\n");
|
|
634
634
|
return 0;
|
|
635
635
|
}
|
|
636
636
|
/**
|
|
@@ -651,17 +651,17 @@ export async function conductLoop(minutes, pass, signals = process) {
|
|
|
651
651
|
signals.on("SIGINT", onSigint);
|
|
652
652
|
try {
|
|
653
653
|
while (!stopped) {
|
|
654
|
-
process.stderr.write(`[
|
|
654
|
+
process.stderr.write(`[h2a] conduct pass — ${new Date().toISOString()}\n`);
|
|
655
655
|
try {
|
|
656
656
|
const { started, finished } = await pass();
|
|
657
|
-
process.stderr.write(`[
|
|
657
|
+
process.stderr.write(`[h2a] conduct: ${started} started, ${finished} finished this pass\n`);
|
|
658
658
|
}
|
|
659
659
|
catch (error) {
|
|
660
|
-
process.stderr.write(`[
|
|
660
|
+
process.stderr.write(`[h2a] conduct pass failed: ${(error instanceof Error ? error.message : String(error)).slice(0, 200)}\n`);
|
|
661
661
|
}
|
|
662
662
|
if (stopped)
|
|
663
663
|
break;
|
|
664
|
-
process.stderr.write(`[
|
|
664
|
+
process.stderr.write(`[h2a] next conduct pass in ${minutes} min (Ctrl-C to stop)\n`);
|
|
665
665
|
await new Promise((resolve) => {
|
|
666
666
|
const timer = setTimeout(resolve, minutes * 60_000);
|
|
667
667
|
wake = () => {
|
|
@@ -675,7 +675,7 @@ export async function conductLoop(minutes, pass, signals = process) {
|
|
|
675
675
|
finally {
|
|
676
676
|
signals.removeListener("SIGINT", onSigint);
|
|
677
677
|
}
|
|
678
|
-
process.stderr.write("[
|
|
678
|
+
process.stderr.write("[h2a] conduct stopped (SIGINT)\n");
|
|
679
679
|
return 0;
|
|
680
680
|
}
|
|
681
681
|
/**
|
|
@@ -809,7 +809,7 @@ export async function startJob(job) {
|
|
|
809
809
|
: selectAccountWithFallback(preferredProvider, job.id);
|
|
810
810
|
if (!("allExhausted" in sel) && sel.candidate !== undefined) {
|
|
811
811
|
if (sel.crossProvider) {
|
|
812
|
-
const msg = `[
|
|
812
|
+
const msg = `[h2a] account-pool: all ${preferredProvider} accounts exhausted — ` +
|
|
813
813
|
`falling back to ${sel.candidate.provider} (${sel.candidate.label})`;
|
|
814
814
|
process.stderr.write(msg + "\n");
|
|
815
815
|
// Notify the current tmux pane (non-blocking, best-effort).
|
|
@@ -959,11 +959,11 @@ export function resumeThrottledJob(job) {
|
|
|
959
959
|
const sel = selectAccountWithFallback(preferredProvider, job.id);
|
|
960
960
|
const accountEnvOverrides = {};
|
|
961
961
|
if ("allExhausted" in sel) {
|
|
962
|
-
process.stderr.write(`[
|
|
962
|
+
process.stderr.write(`[h2a] account-pool: all accounts exhausted for job ${job.id} — resuming without account override (may throttle again)\n`);
|
|
963
963
|
}
|
|
964
964
|
else if (sel.candidate !== undefined) {
|
|
965
965
|
if (sel.crossProvider) {
|
|
966
|
-
const msg = `[
|
|
966
|
+
const msg = `[h2a] account-pool: all ${preferredProvider} accounts exhausted — falling back to ${sel.candidate.provider} (${sel.candidate.label}) for resume`;
|
|
967
967
|
process.stderr.write(msg + "\n");
|
|
968
968
|
if (process.env.TMUX)
|
|
969
969
|
spawnSync("tmux", ["display-message", msg], { stdio: "ignore" });
|
|
@@ -1047,7 +1047,7 @@ export function resumeThrottledJob(job) {
|
|
|
1047
1047
|
function getConfiguredRemote(overrideUrl) {
|
|
1048
1048
|
const remote = overrideUrl ?? getConfiguredRemoteOptional();
|
|
1049
1049
|
if (!remote) {
|
|
1050
|
-
throw new Error("No remote URL configured. Set one with `
|
|
1050
|
+
throw new Error("No remote URL configured. Set one with `h2a config set <url>` (or `h2a install <url>`) or pass --remote/URL explicitly.");
|
|
1051
1051
|
}
|
|
1052
1052
|
return remote;
|
|
1053
1053
|
}
|
|
@@ -1075,7 +1075,7 @@ function resolveTools(withOpt) {
|
|
|
1075
1075
|
: getDefaultTools();
|
|
1076
1076
|
const { known, unknown } = partitionTools(raw);
|
|
1077
1077
|
if (unknown.length > 0) {
|
|
1078
|
-
process.stderr.write(`[
|
|
1078
|
+
process.stderr.write(`[h2a] ignoring unknown tools: ${unknown.join(", ")} (known: ${KNOWN_TOOLS.join(", ")})\n`);
|
|
1079
1079
|
}
|
|
1080
1080
|
return known;
|
|
1081
1081
|
}
|
|
@@ -1183,16 +1183,16 @@ async function injectLlmMeshGatewayEnv(mode = "auto") {
|
|
|
1183
1183
|
ANTHROPIC_AUTH_TOKEN: result.gatewayToken,
|
|
1184
1184
|
ANTHROPIC_API_KEY: result.gatewayToken,
|
|
1185
1185
|
};
|
|
1186
|
-
process.stderr.write(`[
|
|
1186
|
+
process.stderr.write(`[h2a] llm-mesh: gateway was stopped; started on ${meshEnv.ANTHROPIC_BASE_URL}\n`);
|
|
1187
1187
|
}
|
|
1188
1188
|
catch (err) {
|
|
1189
|
-
process.stderr.write(`[
|
|
1189
|
+
process.stderr.write(`[h2a] llm-mesh: gateway env unavailable (${String(err)}); Claude may ask for login.\n`);
|
|
1190
1190
|
}
|
|
1191
1191
|
}
|
|
1192
1192
|
}
|
|
1193
1193
|
if (!meshEnv) {
|
|
1194
1194
|
if (mode === "gateway") {
|
|
1195
|
-
process.stderr.write("[
|
|
1195
|
+
process.stderr.write("[h2a] llm-mesh: --gw requested but no gateway env is available; continuing direct. Run `h2a llm-mesh start` or enroll an account first.\n");
|
|
1196
1196
|
}
|
|
1197
1197
|
return undefined;
|
|
1198
1198
|
}
|
|
@@ -1202,7 +1202,7 @@ async function injectLlmMeshGatewayEnv(mode = "auto") {
|
|
|
1202
1202
|
for (const [k, v] of Object.entries(meshEnv))
|
|
1203
1203
|
process.env[k] = v;
|
|
1204
1204
|
if (!alreadyCurrent) {
|
|
1205
|
-
process.stderr.write(`[
|
|
1205
|
+
process.stderr.write(`[h2a] llm-mesh: injecting gateway env (${meshEnv.ANTHROPIC_BASE_URL})\n`);
|
|
1206
1206
|
}
|
|
1207
1207
|
return meshEnv.ANTHROPIC_BASE_URL;
|
|
1208
1208
|
}
|
|
@@ -1211,20 +1211,20 @@ async function prepareLlmMeshForRestore(opts = {}) {
|
|
|
1211
1211
|
return;
|
|
1212
1212
|
const config = readLlmMeshConfig();
|
|
1213
1213
|
if (!config?.accounts.length) {
|
|
1214
|
-
process.stderr.write("[
|
|
1214
|
+
process.stderr.write("[h2a] llm-mesh: restore config enabled, but no llm-mesh account is enrolled; Claude may ask for login.\n");
|
|
1215
1215
|
return;
|
|
1216
1216
|
}
|
|
1217
1217
|
const port = config.port ?? 3002;
|
|
1218
1218
|
const pid = readGatewayPid();
|
|
1219
1219
|
if (opts.dryRun) {
|
|
1220
1220
|
process.stderr.write(pid
|
|
1221
|
-
? `[
|
|
1222
|
-
: `[
|
|
1221
|
+
? `[h2a] llm-mesh: restore config enabled; gateway running (pid ${pid}, port ${port})\n`
|
|
1222
|
+
: `[h2a] llm-mesh: restore config enabled; gateway would be started on port ${port}\n`);
|
|
1223
1223
|
return;
|
|
1224
1224
|
}
|
|
1225
1225
|
const gateway = await injectLlmMeshGatewayEnv();
|
|
1226
1226
|
if (gateway) {
|
|
1227
|
-
process.stderr.write(`[
|
|
1227
|
+
process.stderr.write(`[h2a] llm-mesh: restore context active (${gateway})\n`);
|
|
1228
1228
|
}
|
|
1229
1229
|
}
|
|
1230
1230
|
function registryEntryForResumeTarget(target, local) {
|
|
@@ -1275,7 +1275,7 @@ function resolveUrlAndSessionId(first, second) {
|
|
|
1275
1275
|
function setAndReportDefaultRemote(url) {
|
|
1276
1276
|
const configured = setDefaultRemote(url);
|
|
1277
1277
|
ensureManagedTmuxProfile(getTmuxProfileConfig().profile);
|
|
1278
|
-
process.stderr.write(`[
|
|
1278
|
+
process.stderr.write(`[h2a] default remote set to ${configured}\n`);
|
|
1279
1279
|
}
|
|
1280
1280
|
export async function main(argv) {
|
|
1281
1281
|
// ②b — auto-migrate the config home remote-cli/ -> h2a/ on first run (backup +
|
|
@@ -1292,7 +1292,7 @@ export async function main(argv) {
|
|
|
1292
1292
|
if (shouldShowProfileMenu(argv, process.stdin.isTTY === true && process.stdout.isTTY === true)) {
|
|
1293
1293
|
const profile = await promptProfileMenu(process.stdin, process.stderr, process.cwd());
|
|
1294
1294
|
if (!profile) {
|
|
1295
|
-
process.stderr.write("[
|
|
1295
|
+
process.stderr.write("[h2a] no profile selected\n");
|
|
1296
1296
|
return 1;
|
|
1297
1297
|
}
|
|
1298
1298
|
return main([
|
|
@@ -1306,7 +1306,7 @@ export async function main(argv) {
|
|
|
1306
1306
|
const program = new Command();
|
|
1307
1307
|
program
|
|
1308
1308
|
.name("remote")
|
|
1309
|
-
.description("Wrap a local agent CLI (codex/claude/agy/gemini/mistral) and expose its session for
|
|
1309
|
+
.description("Wrap a local agent CLI (codex/claude/agy/gemini/mistral) and expose its session for h2a attach.")
|
|
1310
1310
|
.version("0.0.0");
|
|
1311
1311
|
for (const [profileName, alias] of [
|
|
1312
1312
|
["codex", undefined],
|
|
@@ -1341,7 +1341,7 @@ export async function main(argv) {
|
|
|
1341
1341
|
await runProfile(profileName, { ...rest }, commandArgs ?? []);
|
|
1342
1342
|
}
|
|
1343
1343
|
catch (err) {
|
|
1344
|
-
process.stderr.write(`[
|
|
1344
|
+
process.stderr.write(`[h2a] ${err.message}\n`);
|
|
1345
1345
|
process.exitCode = 1;
|
|
1346
1346
|
}
|
|
1347
1347
|
return;
|
|
@@ -1355,7 +1355,7 @@ export async function main(argv) {
|
|
|
1355
1355
|
: readWorkspaceMarker(process.cwd());
|
|
1356
1356
|
const remote = getConfiguredRemote(remoteOverride ?? marker?.remote);
|
|
1357
1357
|
if (marker) {
|
|
1358
|
-
process.stderr.write(`[
|
|
1358
|
+
process.stderr.write(`[h2a] cwd mapped to ${marker.workspaceId} (reusing workspace)\n`);
|
|
1359
1359
|
}
|
|
1360
1360
|
try {
|
|
1361
1361
|
await runProfile(profileName, {
|
|
@@ -1365,7 +1365,7 @@ export async function main(argv) {
|
|
|
1365
1365
|
}, commandArgs ?? []);
|
|
1366
1366
|
}
|
|
1367
1367
|
catch (err) {
|
|
1368
|
-
process.stderr.write(`[
|
|
1368
|
+
process.stderr.write(`[h2a] ${err.message}\n`);
|
|
1369
1369
|
process.exitCode = 1;
|
|
1370
1370
|
}
|
|
1371
1371
|
});
|
|
@@ -1390,13 +1390,13 @@ export async function main(argv) {
|
|
|
1390
1390
|
const cwd = process.cwd();
|
|
1391
1391
|
const existing = readWorkspaceMarker(cwd);
|
|
1392
1392
|
if (existing) {
|
|
1393
|
-
process.stderr.write(`[
|
|
1393
|
+
process.stderr.write(`[h2a] ${cwd} already mapped to ${existing.workspaceId} (${existing.remote})\n`);
|
|
1394
1394
|
return;
|
|
1395
1395
|
}
|
|
1396
1396
|
const remote = getConfiguredRemote(opts.remote);
|
|
1397
1397
|
const ws = await createWorkspace(remote, opts.name ? { displayName: opts.name } : {});
|
|
1398
1398
|
writeWorkspaceMarker(cwd, { remote, workspaceId: ws.id });
|
|
1399
|
-
process.stderr.write(`[
|
|
1399
|
+
process.stderr.write(`[h2a] linked ${cwd} -> ${ws.id} (wrote .remote/workspace.json)\n`);
|
|
1400
1400
|
});
|
|
1401
1401
|
workspaceCommand
|
|
1402
1402
|
.command("list [url]")
|
|
@@ -1405,7 +1405,7 @@ export async function main(argv) {
|
|
|
1405
1405
|
const remote = getConfiguredRemote(url);
|
|
1406
1406
|
const workspaces = await listWorkspaces(remote);
|
|
1407
1407
|
if (workspaces.length === 0) {
|
|
1408
|
-
process.stderr.write("[
|
|
1408
|
+
process.stderr.write("[h2a] no workspaces\n");
|
|
1409
1409
|
return;
|
|
1410
1410
|
}
|
|
1411
1411
|
const rows = workspaces.map((w) => [w.id, w.createdAt, w.displayName ?? ""].join("\t"));
|
|
@@ -1417,7 +1417,7 @@ export async function main(argv) {
|
|
|
1417
1417
|
.action(() => {
|
|
1418
1418
|
const marker = readWorkspaceMarker(process.cwd());
|
|
1419
1419
|
if (!marker) {
|
|
1420
|
-
process.stdout.write("[
|
|
1420
|
+
process.stdout.write("[h2a] no workspace mapped for this directory (run `h2a workspace link`)\n");
|
|
1421
1421
|
return;
|
|
1422
1422
|
}
|
|
1423
1423
|
process.stdout.write(`workspace: ${marker.workspaceId}\n`);
|
|
@@ -1426,7 +1426,7 @@ export async function main(argv) {
|
|
|
1426
1426
|
const requireMarker = (cwd) => {
|
|
1427
1427
|
const marker = readWorkspaceMarker(cwd);
|
|
1428
1428
|
if (!marker) {
|
|
1429
|
-
throw new Error("No workspace mapped for this directory. Run `
|
|
1429
|
+
throw new Error("No workspace mapped for this directory. Run `h2a workspace link` first.");
|
|
1430
1430
|
}
|
|
1431
1431
|
return marker;
|
|
1432
1432
|
};
|
|
@@ -1437,7 +1437,7 @@ export async function main(argv) {
|
|
|
1437
1437
|
throw new Error(`Workspace ${workspaceId} is held by ${lock.holder} since ${lock.since}. ` +
|
|
1438
1438
|
`Coordinate, or pass --force to override the soft lock.`);
|
|
1439
1439
|
}
|
|
1440
|
-
process.stderr.write(`[
|
|
1440
|
+
process.stderr.write(`[h2a] warning: overriding soft lock held by ${lock.holder} (--force)\n`);
|
|
1441
1441
|
}
|
|
1442
1442
|
};
|
|
1443
1443
|
workspaceCommand
|
|
@@ -1449,9 +1449,9 @@ export async function main(argv) {
|
|
|
1449
1449
|
const marker = requireMarker(cwd);
|
|
1450
1450
|
await guardLock(marker.remote, marker.workspaceId, opts.force ?? false);
|
|
1451
1451
|
try {
|
|
1452
|
-
process.stderr.write(`[
|
|
1452
|
+
process.stderr.write(`[h2a] packing ${cwd} (respecting .gitignore)\n`);
|
|
1453
1453
|
const archive = await buildWorkspaceArchive(cwd);
|
|
1454
|
-
process.stderr.write(`[
|
|
1454
|
+
process.stderr.write(`[h2a] archive: ${(archive.byteLength / 1024).toFixed(0)} KiB -> ${marker.workspaceId}\n`);
|
|
1455
1455
|
// Reuse the --sync path via a throwaway session bound to the workspace:
|
|
1456
1456
|
// the session-agent extracts the archive into the retained PVC, then the
|
|
1457
1457
|
// shell exits and the session auto-cleans (the workspace PVC is kept).
|
|
@@ -1469,7 +1469,7 @@ export async function main(argv) {
|
|
|
1469
1469
|
await attached.finished;
|
|
1470
1470
|
// The pushed tree is now the shared sync base.
|
|
1471
1471
|
writeBaseSnapshot(cwd, archive);
|
|
1472
|
-
process.stderr.write(`[
|
|
1472
|
+
process.stderr.write(`[h2a] pushed ${cwd} to ${marker.workspaceId}\n`);
|
|
1473
1473
|
}
|
|
1474
1474
|
finally {
|
|
1475
1475
|
await releaseWorkspaceLock(marker.remote, marker.workspaceId);
|
|
@@ -1510,7 +1510,7 @@ export async function main(argv) {
|
|
|
1510
1510
|
await stopRemoteSession(marker.remote, session.id, "pull-complete");
|
|
1511
1511
|
}
|
|
1512
1512
|
if (!remoteArchive) {
|
|
1513
|
-
process.stderr.write(`[
|
|
1513
|
+
process.stderr.write(`[h2a] nothing to pull (workspace ${marker.workspaceId} produced no export)\n`);
|
|
1514
1514
|
return;
|
|
1515
1515
|
}
|
|
1516
1516
|
const result = mergeWorkspaceArchive({
|
|
@@ -1518,9 +1518,9 @@ export async function main(argv) {
|
|
|
1518
1518
|
remoteArchive,
|
|
1519
1519
|
baseArchive: readBaseSnapshot(cwd),
|
|
1520
1520
|
});
|
|
1521
|
-
process.stderr.write(`[
|
|
1521
|
+
process.stderr.write(`[h2a] pull: ${result.tookRemote.length} from remote, ${result.keptLocal.length} kept local, ${result.merged.length} merged\n`);
|
|
1522
1522
|
if (result.conflicts.length > 0) {
|
|
1523
|
-
process.stderr.write(`[
|
|
1523
|
+
process.stderr.write(`[h2a] ${result.conflicts.length} conflict(s) (left with markers, resolve then re-run):\n`);
|
|
1524
1524
|
for (const f of result.conflicts)
|
|
1525
1525
|
process.stderr.write(` ${f}\n`);
|
|
1526
1526
|
process.exitCode = 1;
|
|
@@ -1528,7 +1528,7 @@ export async function main(argv) {
|
|
|
1528
1528
|
}
|
|
1529
1529
|
// Clean merge → the remote tree is the new shared base.
|
|
1530
1530
|
writeBaseSnapshot(cwd, remoteArchive);
|
|
1531
|
-
process.stderr.write(`[
|
|
1531
|
+
process.stderr.write(`[h2a] pulled ${marker.workspaceId} into ${cwd}\n`);
|
|
1532
1532
|
if (opts.restoreSessions) {
|
|
1533
1533
|
const onConflict = opts.onConflict === "backup"
|
|
1534
1534
|
? "backup"
|
|
@@ -1547,7 +1547,7 @@ export async function main(argv) {
|
|
|
1547
1547
|
const touched = r.restored.length + r.backedUp.length + r.conflicts.length;
|
|
1548
1548
|
if (touched === 0 && r.keptLocal.length === 0)
|
|
1549
1549
|
continue;
|
|
1550
|
-
process.stderr.write(`[
|
|
1550
|
+
process.stderr.write(`[h2a] sessions(${profile}): ${r.restored.length} restored, ${r.backedUp.length} backed-up, ${r.keptLocal.length} kept, ${r.conflicts.length} conflict\n`);
|
|
1551
1551
|
for (const b of r.backedUp)
|
|
1552
1552
|
process.stderr.write(` backup ${b}\n`);
|
|
1553
1553
|
if (r.conflicts.length > 0) {
|
|
@@ -1557,7 +1557,7 @@ export async function main(argv) {
|
|
|
1557
1557
|
}
|
|
1558
1558
|
}
|
|
1559
1559
|
if (anyConflict) {
|
|
1560
|
-
process.stderr.write(`[
|
|
1560
|
+
process.stderr.write(`[h2a] diverged conversations left untouched. Re-run with --on-conflict backup (keep both) or keep-local.\n`);
|
|
1561
1561
|
process.exitCode = 1;
|
|
1562
1562
|
}
|
|
1563
1563
|
}
|
|
@@ -1579,8 +1579,8 @@ export async function main(argv) {
|
|
|
1579
1579
|
}
|
|
1580
1580
|
const deleted = await deleteWorkspace(remote, id);
|
|
1581
1581
|
process.stderr.write(deleted
|
|
1582
|
-
? `[
|
|
1583
|
-
: `[
|
|
1582
|
+
? `[h2a] deleted workspace ${id}\n`
|
|
1583
|
+
: `[h2a] workspace ${id} not found\n`);
|
|
1584
1584
|
});
|
|
1585
1585
|
workspaceCommand
|
|
1586
1586
|
.command("gc")
|
|
@@ -1605,16 +1605,16 @@ export async function main(argv) {
|
|
|
1605
1605
|
// before the candidate list has been printed (and confirmed).
|
|
1606
1606
|
const dryRun = await requestWorkspaceGc(remote, { olderThanDays });
|
|
1607
1607
|
if (dryRun.candidates.length === 0) {
|
|
1608
|
-
process.stderr.write(`[
|
|
1608
|
+
process.stderr.write(`[h2a] workspace gc: no candidates older than ${olderThanDays} day(s) — nothing to do\n`);
|
|
1609
1609
|
return;
|
|
1610
1610
|
}
|
|
1611
|
-
process.stderr.write(`[
|
|
1611
|
+
process.stderr.write(`[h2a] workspace gc: ${dryRun.candidates.length} candidate(s) older than ${olderThanDays} day(s) (workspaces of known sessions are always kept):\n`);
|
|
1612
1612
|
process.stdout.write([
|
|
1613
1613
|
"ID\tSIZE\tLAST-MODIFIED",
|
|
1614
1614
|
...dryRun.candidates.map((c) => `${c.id}\t${c.sizeH}\t${c.lastModified}`),
|
|
1615
1615
|
].join("\n") + "\n");
|
|
1616
1616
|
if (!opts.apply) {
|
|
1617
|
-
process.stderr.write("[
|
|
1617
|
+
process.stderr.write("[h2a] dry-run only — nothing was deleted. Re-run with --apply to archive these to the volume's .trash/ and remove them.\n");
|
|
1618
1618
|
return;
|
|
1619
1619
|
}
|
|
1620
1620
|
if (!opts.yes) {
|
|
@@ -1623,12 +1623,12 @@ export async function main(argv) {
|
|
|
1623
1623
|
input: process.stdin,
|
|
1624
1624
|
output: process.stderr,
|
|
1625
1625
|
});
|
|
1626
|
-
const answer = (await rl.question(`[
|
|
1626
|
+
const answer = (await rl.question(`[h2a] archive ${dryRun.candidates.length} director(y/ies) to on-volume .trash/ and DELETE them? [y/N] `))
|
|
1627
1627
|
.trim()
|
|
1628
1628
|
.toLowerCase();
|
|
1629
1629
|
rl.close();
|
|
1630
1630
|
if (answer !== "y" && answer !== "yes") {
|
|
1631
|
-
process.stderr.write("[
|
|
1631
|
+
process.stderr.write("[h2a] aborted — nothing was deleted\n");
|
|
1632
1632
|
return;
|
|
1633
1633
|
}
|
|
1634
1634
|
}
|
|
@@ -1639,18 +1639,18 @@ export async function main(argv) {
|
|
|
1639
1639
|
// The janitor re-checks keep-list and age at apply time, so the applied
|
|
1640
1640
|
// set can legitimately be smaller than the dry-run shown above.
|
|
1641
1641
|
for (const c of report.candidates) {
|
|
1642
|
-
process.stderr.write(`[
|
|
1642
|
+
process.stderr.write(`[h2a] archived ${c.id} (${c.sizeH}) -> ${c.archivedTo ?? ".trash/"} then removed\n`);
|
|
1643
1643
|
}
|
|
1644
1644
|
for (const f of report.failed ?? []) {
|
|
1645
|
-
process.stderr.write(`[
|
|
1645
|
+
process.stderr.write(`[h2a] FAILED ${f.id}: ${f.reason} — directory left untouched\n`);
|
|
1646
1646
|
}
|
|
1647
1647
|
if ((report.failed ?? []).length > 0)
|
|
1648
1648
|
process.exitCode = 1;
|
|
1649
1649
|
if (report.candidates.length === 0) {
|
|
1650
|
-
process.stderr.write("[
|
|
1650
|
+
process.stderr.write("[h2a] nothing collected (candidates became active or protected since the dry-run)\n");
|
|
1651
1651
|
}
|
|
1652
1652
|
else {
|
|
1653
|
-
process.stderr.write(`[
|
|
1653
|
+
process.stderr.write(`[h2a] workspace gc done: ${report.candidates.length} archived+removed (recoverable from the volume's .trash/)\n`);
|
|
1654
1654
|
}
|
|
1655
1655
|
});
|
|
1656
1656
|
const authCommand = program
|
|
@@ -1683,7 +1683,7 @@ export async function main(argv) {
|
|
|
1683
1683
|
return;
|
|
1684
1684
|
}
|
|
1685
1685
|
if (!profileName) {
|
|
1686
|
-
throw new Error("Specify a profile (e.g. `
|
|
1686
|
+
throw new Error("Specify a profile (e.g. `h2a auth status codex`) or pass --all.");
|
|
1687
1687
|
}
|
|
1688
1688
|
const profile = coerceCliProfileName(profileName);
|
|
1689
1689
|
if (!profile) {
|
|
@@ -1701,13 +1701,13 @@ export async function main(argv) {
|
|
|
1701
1701
|
}
|
|
1702
1702
|
const loginCommand = getLoginCommand(profile);
|
|
1703
1703
|
if (!loginCommand) {
|
|
1704
|
-
process.stderr.write(`[
|
|
1704
|
+
process.stderr.write(`[h2a] ${profile} has no scripted login. Run \`${profile}\` directly and complete its sign-in flow (browser / SSH-mode URL), then \`h2a auth status ${profile}\`.\n`);
|
|
1705
1705
|
return;
|
|
1706
1706
|
}
|
|
1707
|
-
process.stderr.write(`[
|
|
1707
|
+
process.stderr.write(`[h2a] running ${loginCommand.command} ${loginCommand.args.join(" ")}\n`);
|
|
1708
1708
|
const code = await runInteractiveLogin(loginCommand);
|
|
1709
1709
|
if (code !== 0) {
|
|
1710
|
-
process.stderr.write(`[
|
|
1710
|
+
process.stderr.write(`[h2a] login exited with code ${code}; check the output above.\n`);
|
|
1711
1711
|
process.exitCode = code;
|
|
1712
1712
|
return;
|
|
1713
1713
|
}
|
|
@@ -1732,7 +1732,7 @@ export async function main(argv) {
|
|
|
1732
1732
|
if (opts.authRefresh !== false) {
|
|
1733
1733
|
const fresh = await ensureProfileAuthFresh(resolved);
|
|
1734
1734
|
if (fresh.checked)
|
|
1735
|
-
process.stderr.write(`[
|
|
1735
|
+
process.stderr.write(`[h2a] auth status ok: ${fresh.command}\n`);
|
|
1736
1736
|
}
|
|
1737
1737
|
await softRefreshSession(sessionId, resolved);
|
|
1738
1738
|
return;
|
|
@@ -1790,14 +1790,14 @@ export async function main(argv) {
|
|
|
1790
1790
|
.description("Store the bearer token sent as Authorization on control-plane requests (overridden by $REMOTE_TOKEN)")
|
|
1791
1791
|
.action((value) => {
|
|
1792
1792
|
setToken(value);
|
|
1793
|
-
process.stderr.write("[
|
|
1793
|
+
process.stderr.write("[h2a] stored bearer token\n");
|
|
1794
1794
|
});
|
|
1795
1795
|
configCommand
|
|
1796
1796
|
.command("target <target>")
|
|
1797
1797
|
.description("Set the default session target label (where the workload runs), e.g. scaleway-kapsule")
|
|
1798
1798
|
.action((target) => {
|
|
1799
1799
|
setDefaultTarget(target);
|
|
1800
|
-
process.stderr.write(`[
|
|
1800
|
+
process.stderr.write(`[h2a] default target set to ${target}\n`);
|
|
1801
1801
|
});
|
|
1802
1802
|
configCommand
|
|
1803
1803
|
.command("tools <list>")
|
|
@@ -1811,10 +1811,10 @@ export async function main(argv) {
|
|
|
1811
1811
|
.filter(Boolean);
|
|
1812
1812
|
const { known, unknown } = partitionTools(requested);
|
|
1813
1813
|
if (unknown.length > 0) {
|
|
1814
|
-
process.stderr.write(`[
|
|
1814
|
+
process.stderr.write(`[h2a] unknown tools ignored: ${unknown.join(", ")}\n`);
|
|
1815
1815
|
}
|
|
1816
1816
|
setDefaultTools(known);
|
|
1817
|
-
process.stderr.write(`[
|
|
1817
|
+
process.stderr.write(`[h2a] default tools: ${known.length > 0 ? known.join(", ") : "(none)"}\n`);
|
|
1818
1818
|
});
|
|
1819
1819
|
configCommand
|
|
1820
1820
|
.command("tmux-profile <name>")
|
|
@@ -1823,21 +1823,21 @@ export async function main(argv) {
|
|
|
1823
1823
|
setTmuxProfileConfig({ profile: name });
|
|
1824
1824
|
const profile = getTmuxProfileConfig().profile;
|
|
1825
1825
|
ensureManagedTmuxProfile(profile);
|
|
1826
|
-
process.stderr.write(`[
|
|
1826
|
+
process.stderr.write(`[h2a] tmux profile set to ${profile}\n`);
|
|
1827
1827
|
});
|
|
1828
1828
|
configCommand
|
|
1829
1829
|
.command("clear")
|
|
1830
1830
|
.description("Clear default remote URL")
|
|
1831
1831
|
.action(() => {
|
|
1832
1832
|
clearDefaultRemote();
|
|
1833
|
-
process.stderr.write("[
|
|
1833
|
+
process.stderr.write("[h2a] cleared default remote\n");
|
|
1834
1834
|
});
|
|
1835
1835
|
configCommand
|
|
1836
1836
|
.command("show")
|
|
1837
1837
|
.description("Display configured default remote URL, managed tmux profile, and tunnel")
|
|
1838
1838
|
.action(() => {
|
|
1839
1839
|
const remote = getDefaultRemote();
|
|
1840
|
-
process.stdout.write(remote ? `${remote}\n` : "[
|
|
1840
|
+
process.stdout.write(remote ? `${remote}\n` : "[h2a] no default remote configured\n");
|
|
1841
1841
|
process.stdout.write(`target: ${getDefaultTarget()}\n`);
|
|
1842
1842
|
const tools = getDefaultTools();
|
|
1843
1843
|
if (tools.length > 0)
|
|
@@ -1867,7 +1867,7 @@ export async function main(argv) {
|
|
|
1867
1867
|
...(opts.kubeconfig ? { kubeconfig: opts.kubeconfig } : {}),
|
|
1868
1868
|
};
|
|
1869
1869
|
setTunnel(tunnel);
|
|
1870
|
-
process.stderr.write(`[
|
|
1870
|
+
process.stderr.write(`[h2a] tunnel configured: kubectl -n ${tunnel.namespace} port-forward svc/${tunnel.service} ${tunnel.localPort}:${tunnel.remotePort}\n`);
|
|
1871
1871
|
});
|
|
1872
1872
|
// ---------------------------------------------------------------------------
|
|
1873
1873
|
// connect / disconnect — manage the on-demand tunnel
|
|
@@ -1879,7 +1879,7 @@ export async function main(argv) {
|
|
|
1879
1879
|
.action(async (opts) => {
|
|
1880
1880
|
const url = getConfiguredRemote(opts.remote);
|
|
1881
1881
|
await ensureConnected(url);
|
|
1882
|
-
process.stderr.write(`[
|
|
1882
|
+
process.stderr.write(`[h2a] connected: ${url}\n`);
|
|
1883
1883
|
});
|
|
1884
1884
|
program
|
|
1885
1885
|
.command("disconnect")
|
|
@@ -1887,8 +1887,8 @@ export async function main(argv) {
|
|
|
1887
1887
|
.action(() => {
|
|
1888
1888
|
const stopped = stopTunnel();
|
|
1889
1889
|
process.stderr.write(stopped
|
|
1890
|
-
? "[
|
|
1891
|
-
: "[
|
|
1890
|
+
? "[h2a] tunnel closed\n"
|
|
1891
|
+
: "[h2a] no managed tunnel was running\n");
|
|
1892
1892
|
});
|
|
1893
1893
|
program
|
|
1894
1894
|
.command("status")
|
|
@@ -1960,7 +1960,7 @@ export async function main(argv) {
|
|
|
1960
1960
|
if (sessionId) {
|
|
1961
1961
|
const entries = transmittedSecrets(sessionId);
|
|
1962
1962
|
if (entries === undefined) {
|
|
1963
|
-
process.stdout.write(`[
|
|
1963
|
+
process.stdout.write(`[h2a] cannot read secrets for ${sessionId} (no tunnel configured, or no auth Secret)\n`);
|
|
1964
1964
|
return;
|
|
1965
1965
|
}
|
|
1966
1966
|
process.stdout.write(`Secrets transmitted to ${sessionId} (live, names only):\n`);
|
|
@@ -1986,7 +1986,7 @@ export async function main(argv) {
|
|
|
1986
1986
|
for (const s of sessions) {
|
|
1987
1987
|
process.stdout.write(` ${projectName(s).padEnd(22)} ${secretsSummary(s.id)} (${s.id})\n`);
|
|
1988
1988
|
}
|
|
1989
|
-
process.stdout.write("\n(⚠ = account-wide cloud cred. Detail:
|
|
1989
|
+
process.stdout.write("\n(⚠ = account-wide cloud cred. Detail: h2a secrets status <sessionId>.)\n");
|
|
1990
1990
|
});
|
|
1991
1991
|
// ---------------------------------------------------------------------------
|
|
1992
1992
|
// diff — is the remote session aligned with the local one?
|
|
@@ -1999,7 +1999,7 @@ export async function main(argv) {
|
|
|
1999
1999
|
.option("--remote <url>", "control-plane URL (defaults to configured remote)")
|
|
2000
2000
|
.action(async (sessionId, opts) => {
|
|
2001
2001
|
if (opts.session && opts.files) {
|
|
2002
|
-
process.stderr.write("[
|
|
2002
|
+
process.stderr.write("[h2a] --session and --files are mutually exclusive\n");
|
|
2003
2003
|
process.exitCode = 1;
|
|
2004
2004
|
return;
|
|
2005
2005
|
}
|
|
@@ -2010,7 +2010,7 @@ export async function main(argv) {
|
|
|
2010
2010
|
? all.filter((s) => s.id === sessionId)
|
|
2011
2011
|
: all;
|
|
2012
2012
|
if (sessions.length === 0) {
|
|
2013
|
-
process.stdout.write("[
|
|
2013
|
+
process.stdout.write("[h2a] no matching session\n");
|
|
2014
2014
|
return;
|
|
2015
2015
|
}
|
|
2016
2016
|
const icon = {
|
|
@@ -2055,12 +2055,12 @@ export async function main(argv) {
|
|
|
2055
2055
|
.option("--interval <seconds>", "seconds between sync passes in --watch mode (default: 30)", (v) => Number(v))
|
|
2056
2056
|
.action(async (sessionId, opts) => {
|
|
2057
2057
|
if (opts.files) {
|
|
2058
|
-
process.stderr.write("[
|
|
2058
|
+
process.stderr.write("[h2a] files: utilise git (commit/push des deux côtés) — non automatisé\n");
|
|
2059
2059
|
process.exitCode = 1;
|
|
2060
2060
|
return;
|
|
2061
2061
|
}
|
|
2062
2062
|
if (opts.session !== "push" && opts.session !== "pull") {
|
|
2063
|
-
process.stderr.write("[
|
|
2063
|
+
process.stderr.write("[h2a] usage: h2a sync <sessionId> --session <push|pull>\n");
|
|
2064
2064
|
process.exitCode = 1;
|
|
2065
2065
|
return;
|
|
2066
2066
|
}
|
|
@@ -2069,7 +2069,7 @@ export async function main(argv) {
|
|
|
2069
2069
|
await ensureConnected(url);
|
|
2070
2070
|
const session = (await listRemoteSessions(url)).find((s) => s.id === sessionId);
|
|
2071
2071
|
if (!session?.workspacePath) {
|
|
2072
|
-
process.stderr.write(`[
|
|
2072
|
+
process.stderr.write(`[h2a] no session ${sessionId} with a workspace path\n`);
|
|
2073
2073
|
process.exitCode = 1;
|
|
2074
2074
|
return;
|
|
2075
2075
|
}
|
|
@@ -2082,23 +2082,23 @@ export async function main(argv) {
|
|
|
2082
2082
|
force: opts.force ?? false,
|
|
2083
2083
|
});
|
|
2084
2084
|
if (!result.ok) {
|
|
2085
|
-
process.stderr.write(`[
|
|
2085
|
+
process.stderr.write(`[h2a] refused: ${result.reason}\n`);
|
|
2086
2086
|
return false;
|
|
2087
2087
|
}
|
|
2088
2088
|
if (result.backup) {
|
|
2089
|
-
process.stderr.write(`[
|
|
2089
|
+
process.stderr.write(`[h2a] backup: ${result.backup}\n`);
|
|
2090
2090
|
}
|
|
2091
2091
|
const lines = direction === "pull" ? result.lines.remote : result.lines.local;
|
|
2092
2092
|
const mode = result.incremental ? " [incremental]" : "";
|
|
2093
|
-
process.stderr.write(`[
|
|
2093
|
+
process.stderr.write(`[h2a] ${direction === "pull" ? "pulled" : "pushed"} ${result.convId} (${lines} lines) → ${result.written}${mode}\n`);
|
|
2094
2094
|
if (direction === "push") {
|
|
2095
|
-
process.stderr.write(`[
|
|
2095
|
+
process.stderr.write(`[h2a] not relaunching the Pod CLI — relance la session pour charger : h2a refresh ${sessionId} --soft\n`);
|
|
2096
2096
|
}
|
|
2097
2097
|
return true;
|
|
2098
2098
|
};
|
|
2099
2099
|
if (opts.watch) {
|
|
2100
2100
|
const intervalSec = opts.interval ?? 30;
|
|
2101
|
-
process.stderr.write(`[
|
|
2101
|
+
process.stderr.write(`[h2a] sync --watch: syncing every ${intervalSec}s (Ctrl-C to stop)\n`);
|
|
2102
2102
|
let stopped = false;
|
|
2103
2103
|
let wake;
|
|
2104
2104
|
const onSigint = () => {
|
|
@@ -2108,12 +2108,12 @@ export async function main(argv) {
|
|
|
2108
2108
|
process.on("SIGINT", onSigint);
|
|
2109
2109
|
try {
|
|
2110
2110
|
while (!stopped) {
|
|
2111
|
-
process.stderr.write(`[
|
|
2111
|
+
process.stderr.write(`[h2a] sync pass — ${new Date().toISOString()}\n`);
|
|
2112
2112
|
try {
|
|
2113
2113
|
runOnce();
|
|
2114
2114
|
}
|
|
2115
2115
|
catch (err) {
|
|
2116
|
-
process.stderr.write(`[
|
|
2116
|
+
process.stderr.write(`[h2a] sync pass error: ${err.message}\n`);
|
|
2117
2117
|
}
|
|
2118
2118
|
if (stopped)
|
|
2119
2119
|
break;
|
|
@@ -2130,7 +2130,7 @@ export async function main(argv) {
|
|
|
2130
2130
|
finally {
|
|
2131
2131
|
process.removeListener("SIGINT", onSigint);
|
|
2132
2132
|
}
|
|
2133
|
-
process.stderr.write("[
|
|
2133
|
+
process.stderr.write("[h2a] sync --watch stopped\n");
|
|
2134
2134
|
return;
|
|
2135
2135
|
}
|
|
2136
2136
|
const ok = runOnce();
|
|
@@ -2208,7 +2208,7 @@ export async function main(argv) {
|
|
|
2208
2208
|
program
|
|
2209
2209
|
.command("sync-files")
|
|
2210
2210
|
.description("Push the current git workspace to the session pod incrementally (git bundle on first push, diff on subsequent). " +
|
|
2211
|
-
"Distinct from `
|
|
2211
|
+
"Distinct from `h2a sync` which syncs conversations.")
|
|
2212
2212
|
.option("--remote <url>", "control-plane URL (defaults to configured remote)")
|
|
2213
2213
|
.option("--session <id>", "session id to sync into (defaults to .remote/workspace.json marker)")
|
|
2214
2214
|
.option("--dry-run", "show what would be sent without uploading")
|
|
@@ -2216,13 +2216,13 @@ export async function main(argv) {
|
|
|
2216
2216
|
const cwd = process.cwd();
|
|
2217
2217
|
const { isGitRepo: checkGit, getHeadSha, buildGitBundle, buildIncrementalManifest, buildUntrackedTarball, } = await import("./workspace-sync-incremental.js");
|
|
2218
2218
|
if (!checkGit(cwd)) {
|
|
2219
|
-
process.stderr.write("[
|
|
2219
|
+
process.stderr.write("[h2a] sync-files: current directory is not a git repo\n");
|
|
2220
2220
|
process.exitCode = 1;
|
|
2221
2221
|
return;
|
|
2222
2222
|
}
|
|
2223
2223
|
const headSha = getHeadSha(cwd);
|
|
2224
2224
|
if (!headSha) {
|
|
2225
|
-
process.stderr.write("[
|
|
2225
|
+
process.stderr.write("[h2a] sync-files: cannot resolve HEAD — make at least one commit\n");
|
|
2226
2226
|
process.exitCode = 1;
|
|
2227
2227
|
return;
|
|
2228
2228
|
}
|
|
@@ -2231,7 +2231,7 @@ export async function main(argv) {
|
|
|
2231
2231
|
// Resolve session id: explicit --session, or from marker
|
|
2232
2232
|
const sessionId = opts.session ?? marker?.workspaceId;
|
|
2233
2233
|
if (!sessionId) {
|
|
2234
|
-
process.stderr.write("[
|
|
2234
|
+
process.stderr.write("[h2a] sync-files: no session id — pass --session or run `h2a workspace link` first\n");
|
|
2235
2235
|
process.exitCode = 1;
|
|
2236
2236
|
return;
|
|
2237
2237
|
}
|
|
@@ -2245,18 +2245,18 @@ export async function main(argv) {
|
|
|
2245
2245
|
}
|
|
2246
2246
|
if (opts.dryRun) {
|
|
2247
2247
|
if (baseSha) {
|
|
2248
|
-
process.stderr.write(`[
|
|
2248
|
+
process.stderr.write(`[h2a] sync-files (dry-run): incremental push HEAD=${headSha} base=${baseSha}\n`);
|
|
2249
2249
|
}
|
|
2250
2250
|
else {
|
|
2251
|
-
process.stderr.write(`[
|
|
2251
|
+
process.stderr.write(`[h2a] sync-files (dry-run): bootstrap push HEAD=${headSha} (no base on CP)\n`);
|
|
2252
2252
|
}
|
|
2253
2253
|
return;
|
|
2254
2254
|
}
|
|
2255
2255
|
if (!baseSha) {
|
|
2256
2256
|
// Bootstrap: upload full git bundle.
|
|
2257
|
-
process.stderr.write(`[
|
|
2257
|
+
process.stderr.write(`[h2a] sync-files: bootstrap — building git bundle (HEAD=${headSha})\n`);
|
|
2258
2258
|
const bundle = buildGitBundle(cwd);
|
|
2259
|
-
process.stderr.write(`[
|
|
2259
|
+
process.stderr.write(`[h2a] sync-files: bundle ${(bundle.byteLength / 1024).toFixed(0)} KiB → ${sessionId}\n`);
|
|
2260
2260
|
const uploadRes = await fetch(`${remote.replace(/\/$/, "")}/sessions/${sessionId}/workspace/incremental/bundle`, {
|
|
2261
2261
|
method: "POST",
|
|
2262
2262
|
headers: {
|
|
@@ -2287,7 +2287,7 @@ export async function main(argv) {
|
|
|
2287
2287
|
if (!manifestRes.ok) {
|
|
2288
2288
|
throw new Error(`manifest record failed: ${manifestRes.status} ${manifestRes.statusText}`);
|
|
2289
2289
|
}
|
|
2290
|
-
process.stderr.write(`[
|
|
2290
|
+
process.stderr.write(`[h2a] sync-files: bootstrap complete, base=${headSha}\n`);
|
|
2291
2291
|
const { writeSyncStatus, emptyMetrics: em } = await import("./sync-status.js");
|
|
2292
2292
|
writeSyncStatus(sessionId, {
|
|
2293
2293
|
state: "synced",
|
|
@@ -2300,11 +2300,11 @@ export async function main(argv) {
|
|
|
2300
2300
|
}
|
|
2301
2301
|
else {
|
|
2302
2302
|
// Incremental push.
|
|
2303
|
-
process.stderr.write(`[
|
|
2303
|
+
process.stderr.write(`[h2a] sync-files: incremental push base=${baseSha} → HEAD=${headSha}\n`);
|
|
2304
2304
|
const manifest = buildIncrementalManifest(cwd, baseSha);
|
|
2305
2305
|
const untrackedPaths = manifest.untrackedManifest.map((e) => e.path);
|
|
2306
2306
|
const trackedBytes = Buffer.from(manifest.tracked, "base64").byteLength;
|
|
2307
|
-
process.stderr.write(`[
|
|
2307
|
+
process.stderr.write(`[h2a] sync-files: tracked diff ${(trackedBytes / 1024).toFixed(1)} KiB, ${untrackedPaths.length} untracked file(s)\n`);
|
|
2308
2308
|
// Upload manifest.
|
|
2309
2309
|
const manifestRes = await fetch(`${remote.replace(/\/$/, "")}/sessions/${sessionId}/workspace/incremental`, {
|
|
2310
2310
|
method: "POST",
|
|
@@ -2332,7 +2332,7 @@ export async function main(argv) {
|
|
|
2332
2332
|
throw new Error(`untracked upload failed: ${tarRes.status} ${tarRes.statusText}`);
|
|
2333
2333
|
}
|
|
2334
2334
|
}
|
|
2335
|
-
process.stderr.write(`[
|
|
2335
|
+
process.stderr.write(`[h2a] sync-files: incremental complete, base=${headSha}\n`);
|
|
2336
2336
|
const { writeSyncStatus: wss, emptyMetrics: em2 } = await import("./sync-status.js");
|
|
2337
2337
|
wss(sessionId, {
|
|
2338
2338
|
state: "synced",
|
|
@@ -2355,7 +2355,7 @@ export async function main(argv) {
|
|
|
2355
2355
|
.action(async (sessionId, podPort, localPort, opts) => {
|
|
2356
2356
|
const port = Number(podPort);
|
|
2357
2357
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
2358
|
-
process.stderr.write(`[
|
|
2358
|
+
process.stderr.write(`[h2a] invalid pod port "${podPort}"\n`);
|
|
2359
2359
|
process.exitCode = 1;
|
|
2360
2360
|
return;
|
|
2361
2361
|
}
|
|
@@ -2363,7 +2363,7 @@ export async function main(argv) {
|
|
|
2363
2363
|
if (localPort !== undefined) {
|
|
2364
2364
|
local = Number(localPort);
|
|
2365
2365
|
if (!Number.isInteger(local) || local < 1 || local > 65535) {
|
|
2366
|
-
process.stderr.write(`[
|
|
2366
|
+
process.stderr.write(`[h2a] invalid local port "${localPort}"\n`);
|
|
2367
2367
|
process.exitCode = 1;
|
|
2368
2368
|
return;
|
|
2369
2369
|
}
|
|
@@ -2387,7 +2387,7 @@ export async function main(argv) {
|
|
|
2387
2387
|
browserCommand
|
|
2388
2388
|
.command("open <sessionId>")
|
|
2389
2389
|
.description("Print the steps to open the headful browser view for a session: the " +
|
|
2390
|
-
"`
|
|
2390
|
+
"`h2a forward` command to run and the token-gated noVNC URL to open. " +
|
|
2391
2391
|
"Default exposure is session-private (owner only, token-gated) and interactive (you drive the 2FA).")
|
|
2392
2392
|
.option("--local-port <port>", "local port to bind the forward to")
|
|
2393
2393
|
.option("--policy <policy>", "uat exposure policy: operator-only | session-private | public-expiring (default session-private)")
|
|
@@ -2400,7 +2400,7 @@ export async function main(argv) {
|
|
|
2400
2400
|
if (!Number.isInteger(localPort) ||
|
|
2401
2401
|
localPort < 1 ||
|
|
2402
2402
|
localPort > 65535) {
|
|
2403
|
-
process.stderr.write(`[
|
|
2403
|
+
process.stderr.write(`[h2a] invalid local port "${opts.localPort}"\n`);
|
|
2404
2404
|
process.exitCode = 1;
|
|
2405
2405
|
return;
|
|
2406
2406
|
}
|
|
@@ -2410,7 +2410,7 @@ export async function main(argv) {
|
|
|
2410
2410
|
policy !== "operator-only" &&
|
|
2411
2411
|
policy !== "session-private" &&
|
|
2412
2412
|
policy !== "public-expiring") {
|
|
2413
|
-
process.stderr.write(`[
|
|
2413
|
+
process.stderr.write(`[h2a] invalid --policy "${policy}" (operator-only | session-private | public-expiring)\n`);
|
|
2414
2414
|
process.exitCode = 1;
|
|
2415
2415
|
return;
|
|
2416
2416
|
}
|
|
@@ -2418,7 +2418,7 @@ export async function main(argv) {
|
|
|
2418
2418
|
if (opts.ttl !== undefined) {
|
|
2419
2419
|
ttlMs = Number(opts.ttl);
|
|
2420
2420
|
if (!Number.isFinite(ttlMs) || ttlMs <= 0) {
|
|
2421
|
-
process.stderr.write(`[
|
|
2421
|
+
process.stderr.write(`[h2a] invalid --ttl "${opts.ttl}"\n`);
|
|
2422
2422
|
process.exitCode = 1;
|
|
2423
2423
|
return;
|
|
2424
2424
|
}
|
|
@@ -2431,7 +2431,7 @@ export async function main(argv) {
|
|
|
2431
2431
|
...(opts.viewOnly ? { interactive: false } : {}),
|
|
2432
2432
|
});
|
|
2433
2433
|
if (!plan.ok) {
|
|
2434
|
-
process.stderr.write(`[
|
|
2434
|
+
process.stderr.write(`[h2a] ${plan.reason}\n`);
|
|
2435
2435
|
process.exitCode = 1;
|
|
2436
2436
|
return;
|
|
2437
2437
|
}
|
|
@@ -2454,7 +2454,7 @@ export async function main(argv) {
|
|
|
2454
2454
|
.option("-r, --resume [convId]", "resume the most recent (or a specific) conversation on the remote CLI")
|
|
2455
2455
|
.option("--no-attach", "create the remote session without hijacking this terminal; print the attach command instead (for bulk migration / reconnecting your own terminal)")
|
|
2456
2456
|
.option("--reconnect", "revive a session on the EXISTING workspace without re-pushing files (preserves work done remotely) — use after an accidental exit (Ctrl+C/Ctrl+D) to bring the session back from its retained PVC with path parity + --resume")
|
|
2457
|
-
.option("--with <tools>", `comma-separated tool CLIs whose local auth to also bundle into the Pod (known: ${KNOWN_TOOLS.join(", ")}); defaults to '
|
|
2457
|
+
.option("--with <tools>", `comma-separated tool CLIs whose local auth to also bundle into the Pod (known: ${KNOWN_TOOLS.join(", ")}); defaults to 'h2a config tools'`)
|
|
2458
2458
|
.option("--force", "migrate even if the conversation already has a live writer (risk: conversation .jsonl corruption)")
|
|
2459
2459
|
.action(async (profile, opts) => {
|
|
2460
2460
|
const remoteUrl = getConfiguredRemote(opts.remote);
|
|
@@ -2503,7 +2503,7 @@ export async function main(argv) {
|
|
|
2503
2503
|
const now = Date.now();
|
|
2504
2504
|
const candidates = listMigrationCandidates();
|
|
2505
2505
|
if (candidates.length === 0) {
|
|
2506
|
-
process.stdout.write("[
|
|
2506
|
+
process.stdout.write("[h2a] no local claude sessions found under ~/.claude/projects\n");
|
|
2507
2507
|
return;
|
|
2508
2508
|
}
|
|
2509
2509
|
process.stdout.write("# AGE SIZE CONVS GIT MIGRATED PATH\n");
|
|
@@ -2517,8 +2517,8 @@ export async function main(argv) {
|
|
|
2517
2517
|
const missing = c.exists ? "" : " (dir missing)";
|
|
2518
2518
|
process.stdout.write(`${n} ${age} ${size} ${convs} ${git} ${mig} ${c.path}${missing}\n`);
|
|
2519
2519
|
});
|
|
2520
|
-
process.stdout.write("\nMigrate one: cd <path> &&
|
|
2521
|
-
"Pick interactively:
|
|
2520
|
+
process.stdout.write("\nMigrate one: cd <path> && h2a migrate forward claude --resume\n" +
|
|
2521
|
+
"Pick interactively: h2a migrate pick\n");
|
|
2522
2522
|
});
|
|
2523
2523
|
migrateCommand
|
|
2524
2524
|
.command("pick")
|
|
@@ -2526,7 +2526,7 @@ export async function main(argv) {
|
|
|
2526
2526
|
.option("--remote <url>", "control-plane URL (defaults to configured remote)")
|
|
2527
2527
|
.option("--profile <profile>", "CLI profile to start remotely", "claude")
|
|
2528
2528
|
.option("--no-resume", "do not resume the conversation on the remote CLI")
|
|
2529
|
-
.option("--with <tools>", `comma-separated tool CLIs whose auth to bundle (known: ${KNOWN_TOOLS.join(", ")}); defaults to '
|
|
2529
|
+
.option("--with <tools>", `comma-separated tool CLIs whose auth to bundle (known: ${KNOWN_TOOLS.join(", ")}); defaults to 'h2a config tools'`)
|
|
2530
2530
|
.action(async (opts) => {
|
|
2531
2531
|
const remoteUrl = getConfiguredRemote(opts.remote);
|
|
2532
2532
|
await ensureConnected(remoteUrl);
|
|
@@ -2535,7 +2535,7 @@ export async function main(argv) {
|
|
|
2535
2535
|
const now = Date.now();
|
|
2536
2536
|
const candidates = listMigrationCandidates().filter((c) => c.exists && c.isGit);
|
|
2537
2537
|
if (candidates.length === 0) {
|
|
2538
|
-
process.stdout.write("[
|
|
2538
|
+
process.stdout.write("[h2a] no migratable git-backed sessions found\n");
|
|
2539
2539
|
return;
|
|
2540
2540
|
}
|
|
2541
2541
|
process.stdout.write("Local sessions you can migrate:\n\n");
|
|
@@ -2559,7 +2559,7 @@ export async function main(argv) {
|
|
|
2559
2559
|
.filter((n) => Number.isInteger(n) && n >= 1 && n <= candidates.length)
|
|
2560
2560
|
.map((n) => candidates[n - 1]);
|
|
2561
2561
|
if (chosen.length === 0) {
|
|
2562
|
-
process.stdout.write("[
|
|
2562
|
+
process.stdout.write("[h2a] nothing selected — cancelled\n");
|
|
2563
2563
|
return;
|
|
2564
2564
|
}
|
|
2565
2565
|
for (const c of chosen) {
|
|
@@ -2575,7 +2575,7 @@ export async function main(argv) {
|
|
|
2575
2575
|
});
|
|
2576
2576
|
}
|
|
2577
2577
|
catch (err) {
|
|
2578
|
-
process.stderr.write(`[
|
|
2578
|
+
process.stderr.write(`[h2a] failed to migrate ${c.path}: ${String(err)}\n`);
|
|
2579
2579
|
}
|
|
2580
2580
|
}
|
|
2581
2581
|
});
|
|
@@ -2604,15 +2604,15 @@ export async function main(argv) {
|
|
|
2604
2604
|
if (match) {
|
|
2605
2605
|
const lastWs = match.wsHistory[match.wsHistory.length - 1];
|
|
2606
2606
|
if (lastWs && !workspaceId) {
|
|
2607
|
-
process.stderr.write(`[
|
|
2607
|
+
process.stderr.write(`[h2a] --lineage ${lineageId} → workspace history includes ${lastWs}\n`);
|
|
2608
2608
|
}
|
|
2609
2609
|
if (match.incarnation.remote?.sessionId) {
|
|
2610
2610
|
knownSessionId = match.incarnation.remote.sessionId;
|
|
2611
|
-
process.stderr.write(`[
|
|
2611
|
+
process.stderr.write(`[h2a] targeting session ${knownSessionId} (from lineage)\n`);
|
|
2612
2612
|
}
|
|
2613
2613
|
}
|
|
2614
2614
|
else {
|
|
2615
|
-
process.stderr.write(`[
|
|
2615
|
+
process.stderr.write(`[h2a] warning: lineage ${lineageId} not found locally; proceeding without lineage filter\n`);
|
|
2616
2616
|
}
|
|
2617
2617
|
}
|
|
2618
2618
|
await migrateBack({
|
|
@@ -2637,14 +2637,14 @@ export async function main(argv) {
|
|
|
2637
2637
|
// Step 1: checkReadiness
|
|
2638
2638
|
const readiness = checkReadiness({ cwd, profile: resolvedProfile });
|
|
2639
2639
|
if (readiness.blockers.length > 0) {
|
|
2640
|
-
process.stderr.write(`[
|
|
2640
|
+
process.stderr.write(`[h2a] migration blocked:\n${readiness.blockers.map((b) => ` • ${b}`).join("\n")}\n`);
|
|
2641
2641
|
process.exitCode = 1;
|
|
2642
2642
|
return;
|
|
2643
2643
|
}
|
|
2644
2644
|
if (readiness.warnings.length > 0) {
|
|
2645
|
-
process.stderr.write(`[
|
|
2645
|
+
process.stderr.write(`[h2a] readiness warnings:\n${readiness.warnings.map((w) => ` ⚠ ${w}`).join("\n")}\n`);
|
|
2646
2646
|
}
|
|
2647
|
-
process.stderr.write(`[
|
|
2647
|
+
process.stderr.write(`[h2a] readiness ok (mode: ${readiness.mode}, pending: ${readiness.pending.files} files / ${(readiness.pending.bytes / 1024).toFixed(0)} KiB)\n`);
|
|
2648
2648
|
// Step 2: compute durable workspace id (best-effort)
|
|
2649
2649
|
let wsHex = "ws:unknown";
|
|
2650
2650
|
try {
|
|
@@ -2685,21 +2685,21 @@ export async function main(argv) {
|
|
|
2685
2685
|
const TTL_MS = 300_000; // 5 minutes
|
|
2686
2686
|
const leaseResult = acquireLease(lineageId, thisInstance, localSlug, "local", TTL_MS, cwd);
|
|
2687
2687
|
if ("error" in leaseResult) {
|
|
2688
|
-
process.stderr.write(`[
|
|
2689
|
-
`[
|
|
2688
|
+
process.stderr.write(`[h2a] lease conflict: lineage ${lineageId} is held by ${leaseResult.current.holder} (${leaseResult.current.location}) until ${leaseResult.current.expiresAt}\n` +
|
|
2689
|
+
`[h2a] wait for the lease to expire or use \`h2a migrate to-remote --force\` (not yet implemented)\n`);
|
|
2690
2690
|
process.exitCode = 1;
|
|
2691
2691
|
return;
|
|
2692
2692
|
}
|
|
2693
|
-
process.stderr.write(`[
|
|
2693
|
+
process.stderr.write(`[h2a] lineage: ${lineageId} (epoch ${leaseResult.epoch}, holder ${thisInstance})\n`);
|
|
2694
2694
|
if (opts.dryRun) {
|
|
2695
|
-
process.stderr.write(`[
|
|
2696
|
-
process.stderr.write(`[
|
|
2695
|
+
process.stderr.write(`[h2a] --dry-run: would call migrate forward ${resolvedProfile} (mode: ${readiness.mode})\n`);
|
|
2696
|
+
process.stderr.write(`[h2a] dry-run complete — no session created, lease NOT handed off\n`);
|
|
2697
2697
|
// Release the lease on dry-run (we only acquired it to check)
|
|
2698
2698
|
return;
|
|
2699
2699
|
}
|
|
2700
2700
|
// Step 5: suspend local incarnation BEFORE handoff
|
|
2701
2701
|
suspendLocalIncarnation(lineageId, cwd);
|
|
2702
|
-
process.stderr.write(`[
|
|
2702
|
+
process.stderr.write(`[h2a] local incarnation suspended (sentinel written)\n`);
|
|
2703
2703
|
// Step 6: call existing migrate forward
|
|
2704
2704
|
const remoteUrl = getConfiguredRemote(opts.remote);
|
|
2705
2705
|
await ensureConnected(remoteUrl);
|
|
@@ -2718,8 +2718,8 @@ export async function main(argv) {
|
|
|
2718
2718
|
catch (err) {
|
|
2719
2719
|
// On error: resume local incarnation (undo suspend)
|
|
2720
2720
|
resumeLocalIncarnation(lineageId, cwd);
|
|
2721
|
-
process.stderr.write(`[
|
|
2722
|
-
`[
|
|
2721
|
+
process.stderr.write(`[h2a] migrate forward failed: ${err.message}\n` +
|
|
2722
|
+
`[h2a] local incarnation resumed\n`);
|
|
2723
2723
|
process.exitCode = 1;
|
|
2724
2724
|
return;
|
|
2725
2725
|
}
|
|
@@ -2727,10 +2727,10 @@ export async function main(argv) {
|
|
|
2727
2727
|
const remoteHolder = `remote:pod:${sessionId ?? "unknown"}`;
|
|
2728
2728
|
const handoffResult = handoffLease(lineageId, thisInstance, leaseResult.epoch, remoteHolder, sessionId ?? "unknown", "remote", TTL_MS, cwd);
|
|
2729
2729
|
if ("error" in handoffResult) {
|
|
2730
|
-
process.stderr.write(`[
|
|
2730
|
+
process.stderr.write(`[h2a] warning: lease handoff failed (${handoffResult.error}) — lease may be stale\n`);
|
|
2731
2731
|
}
|
|
2732
2732
|
else {
|
|
2733
|
-
process.stderr.write(`[
|
|
2733
|
+
process.stderr.write(`[h2a] lease handed off to remote (epoch ${handoffResult.epoch})\n`);
|
|
2734
2734
|
}
|
|
2735
2735
|
// Persist remote session id in the lineage record so `migrate back
|
|
2736
2736
|
// --lineage <id>` can target it without listing all sessions.
|
|
@@ -2748,7 +2748,7 @@ export async function main(argv) {
|
|
|
2748
2748
|
}
|
|
2749
2749
|
}
|
|
2750
2750
|
// Step 8: summary
|
|
2751
|
-
process.stderr.write(`[
|
|
2751
|
+
process.stderr.write(`[h2a] migration complete:\n` +
|
|
2752
2752
|
` lineage: ${lineageId}\n` +
|
|
2753
2753
|
` session: ${sessionId ?? "unknown"}\n` +
|
|
2754
2754
|
` remote: ${remoteUrl}\n`);
|
|
@@ -2770,7 +2770,7 @@ export async function main(argv) {
|
|
|
2770
2770
|
const repoBlocker = readiness.blockers.find((b) => b.startsWith("repo:"));
|
|
2771
2771
|
const toLocalBlockers = [authBlocker, repoBlocker].filter((b) => b !== undefined);
|
|
2772
2772
|
if (toLocalBlockers.length > 0) {
|
|
2773
|
-
process.stderr.write(`[
|
|
2773
|
+
process.stderr.write(`[h2a] migration blocked:\n${toLocalBlockers.map((b) => ` • ${b}`).join("\n")}\n`);
|
|
2774
2774
|
process.exitCode = 1;
|
|
2775
2775
|
return;
|
|
2776
2776
|
}
|
|
@@ -2778,13 +2778,13 @@ export async function main(argv) {
|
|
|
2778
2778
|
const lineages = listLineages(cwd);
|
|
2779
2779
|
const remoteLineage = lineages.find((l) => l.incarnation.remote !== null);
|
|
2780
2780
|
if (!remoteLineage) {
|
|
2781
|
-
process.stderr.write(`[
|
|
2782
|
-
`[
|
|
2781
|
+
process.stderr.write(`[h2a] no active remote lineage found in ${cwd}/.remote/lineages/\n` +
|
|
2782
|
+
`[h2a] run \`h2a migrate to-remote\` first, or check \`ls .remote/lineages/\`\n`);
|
|
2783
2783
|
process.exitCode = 1;
|
|
2784
2784
|
return;
|
|
2785
2785
|
}
|
|
2786
2786
|
const lineageId = remoteLineage.lineage;
|
|
2787
|
-
process.stderr.write(`[
|
|
2787
|
+
process.stderr.write(`[h2a] found remote lineage ${lineageId} (session ${remoteLineage.incarnation.remote?.sessionId ?? "unknown"})\n`);
|
|
2788
2788
|
// Step 3: try to acquire the local lease
|
|
2789
2789
|
const localSlug = basename(cwd);
|
|
2790
2790
|
const thisInstance = `claude:local:${randomUUID().replace(/-/g, "").slice(0, 12)}`;
|
|
@@ -2793,23 +2793,23 @@ export async function main(argv) {
|
|
|
2793
2793
|
if ("error" in leaseResult) {
|
|
2794
2794
|
const current = leaseResult.current;
|
|
2795
2795
|
if (current.location === "remote") {
|
|
2796
|
-
process.stderr.write(`[
|
|
2796
|
+
process.stderr.write(`[h2a] un agent remote tient le lease ; attendez l'expiry (${current.expiresAt}) ou utilisez \`h2a migrate to-local --force\` pour forcer\n`);
|
|
2797
2797
|
}
|
|
2798
2798
|
else {
|
|
2799
|
-
process.stderr.write(`[
|
|
2799
|
+
process.stderr.write(`[h2a] lease conflict: held by ${current.holder} until ${current.expiresAt}\n`);
|
|
2800
2800
|
}
|
|
2801
2801
|
process.exitCode = 1;
|
|
2802
2802
|
return;
|
|
2803
2803
|
}
|
|
2804
|
-
process.stderr.write(`[
|
|
2804
|
+
process.stderr.write(`[h2a] lease acquired (epoch ${leaseResult.epoch}, holder ${thisInstance})\n`);
|
|
2805
2805
|
if (opts.dryRun) {
|
|
2806
|
-
process.stderr.write(`[
|
|
2807
|
-
process.stderr.write(`[
|
|
2806
|
+
process.stderr.write(`[h2a] --dry-run: would call migrate back (lineage ${lineageId})\n`);
|
|
2807
|
+
process.stderr.write(`[h2a] dry-run complete — no session stopped, lease NOT committed\n`);
|
|
2808
2808
|
return;
|
|
2809
2809
|
}
|
|
2810
2810
|
// Step 4: reset suspend sentinel (local incarnation resumes)
|
|
2811
2811
|
resumeLocalIncarnation(lineageId, cwd);
|
|
2812
|
-
process.stderr.write(`[
|
|
2812
|
+
process.stderr.write(`[h2a] local incarnation resumed (sentinel cleared)\n`);
|
|
2813
2813
|
// Step 5: call existing migrate back
|
|
2814
2814
|
const remoteUrl = getConfiguredRemote(opts.remote);
|
|
2815
2815
|
const onConflict = opts.onConflict === "backup"
|
|
@@ -2824,12 +2824,12 @@ export async function main(argv) {
|
|
|
2824
2824
|
});
|
|
2825
2825
|
}
|
|
2826
2826
|
catch (err) {
|
|
2827
|
-
process.stderr.write(`[
|
|
2827
|
+
process.stderr.write(`[h2a] migrate back failed: ${err.message}\n`);
|
|
2828
2828
|
process.exitCode = 1;
|
|
2829
2829
|
return;
|
|
2830
2830
|
}
|
|
2831
2831
|
// Step 6: summary
|
|
2832
|
-
process.stderr.write(`[
|
|
2832
|
+
process.stderr.write(`[h2a] to-local complete (lineage ${lineageId})\n`);
|
|
2833
2833
|
});
|
|
2834
2834
|
// ---------------------------------------------------------------------------
|
|
2835
2835
|
// plugin — npm packages providing a CLI + an MCP server, for all agent CLIs
|
|
@@ -2847,7 +2847,7 @@ export async function main(argv) {
|
|
|
2847
2847
|
.option("--install <shell>", "install in Pods by running this shell command (non-npm)")
|
|
2848
2848
|
.action((pkgOrName, opts) => {
|
|
2849
2849
|
if (opts.curl !== undefined && opts.install !== undefined) {
|
|
2850
|
-
process.stderr.write("[
|
|
2850
|
+
process.stderr.write("[h2a] pass only one of --curl / --install\n");
|
|
2851
2851
|
process.exitCode = 1;
|
|
2852
2852
|
return;
|
|
2853
2853
|
}
|
|
@@ -2936,7 +2936,7 @@ export async function main(argv) {
|
|
|
2936
2936
|
.option("--no-gw", "alias for --no-llm-gateway")
|
|
2937
2937
|
.action(async (slug, opts) => {
|
|
2938
2938
|
if (!tmuxAvailable()) {
|
|
2939
|
-
process.stderr.write("[
|
|
2939
|
+
process.stderr.write("[h2a] tmux is not installed locally — `h2a resume` needs it (e.g. `sudo apt install tmux`).\n");
|
|
2940
2940
|
process.exitCode = 1;
|
|
2941
2941
|
return;
|
|
2942
2942
|
}
|
|
@@ -2946,7 +2946,7 @@ export async function main(argv) {
|
|
|
2946
2946
|
? "codex"
|
|
2947
2947
|
: undefined;
|
|
2948
2948
|
if (opts.claude !== undefined && opts.codex !== undefined) {
|
|
2949
|
-
process.stderr.write("[
|
|
2949
|
+
process.stderr.write("[h2a] pass only one of --claude or --codex.\n");
|
|
2950
2950
|
process.exitCode = 1;
|
|
2951
2951
|
return;
|
|
2952
2952
|
}
|
|
@@ -2956,17 +2956,17 @@ export async function main(argv) {
|
|
|
2956
2956
|
? opts.codex.trim()
|
|
2957
2957
|
: undefined;
|
|
2958
2958
|
if (opts.last && !explicitProfile) {
|
|
2959
|
-
process.stderr.write("[
|
|
2959
|
+
process.stderr.write("[h2a] --last is only valid with --claude or --codex.\n");
|
|
2960
2960
|
process.exitCode = 1;
|
|
2961
2961
|
return;
|
|
2962
2962
|
}
|
|
2963
2963
|
if (opts.last && explicitConvId) {
|
|
2964
|
-
process.stderr.write("[
|
|
2964
|
+
process.stderr.write("[h2a] pass either a conversation id or --last, not both.\n");
|
|
2965
2965
|
process.exitCode = 1;
|
|
2966
2966
|
return;
|
|
2967
2967
|
}
|
|
2968
2968
|
if (slug === undefined && !explicitProfile) {
|
|
2969
|
-
process.stderr.write("[
|
|
2969
|
+
process.stderr.write("[h2a] resume needs a known session name, or `h2a resume --claude [conversation-id]` / `h2a resume --codex [conversation-id]` from the repo directory.\n");
|
|
2970
2970
|
process.exitCode = 1;
|
|
2971
2971
|
return;
|
|
2972
2972
|
}
|
|
@@ -2978,7 +2978,7 @@ export async function main(argv) {
|
|
|
2978
2978
|
? localConvStat(process.cwd())?.convId
|
|
2979
2979
|
: explicitConvId;
|
|
2980
2980
|
if (explicitProfile === "claude" && opts.last && !resolvedConvId) {
|
|
2981
|
-
process.stderr.write(`[
|
|
2981
|
+
process.stderr.write(`[h2a] cannot resume last Claude conversation: no local Claude conversation found for ${process.cwd()}.\n`);
|
|
2982
2982
|
process.exitCode = 1;
|
|
2983
2983
|
return;
|
|
2984
2984
|
}
|
|
@@ -2998,31 +2998,31 @@ export async function main(argv) {
|
|
|
2998
2998
|
: registryEntry;
|
|
2999
2999
|
const localIdle = local ? localSessionIdle(local.name) : false;
|
|
3000
3000
|
if (!entry && local && !localIdle && !opts.replace) {
|
|
3001
|
-
process.stderr.write(`[
|
|
3001
|
+
process.stderr.write(`[h2a] local session ${displaySlug} already exists and does not look idle; no new CLI was started.\n`);
|
|
3002
3002
|
if (opts.attach || explicitProfile) {
|
|
3003
|
-
process.stderr.write(`[
|
|
3003
|
+
process.stderr.write(`[h2a] switching to existing session ${displaySlug}\n`);
|
|
3004
3004
|
process.exitCode = attachLocalSession(local.name);
|
|
3005
3005
|
return;
|
|
3006
3006
|
}
|
|
3007
|
-
process.stderr.write(`[
|
|
3007
|
+
process.stderr.write(`[h2a] attach: h2a attach ${displaySlug}\n`);
|
|
3008
3008
|
process.exitCode = 2;
|
|
3009
3009
|
return;
|
|
3010
3010
|
}
|
|
3011
3011
|
if (!entry) {
|
|
3012
|
-
process.stderr.write(`[
|
|
3012
|
+
process.stderr.write(`[h2a] cannot resume ${displaySlug}: registry has no single local-tmux entry with profile, cwd and convId.\n`);
|
|
3013
3013
|
if (local) {
|
|
3014
|
-
process.stderr.write(`[
|
|
3015
|
-
process.stderr.write(`[
|
|
3014
|
+
process.stderr.write(`[h2a] local session ${displaySlug} exists but cannot be relaunched without a recorded convId.\n`);
|
|
3015
|
+
process.stderr.write(`[h2a] attach: h2a attach ${displaySlug}\n`);
|
|
3016
3016
|
}
|
|
3017
3017
|
else {
|
|
3018
|
-
process.stderr.write(`[
|
|
3018
|
+
process.stderr.write(`[h2a] start explicitly: h2a resume ${displaySlug} --claude [convId]\n`);
|
|
3019
3019
|
}
|
|
3020
3020
|
process.exitCode = 1;
|
|
3021
3021
|
return;
|
|
3022
3022
|
}
|
|
3023
3023
|
const resumeSlug = entry.id;
|
|
3024
3024
|
if (!entry.cwd || !entry.tool || (!entry.convId && !explicitProfile)) {
|
|
3025
|
-
process.stderr.write(`[
|
|
3025
|
+
process.stderr.write(`[h2a] cannot resume ${displaySlug}: registry entry is incomplete (need profile, cwd and convId).\n`);
|
|
3026
3026
|
process.exitCode = 1;
|
|
3027
3027
|
return;
|
|
3028
3028
|
}
|
|
@@ -3032,7 +3032,7 @@ export async function main(argv) {
|
|
|
3032
3032
|
gatewayMode = gatewayModeFromOptions(opts);
|
|
3033
3033
|
}
|
|
3034
3034
|
catch (error) {
|
|
3035
|
-
process.stderr.write(`[
|
|
3035
|
+
process.stderr.write(`[h2a] ${error.message}.\n`);
|
|
3036
3036
|
process.exitCode = 1;
|
|
3037
3037
|
return;
|
|
3038
3038
|
}
|
|
@@ -3044,7 +3044,7 @@ export async function main(argv) {
|
|
|
3044
3044
|
last: Boolean(opts.last),
|
|
3045
3045
|
});
|
|
3046
3046
|
if (args.length === 0) {
|
|
3047
|
-
process.stderr.write(`[
|
|
3047
|
+
process.stderr.write(`[h2a] cannot resume ${displaySlug}: profile "${profile}" has no verified local resume argv.\n`);
|
|
3048
3048
|
process.exitCode = 1;
|
|
3049
3049
|
return;
|
|
3050
3050
|
}
|
|
@@ -3059,49 +3059,49 @@ export async function main(argv) {
|
|
|
3059
3059
|
})
|
|
3060
3060
|
: "unknown";
|
|
3061
3061
|
if (gatewayEnvStatus === "missing" || gatewayEnvStatus === "stale") {
|
|
3062
|
-
process.stderr.write(`[
|
|
3063
|
-
process.stderr.write(`[
|
|
3062
|
+
process.stderr.write(`[h2a] local session ${displaySlug} is running without current llm-mesh env (${gatewayEnvStatus}); not restarting an active session automatically.\n`);
|
|
3063
|
+
process.stderr.write(`[h2a] to relaunch with gateway env, confirm explicitly: h2a resume ${displaySlug} --replace\n`);
|
|
3064
3064
|
}
|
|
3065
|
-
process.stderr.write(`[
|
|
3065
|
+
process.stderr.write(`[h2a] local session ${displaySlug} already exists and does not look idle; no new ${profile} was started.\n`);
|
|
3066
3066
|
if (opts.attach || explicitProfile) {
|
|
3067
3067
|
if (explicitProfile && currentTmuxSessionIs(local.name)) {
|
|
3068
|
-
process.stderr.write(`[
|
|
3068
|
+
process.stderr.write(`[h2a] already inside ${displaySlug}; running ${profile} resume in this pane\n`);
|
|
3069
3069
|
process.exitCode = runLocalCliForeground(command, args);
|
|
3070
3070
|
return;
|
|
3071
3071
|
}
|
|
3072
|
-
process.stderr.write(`[
|
|
3072
|
+
process.stderr.write(`[h2a] switching to existing session ${displaySlug}\n`);
|
|
3073
3073
|
process.exitCode = attachLocalSession(local.name);
|
|
3074
3074
|
return;
|
|
3075
3075
|
}
|
|
3076
|
-
process.stderr.write(`[
|
|
3076
|
+
process.stderr.write(`[h2a] attach: h2a attach ${displaySlug}\n`);
|
|
3077
3077
|
process.exitCode = 2;
|
|
3078
3078
|
return;
|
|
3079
3079
|
}
|
|
3080
3080
|
if (local) {
|
|
3081
3081
|
if (!localIdle && opts.replace) {
|
|
3082
|
-
process.stderr.write(`[
|
|
3082
|
+
process.stderr.write(`[h2a] local session ${displaySlug} does not look idle; --replace will kill tmux session ${local.name} before resuming${entry.convId ? ` ${entry.convId}` : ""}.\n`);
|
|
3083
3083
|
}
|
|
3084
3084
|
if (!opts.replace) {
|
|
3085
|
-
process.stderr.write(`[
|
|
3086
|
-
process.stderr.write(`[
|
|
3087
|
-
process.stderr.write(`[
|
|
3085
|
+
process.stderr.write(`[h2a] local session ${displaySlug} already exists.\n`);
|
|
3086
|
+
process.stderr.write(`[h2a] pane appears idle; replacing it will kill tmux session ${local.name} and resume${entry.convId ? ` conversation ${entry.convId}` : ""} in ${entry.cwd}.\n`);
|
|
3087
|
+
process.stderr.write(`[h2a] If another CLI is still writing this conversation, replacing can corrupt the .jsonl.\n`);
|
|
3088
3088
|
if (!(await confirmReplace(displaySlug))) {
|
|
3089
|
-
process.stderr.write(`[
|
|
3090
|
-
process.stderr.write(`[
|
|
3091
|
-
process.stderr.write(`[
|
|
3092
|
-
process.stderr.write(`[
|
|
3089
|
+
process.stderr.write(`[h2a] takeover requires confirmation.\n`);
|
|
3090
|
+
process.stderr.write(`[h2a] interactive: h2a resume ${displaySlug}\n`);
|
|
3091
|
+
process.stderr.write(`[h2a] explicit replace: h2a resume ${displaySlug} --replace\n`);
|
|
3092
|
+
process.stderr.write(`[h2a] manual path: h2a stop ${displaySlug} --reason restart && h2a resume ${displaySlug}\n`);
|
|
3093
3093
|
process.exitCode = 1;
|
|
3094
3094
|
return;
|
|
3095
3095
|
}
|
|
3096
3096
|
}
|
|
3097
3097
|
const rechecked = findLocalSession(displaySlug);
|
|
3098
3098
|
if (!rechecked) {
|
|
3099
|
-
process.stderr.write(`[
|
|
3099
|
+
process.stderr.write(`[h2a] local session ${displaySlug} changed state before replace; aborting.\n`);
|
|
3100
3100
|
process.exitCode = 1;
|
|
3101
3101
|
return;
|
|
3102
3102
|
}
|
|
3103
3103
|
if (!opts.replace && !localSessionIdle(rechecked.name)) {
|
|
3104
|
-
process.stderr.write(`[
|
|
3104
|
+
process.stderr.write(`[h2a] local session ${displaySlug} is no longer idle; aborting.\n`);
|
|
3105
3105
|
process.exitCode = 1;
|
|
3106
3106
|
return;
|
|
3107
3107
|
}
|
|
@@ -3121,11 +3121,11 @@ export async function main(argv) {
|
|
|
3121
3121
|
}
|
|
3122
3122
|
}
|
|
3123
3123
|
if (!killLocalSession(rechecked.name)) {
|
|
3124
|
-
process.stderr.write(`[
|
|
3124
|
+
process.stderr.write(`[h2a] local session ${displaySlug} could not be killed; no new ${profile} was started.\n`);
|
|
3125
3125
|
process.exitCode = 1;
|
|
3126
3126
|
return;
|
|
3127
3127
|
}
|
|
3128
|
-
process.stderr.write(`[
|
|
3128
|
+
process.stderr.write(`[h2a] replaced local session ${displaySlug} (${rechecked.name})\n`);
|
|
3129
3129
|
}
|
|
3130
3130
|
else {
|
|
3131
3131
|
if (entry.convId) {
|
|
@@ -3153,19 +3153,19 @@ export async function main(argv) {
|
|
|
3153
3153
|
...(entry.convId ? { convId: entry.convId } : {}),
|
|
3154
3154
|
...(gatewayMode !== "auto" ? { gatewayMode } : {}),
|
|
3155
3155
|
});
|
|
3156
|
-
process.stderr.write(`[
|
|
3156
|
+
process.stderr.write(`[h2a] resumed local session ${resumeSlug} (${profile} resume${entry.convId ? ` ${entry.convId}` : ""} in ${entry.cwd})\n`);
|
|
3157
3157
|
if (gateway) {
|
|
3158
|
-
process.stderr.write(`[
|
|
3158
|
+
process.stderr.write(`[h2a] gateway active: ${gateway}\n`);
|
|
3159
3159
|
}
|
|
3160
3160
|
if (opts.attach) {
|
|
3161
3161
|
process.exitCode = attachLocalSession(name);
|
|
3162
3162
|
return;
|
|
3163
3163
|
}
|
|
3164
|
-
process.stderr.write(`[
|
|
3164
|
+
process.stderr.write(`[h2a] attach: h2a attach ${resumeSlug}\n`);
|
|
3165
3165
|
});
|
|
3166
3166
|
program
|
|
3167
3167
|
.command("run <profile> [path]")
|
|
3168
|
-
.description("Start a LOCAL session in tmux (claude/codex/…) in <path> (default: cwd), then attach this terminal by default. Remote applies its embedded scroll-safe tmux profile at launch; no ~/.tmux.conf is required. Manage it like a remote one: `
|
|
3168
|
+
.description("Start a LOCAL session in tmux (claude/codex/…) in <path> (default: cwd), then attach this terminal by default. Remote applies its embedded scroll-safe tmux profile at launch; no ~/.tmux.conf is required. Manage it like a remote one: `h2a ls`, `h2a attach <slug>`, `h2a stop <slug>`. Detach with Ctrl-b d; the session keeps running.")
|
|
3169
3169
|
.option("--no-attach", "start detached and print the attach command instead (for scripts / fan-out orchestration)")
|
|
3170
3170
|
.option("-r, --resume <convId>", "resume a specific conversation in the CLI")
|
|
3171
3171
|
.option("--force", "start even if the conversation already has a live writer (risk: conversation .jsonl corruption)")
|
|
@@ -3178,7 +3178,7 @@ export async function main(argv) {
|
|
|
3178
3178
|
.option("--no-gw", "alias for --no-llm-gateway")
|
|
3179
3179
|
.action(async (profile, path, opts) => {
|
|
3180
3180
|
if (!tmuxAvailable()) {
|
|
3181
|
-
process.stderr.write("[
|
|
3181
|
+
process.stderr.write("[h2a] tmux is not installed locally — `h2a run` needs it (e.g. `sudo apt install tmux`).\n");
|
|
3182
3182
|
process.exitCode = 1;
|
|
3183
3183
|
return;
|
|
3184
3184
|
}
|
|
@@ -3186,14 +3186,14 @@ export async function main(argv) {
|
|
|
3186
3186
|
if (opts.count !== undefined) {
|
|
3187
3187
|
count = Number(opts.count);
|
|
3188
3188
|
if (!Number.isInteger(count) || count < 1) {
|
|
3189
|
-
process.stderr.write(`[
|
|
3189
|
+
process.stderr.write(`[h2a] --count must be a whole number ≥ 1\n`);
|
|
3190
3190
|
process.exitCode = 1;
|
|
3191
3191
|
return;
|
|
3192
3192
|
}
|
|
3193
3193
|
// Fanning out N agents on the SAME conversation = N writers on one
|
|
3194
3194
|
// .jsonl (corruption). A fan-out is N FRESH conversations.
|
|
3195
3195
|
if (count > 1 && opts.resume) {
|
|
3196
|
-
process.stderr.write(`[
|
|
3196
|
+
process.stderr.write(`[h2a] --count > 1 cannot combine with -r/--resume (each fanned agent is a fresh conversation; resuming one into N would corrupt it)\n`);
|
|
3197
3197
|
process.exitCode = 1;
|
|
3198
3198
|
return;
|
|
3199
3199
|
}
|
|
@@ -3213,9 +3213,9 @@ export async function main(argv) {
|
|
|
3213
3213
|
.filter((slug) => slug !== undefined);
|
|
3214
3214
|
if (existingLocalSessions.length > 0) {
|
|
3215
3215
|
for (const slug of existingLocalSessions) {
|
|
3216
|
-
process.stderr.write(`[
|
|
3217
|
-
process.stderr.write(`[
|
|
3218
|
-
process.stderr.write(`[
|
|
3216
|
+
process.stderr.write(`[h2a] local session ${slug} already exists; no new ${profile} was started.\n`);
|
|
3217
|
+
process.stderr.write(`[h2a] attach: h2a attach ${slug}\n`);
|
|
3218
|
+
process.stderr.write(`[h2a] stop first: h2a stop ${slug} --reason restart\n`);
|
|
3219
3219
|
}
|
|
3220
3220
|
process.exitCode = 1;
|
|
3221
3221
|
return;
|
|
@@ -3244,7 +3244,7 @@ export async function main(argv) {
|
|
|
3244
3244
|
gatewayMode = gatewayModeFromOptions(opts);
|
|
3245
3245
|
}
|
|
3246
3246
|
catch (error) {
|
|
3247
|
-
process.stderr.write(`[
|
|
3247
|
+
process.stderr.write(`[h2a] ${error.message}.\n`);
|
|
3248
3248
|
process.exitCode = 1;
|
|
3249
3249
|
return;
|
|
3250
3250
|
}
|
|
@@ -3255,7 +3255,7 @@ export async function main(argv) {
|
|
|
3255
3255
|
? localResumeArgs(profile, opts.resume, { bare: useBare })
|
|
3256
3256
|
: localStartArgs(profile, { bare: useBare });
|
|
3257
3257
|
if (opts.resume && args.length === 0) {
|
|
3258
|
-
process.stderr.write(`[
|
|
3258
|
+
process.stderr.write(`[h2a] profile "${profile}" has no verified local resume argv; start it without -r/--resume\n`);
|
|
3259
3259
|
process.exitCode = 1;
|
|
3260
3260
|
return;
|
|
3261
3261
|
}
|
|
@@ -3279,22 +3279,22 @@ export async function main(argv) {
|
|
|
3279
3279
|
// it the default. Never fails the run.
|
|
3280
3280
|
if (opts.h2a || h2a.enabled) {
|
|
3281
3281
|
if (startH2aWindow(name, cwd, h2a.command)) {
|
|
3282
|
-
process.stderr.write(`[
|
|
3282
|
+
process.stderr.write(`[h2a] h2a window started in ${slug} (${h2a.command})\n`);
|
|
3283
3283
|
}
|
|
3284
3284
|
}
|
|
3285
3285
|
}
|
|
3286
3286
|
if (count > 1) {
|
|
3287
|
-
process.stderr.write(`[
|
|
3288
|
-
`[
|
|
3287
|
+
process.stderr.write(`[h2a] ${started.length} ${profile} agents started in ${cwd}: ${started.map((s) => s.slug).join(", ")}\n` +
|
|
3288
|
+
`[h2a] attach one with: h2a attach <slug>\n`);
|
|
3289
3289
|
return; // never auto-attach a fleet
|
|
3290
3290
|
}
|
|
3291
3291
|
const only = started[0];
|
|
3292
|
-
process.stderr.write(`[
|
|
3292
|
+
process.stderr.write(`[h2a] local session ${only.slug} started (${profile}${opts.resume ? ` --resume ${opts.resume}` : ""} in ${cwd})\n`);
|
|
3293
3293
|
// Single local runs hand off the terminal by default: there is no value
|
|
3294
3294
|
// in making users type a second `remote attach <slug>` command. Commander
|
|
3295
3295
|
// sets opts.attach=false only for --no-attach.
|
|
3296
3296
|
if (opts.attach === false) {
|
|
3297
|
-
process.stderr.write(`[
|
|
3297
|
+
process.stderr.write(`[h2a] attach with: h2a attach ${only.slug}\n`);
|
|
3298
3298
|
return;
|
|
3299
3299
|
}
|
|
3300
3300
|
process.exitCode = attachLocalSession(only.name);
|
|
@@ -3320,7 +3320,7 @@ export async function main(argv) {
|
|
|
3320
3320
|
.option("--account <id>", "force a specific account from the pool (bypass selectAccountWithFallback). E.g. claude-code-1234567890.")
|
|
3321
3321
|
.action(async (type, task, opts) => {
|
|
3322
3322
|
if (!isDelegateType(type)) {
|
|
3323
|
-
process.stderr.write(`[
|
|
3323
|
+
process.stderr.write(`[h2a] unknown agent type "${type}" (use: claude | codex | agy)\n`);
|
|
3324
3324
|
process.exitCode = 1;
|
|
3325
3325
|
return;
|
|
3326
3326
|
}
|
|
@@ -3332,7 +3332,7 @@ export async function main(argv) {
|
|
|
3332
3332
|
assertSafeName(jobId);
|
|
3333
3333
|
}
|
|
3334
3334
|
catch (err) {
|
|
3335
|
-
process.stderr.write(`[
|
|
3335
|
+
process.stderr.write(`[h2a] ${err.message}\n`);
|
|
3336
3336
|
process.exitCode = 1;
|
|
3337
3337
|
return;
|
|
3338
3338
|
}
|
|
@@ -3345,7 +3345,7 @@ export async function main(argv) {
|
|
|
3345
3345
|
: undefined;
|
|
3346
3346
|
const depthBudget = inheritedDepthBudget(requestedDepth, process.env);
|
|
3347
3347
|
if (!canDelegateAtDepth(depthBudget)) {
|
|
3348
|
-
process.stderr.write("[
|
|
3348
|
+
process.stderr.write("[h2a] spawn-depth budget exhausted (REMOTE_DELEGATE_DEPTH=0) — this job has reached its --max-depth and may not delegate further.\n");
|
|
3349
3349
|
process.exitCode = 1;
|
|
3350
3350
|
return;
|
|
3351
3351
|
}
|
|
@@ -3370,13 +3370,13 @@ export async function main(argv) {
|
|
|
3370
3370
|
remoteTarget = getConfiguredRemote(typeof opts.remote === "string" ? opts.remote : undefined);
|
|
3371
3371
|
}
|
|
3372
3372
|
catch (err) {
|
|
3373
|
-
process.stderr.write(`[
|
|
3373
|
+
process.stderr.write(`[h2a] ${err.message}\n`);
|
|
3374
3374
|
process.exitCode = 1;
|
|
3375
3375
|
return;
|
|
3376
3376
|
}
|
|
3377
3377
|
}
|
|
3378
3378
|
else if (!tmuxAvailable()) {
|
|
3379
|
-
process.stderr.write("[
|
|
3379
|
+
process.stderr.write("[h2a] tmux is not installed locally — local `h2a delegate` needs it (use --remote for a Pod).\n");
|
|
3380
3380
|
process.exitCode = 1;
|
|
3381
3381
|
return;
|
|
3382
3382
|
}
|
|
@@ -3444,7 +3444,7 @@ export async function main(argv) {
|
|
|
3444
3444
|
}
|
|
3445
3445
|
if (!claimed && listJobs().some((e) => e.id === jobId)) {
|
|
3446
3446
|
// The entry exists but the cap is full → stay queued.
|
|
3447
|
-
process.stderr.write(`[
|
|
3447
|
+
process.stderr.write(`[h2a] queued job ${jobId} (${jobType}${opts.headless ? " headless" : ""}${isRemote ? " remote" : ""}) — ` +
|
|
3448
3448
|
`${effectiveCap} concurrent slot(s) busy. Start it with: remote jobs conduct\n`);
|
|
3449
3449
|
process.stdout.write(`${jobId}\n`);
|
|
3450
3450
|
return;
|
|
@@ -3452,7 +3452,7 @@ export async function main(argv) {
|
|
|
3452
3452
|
if (!claimed) {
|
|
3453
3453
|
// Registry write failed entirely (no entry at all) — fall back to a
|
|
3454
3454
|
// throwaway in-memory entry so the launch still happens.
|
|
3455
|
-
process.stderr.write(`[
|
|
3455
|
+
process.stderr.write(`[h2a] job ${jobId} not in registry after enroll — launching from an in-memory spec\n`);
|
|
3456
3456
|
}
|
|
3457
3457
|
const launchEntry = claimed ??
|
|
3458
3458
|
{
|
|
@@ -3484,12 +3484,12 @@ export async function main(argv) {
|
|
|
3484
3484
|
// only reclaim it on a later reconcile pass).
|
|
3485
3485
|
if (claimed)
|
|
3486
3486
|
advanceJob(jobId, "failed");
|
|
3487
|
-
process.stderr.write(`[
|
|
3487
|
+
process.stderr.write(`[h2a] failed to start job ${jobId}: ${result.error}\n`);
|
|
3488
3488
|
process.exitCode = 1;
|
|
3489
3489
|
return;
|
|
3490
3490
|
}
|
|
3491
|
-
process.stderr.write(`[
|
|
3492
|
-
`[
|
|
3491
|
+
process.stderr.write(`[h2a] delegated ${result.target === "remote" ? "REMOTE " : ""}job ${jobId} (${jobType}${opts.headless ? " headless" : ""}) ${result.target === "remote" ? "→ " : "in "}${result.detail}\n` +
|
|
3492
|
+
`[h2a] supervise: remote jobs status ${jobId}` +
|
|
3493
3493
|
(opts.headless ? "" : ` attach: remote jobs attach ${jobId}`) +
|
|
3494
3494
|
"\n");
|
|
3495
3495
|
process.stdout.write(`${jobId}\n`);
|
|
@@ -3517,7 +3517,7 @@ export async function main(argv) {
|
|
|
3517
3517
|
const projection = buildAgentsProjection();
|
|
3518
3518
|
const agent = findProjectedAgent(projection.agents, id);
|
|
3519
3519
|
if (!agent) {
|
|
3520
|
-
process.stderr.write(`[
|
|
3520
|
+
process.stderr.write(`[h2a] no projected agent \"${id}\" (see: h2a agents ls --json)\n`);
|
|
3521
3521
|
process.exitCode = 1;
|
|
3522
3522
|
return;
|
|
3523
3523
|
}
|
|
@@ -3528,7 +3528,7 @@ export async function main(argv) {
|
|
|
3528
3528
|
// ---------------------------------------------------------------------------
|
|
3529
3529
|
const jobsCommand = program
|
|
3530
3530
|
.command("jobs")
|
|
3531
|
-
.description("Supervise delegated agent jobs (see `
|
|
3531
|
+
.description("Supervise delegated agent jobs (see `h2a delegate`).");
|
|
3532
3532
|
const jobLive = (e) => isLive(e);
|
|
3533
3533
|
// S1 — resolve the `actor.instance` a job envelope MUST carry to be trusted,
|
|
3534
3534
|
// off the registry: `job.done`/`decision.requested` (ABOUT the job) must come
|
|
@@ -3627,7 +3627,7 @@ export async function main(argv) {
|
|
|
3627
3627
|
if (step.action === "fail") {
|
|
3628
3628
|
const advanced = advanceJob(job.id, "failed");
|
|
3629
3629
|
if (advanced) {
|
|
3630
|
-
process.stderr.write(`[
|
|
3630
|
+
process.stderr.write(`[h2a] job ${job.id} (${job.tool}) failed: rate-limited ` +
|
|
3631
3631
|
`(gave up after ${job.throttle?.attempts ?? THROTTLE_MAX_ATTEMPTS} resume attempts)\n`);
|
|
3632
3632
|
emitJobDone(advanced, { state: "failed" });
|
|
3633
3633
|
realizeTrackMirror(advanced);
|
|
@@ -3655,7 +3655,7 @@ export async function main(argv) {
|
|
|
3655
3655
|
return { entries, result: true };
|
|
3656
3656
|
});
|
|
3657
3657
|
if (ok) {
|
|
3658
|
-
process.stderr.write(`[
|
|
3658
|
+
process.stderr.write(`[h2a] job ${job.id} (${job.tool}) throttled (${verdict.signature ?? "rate-limited"}) — ` +
|
|
3659
3659
|
`attempt ${step.attempts}/${THROTTLE_MAX_ATTEMPTS}, resume at ${step.nextRetryAt}\n`);
|
|
3660
3660
|
}
|
|
3661
3661
|
return ok;
|
|
@@ -3805,7 +3805,7 @@ export async function main(argv) {
|
|
|
3805
3805
|
await reconcileJobs();
|
|
3806
3806
|
const job = listJobs().find((e) => e.id === id);
|
|
3807
3807
|
if (!job) {
|
|
3808
|
-
process.stderr.write(`[
|
|
3808
|
+
process.stderr.write(`[h2a] no job "${id}" (see: remote jobs ls)\n`);
|
|
3809
3809
|
process.exitCode = 1;
|
|
3810
3810
|
return;
|
|
3811
3811
|
}
|
|
@@ -3854,7 +3854,7 @@ export async function main(argv) {
|
|
|
3854
3854
|
});
|
|
3855
3855
|
jobsCommand
|
|
3856
3856
|
.command("attach <id>")
|
|
3857
|
-
.description("Attach into the job's tmux session (Ctrl-b d to detach). Remote jobs exec into the Pod's tmux, like `
|
|
3857
|
+
.description("Attach into the job's tmux session (Ctrl-b d to detach). Remote jobs exec into the Pod's tmux, like `h2a attach <id> --exec`.")
|
|
3858
3858
|
.action(async (id) => {
|
|
3859
3859
|
const job = listJobs().find((e) => e.id === id);
|
|
3860
3860
|
// REMOTE job (P2): exec into the Pod's tmux over the configured tunnel,
|
|
@@ -3862,13 +3862,13 @@ export async function main(argv) {
|
|
|
3862
3862
|
if (job?.kind === "remote") {
|
|
3863
3863
|
const remoteId = job.remoteId;
|
|
3864
3864
|
if (!remoteId) {
|
|
3865
|
-
process.stderr.write(`[
|
|
3865
|
+
process.stderr.write(`[h2a] remote job "${id}" has no session id recorded (see: remote jobs status ${id})\n`);
|
|
3866
3866
|
process.exitCode = 1;
|
|
3867
3867
|
return;
|
|
3868
3868
|
}
|
|
3869
3869
|
const tunnel = getTunnel();
|
|
3870
3870
|
if (!tunnel) {
|
|
3871
|
-
process.stderr.write("[
|
|
3871
|
+
process.stderr.write("[h2a] attaching a remote job needs a tunnel configured (h2a config tunnel …)\n");
|
|
3872
3872
|
process.exitCode = 1;
|
|
3873
3873
|
return;
|
|
3874
3874
|
}
|
|
@@ -3878,14 +3878,14 @@ export async function main(argv) {
|
|
|
3878
3878
|
catch {
|
|
3879
3879
|
// best-effort: the kubectl exec below works off the tunnel regardless
|
|
3880
3880
|
}
|
|
3881
|
-
process.stderr.write(`[
|
|
3881
|
+
process.stderr.write(`[h2a] exec-attaching into Pod tmux for job ${id} (${remoteId}) (Ctrl-b d to detach)\n`);
|
|
3882
3882
|
process.exitCode = attachPodTmux(tunnel, remoteId);
|
|
3883
3883
|
return;
|
|
3884
3884
|
}
|
|
3885
3885
|
// LOCAL job (P1): attach into the detached tmux session.
|
|
3886
3886
|
const name = job?.tmuxSession ?? localSessionName(id);
|
|
3887
3887
|
if (!findLocalSession(name)) {
|
|
3888
|
-
process.stderr.write(`[
|
|
3888
|
+
process.stderr.write(`[h2a] no live tmux session for job "${id}" (it may have ended; see: remote jobs status ${id})\n`);
|
|
3889
3889
|
process.exitCode = 1;
|
|
3890
3890
|
return;
|
|
3891
3891
|
}
|
|
@@ -3897,7 +3897,7 @@ export async function main(argv) {
|
|
|
3897
3897
|
.action((id) => {
|
|
3898
3898
|
const job = listJobs().find((e) => e.id === id);
|
|
3899
3899
|
if (!job) {
|
|
3900
|
-
process.stderr.write(`[
|
|
3900
|
+
process.stderr.write(`[h2a] no job "${id}" (see: remote jobs ls)\n`);
|
|
3901
3901
|
process.exitCode = 1;
|
|
3902
3902
|
return;
|
|
3903
3903
|
}
|
|
@@ -3909,7 +3909,7 @@ export async function main(argv) {
|
|
|
3909
3909
|
}
|
|
3910
3910
|
// REMOTE job: the output lives in the Pod's tmux, not a local pane.
|
|
3911
3911
|
if (job.kind === "remote") {
|
|
3912
|
-
process.stderr.write(`[
|
|
3912
|
+
process.stderr.write(`[h2a] remote job "${id}" runs in a Pod — view it live with: remote jobs attach ${id}\n`);
|
|
3913
3913
|
process.exitCode = 1;
|
|
3914
3914
|
return;
|
|
3915
3915
|
}
|
|
@@ -3919,7 +3919,7 @@ export async function main(argv) {
|
|
|
3919
3919
|
process.stdout.write(pane);
|
|
3920
3920
|
return;
|
|
3921
3921
|
}
|
|
3922
|
-
process.stderr.write(`[
|
|
3922
|
+
process.stderr.write(`[h2a] no logs for job "${id}" (no output.log and the tmux pane is gone)\n`);
|
|
3923
3923
|
process.exitCode = 1;
|
|
3924
3924
|
});
|
|
3925
3925
|
// P3 — the decision channel (h2a). A delegated INTERACTIVE job can emit a
|
|
@@ -3937,7 +3937,7 @@ export async function main(argv) {
|
|
|
3937
3937
|
envelopes = authenticateJobEnvelopes(readInboxEnvelopes(), expectedInstanceOf);
|
|
3938
3938
|
}
|
|
3939
3939
|
catch (error) {
|
|
3940
|
-
process.stderr.write(`[
|
|
3940
|
+
process.stderr.write(`[h2a] cannot read the h2a inbox: ${(error instanceof Error ? error.message : String(error)).slice(0, 160)}\n`);
|
|
3941
3941
|
process.exitCode = 1;
|
|
3942
3942
|
return;
|
|
3943
3943
|
}
|
|
@@ -3954,13 +3954,13 @@ export async function main(argv) {
|
|
|
3954
3954
|
assertSafeName(jobId);
|
|
3955
3955
|
}
|
|
3956
3956
|
catch (err) {
|
|
3957
|
-
process.stderr.write(`[
|
|
3957
|
+
process.stderr.write(`[h2a] ${err.message}\n`);
|
|
3958
3958
|
process.exitCode = 1;
|
|
3959
3959
|
return;
|
|
3960
3960
|
}
|
|
3961
3961
|
const job = listJobs().find((e) => e.id === jobId);
|
|
3962
3962
|
if (!job) {
|
|
3963
|
-
process.stderr.write(`[
|
|
3963
|
+
process.stderr.write(`[h2a] no job "${jobId}" (see: remote jobs ls)\n`);
|
|
3964
3964
|
process.exitCode = 1;
|
|
3965
3965
|
return;
|
|
3966
3966
|
}
|
|
@@ -3972,13 +3972,13 @@ export async function main(argv) {
|
|
|
3972
3972
|
});
|
|
3973
3973
|
try {
|
|
3974
3974
|
const { path, written } = dropEnvelope(envelope, envelopeFileName("decision.reply", jobId, Date.now()));
|
|
3975
|
-
process.stderr.write(`[
|
|
3975
|
+
process.stderr.write(`[h2a] decision.reply for ${jobId} → ${envelope.to}` +
|
|
3976
3976
|
`${written ? "" : " (already present, not overwritten)"}\n` +
|
|
3977
|
-
`[
|
|
3978
|
-
`[
|
|
3977
|
+
`[h2a] envelope: ${path}\n` +
|
|
3978
|
+
`[h2a] delivered to the job's Pod on the next: remote h2a bridge\n`);
|
|
3979
3979
|
}
|
|
3980
3980
|
catch (error) {
|
|
3981
|
-
process.stderr.write(`[
|
|
3981
|
+
process.stderr.write(`[h2a] failed to write decision.reply: ${(error instanceof Error ? error.message : String(error)).slice(0, 160)}\n`);
|
|
3982
3982
|
process.exitCode = 1;
|
|
3983
3983
|
}
|
|
3984
3984
|
});
|
|
@@ -4033,10 +4033,10 @@ export async function main(argv) {
|
|
|
4033
4033
|
const r = resumeThrottledJob(job);
|
|
4034
4034
|
if (r.started) {
|
|
4035
4035
|
resumed += 1;
|
|
4036
|
-
process.stderr.write(`[
|
|
4036
|
+
process.stderr.write(`[h2a] conduct: resumed throttled ${job.id} (${job.tool}) in ${r.detail}\n`);
|
|
4037
4037
|
}
|
|
4038
4038
|
else {
|
|
4039
|
-
process.stderr.write(`[
|
|
4039
|
+
process.stderr.write(`[h2a] conduct: could not resume throttled ${job.id}: ${r.error}\n`);
|
|
4040
4040
|
}
|
|
4041
4041
|
}
|
|
4042
4042
|
// AIMD effective cap: a provider rate-limit is account-wide, so admit FEWER
|
|
@@ -4046,7 +4046,7 @@ export async function main(argv) {
|
|
|
4046
4046
|
const effectiveCap = aimdEffectiveCap(cap, recentThrottleEvents(refreshed), nowMs, aimdLastCap);
|
|
4047
4047
|
aimdLastCap = effectiveCap;
|
|
4048
4048
|
if (effectiveCap < cap) {
|
|
4049
|
-
process.stderr.write(`[
|
|
4049
|
+
process.stderr.write(`[h2a] conduct: AIMD breaker — admitting up to ${effectiveCap}/${cap} (rate-limit pressure)\n`);
|
|
4050
4050
|
}
|
|
4051
4051
|
// (b) start pending jobs under the EFFECTIVE cap (oldest-first FIFO).
|
|
4052
4052
|
let started = 0;
|
|
@@ -4057,13 +4057,13 @@ export async function main(argv) {
|
|
|
4057
4057
|
const result = await startJob(job);
|
|
4058
4058
|
if (result.started) {
|
|
4059
4059
|
started += 1;
|
|
4060
|
-
process.stderr.write(`[
|
|
4060
|
+
process.stderr.write(`[h2a] conduct: started ${id} (${job.tool}) ${result.target === "remote" ? "→ " : "in "}${result.detail}\n`);
|
|
4061
4061
|
}
|
|
4062
4062
|
else {
|
|
4063
4063
|
// A launch failure fails the job (frees nothing it didn't hold) so the
|
|
4064
4064
|
// queue keeps moving; the error is recorded on stderr.
|
|
4065
4065
|
advanceJob(id, "failed");
|
|
4066
|
-
process.stderr.write(`[
|
|
4066
|
+
process.stderr.write(`[h2a] conduct: failed to start ${id}: ${result.error}\n`);
|
|
4067
4067
|
}
|
|
4068
4068
|
}
|
|
4069
4069
|
// Count a resume as "started" work for the pass recap.
|
|
@@ -4071,7 +4071,7 @@ export async function main(argv) {
|
|
|
4071
4071
|
};
|
|
4072
4072
|
jobsCommand
|
|
4073
4073
|
.command("conduct")
|
|
4074
|
-
.description("Conductor: reconcile job state, consume finished jobs, and START queued (pending) jobs while running < cap — the SAME launch path as `
|
|
4074
|
+
.description("Conductor: reconcile job state, consume finished jobs, and START queued (pending) jobs while running < cap — the SAME launch path as `h2a delegate`. One pass by default; `--watch <min>` loops in the FOREGROUND (run in a dedicated tmux window, no daemon; Ctrl-C to stop).")
|
|
4075
4075
|
.option("--watch <minutes>", "loop in the foreground every <minutes> (whole number >= 1); without it, runs a single pass and exits")
|
|
4076
4076
|
.option("--max-concurrent <n>", "concurrency cap (default: config maxConcurrent / REMOTE_MAX_CONCURRENT / 16)")
|
|
4077
4077
|
.option("--max-depth <d>", "spawn-depth budget recorded for jobs this conductor starts (clamp 1–3, default: each job's recorded budget)")
|
|
@@ -4091,10 +4091,10 @@ export async function main(argv) {
|
|
|
4091
4091
|
const minutes = opts.watch === undefined ? undefined : parseWatchMinutes(opts.watch);
|
|
4092
4092
|
if (minutes === undefined) {
|
|
4093
4093
|
const { started, finished } = await conductPass(cap);
|
|
4094
|
-
process.stderr.write(`[
|
|
4094
|
+
process.stderr.write(`[h2a] conduct: cap ${cap} — ${started} started, ${finished} finished\n`);
|
|
4095
4095
|
return;
|
|
4096
4096
|
}
|
|
4097
|
-
process.stderr.write(`[
|
|
4097
|
+
process.stderr.write(`[h2a] conducting (cap ${cap}, every ${minutes} min) — Ctrl-C to stop\n`);
|
|
4098
4098
|
process.exitCode = await conductLoop(minutes, () => conductPass(cap));
|
|
4099
4099
|
});
|
|
4100
4100
|
// ---------------------------------------------------------------------------
|
|
@@ -4184,7 +4184,7 @@ export async function main(argv) {
|
|
|
4184
4184
|
// proceed — the envelope's workspaceId is authoritative for the launch.
|
|
4185
4185
|
const localId = localDurableWorkspaceId();
|
|
4186
4186
|
if (localId && localId !== request.workspaceId) {
|
|
4187
|
-
process.stderr.write(`[
|
|
4187
|
+
process.stderr.write(`[h2a] conductor-launch: workspace id mismatch — envelope ${request.workspaceId} vs local ${localId} ` +
|
|
4188
4188
|
`(proceeding with the envelope's id; align canonicalization with a2a-cli)\n`);
|
|
4189
4189
|
}
|
|
4190
4190
|
const liveConductors = countLiveConductors(request.workspaceId, slug);
|
|
@@ -4199,7 +4199,7 @@ export async function main(argv) {
|
|
|
4199
4199
|
if (!gate.launch) {
|
|
4200
4200
|
if (confirm)
|
|
4201
4201
|
markLaunchEnvelopeProcessed(env.path, `skip: ${gate.reason}`);
|
|
4202
|
-
process.stderr.write(`[
|
|
4202
|
+
process.stderr.write(`[h2a] conductor-launch: SKIP — ${gate.reason}\n`);
|
|
4203
4203
|
return { launched: false, detail: gate.reason };
|
|
4204
4204
|
}
|
|
4205
4205
|
const available = detectAvailableHosts();
|
|
@@ -4208,12 +4208,12 @@ export async function main(argv) {
|
|
|
4208
4208
|
const detail = `no preferred host available on PATH (wanted ${request.hostPref.join("/")}, found ${[...available].join("/") || "none"})`;
|
|
4209
4209
|
if (confirm)
|
|
4210
4210
|
markLaunchEnvelopeProcessed(env.path, `skip: ${detail}`);
|
|
4211
|
-
process.stderr.write(`[
|
|
4211
|
+
process.stderr.write(`[h2a] conductor-launch: SKIP — ${detail}\n`);
|
|
4212
4212
|
return { launched: false, detail };
|
|
4213
4213
|
}
|
|
4214
4214
|
const task = buildConductorTask(request);
|
|
4215
4215
|
if (!confirm) {
|
|
4216
|
-
process.stderr.write(`[
|
|
4216
|
+
process.stderr.write(`[h2a] conductor-launch DRY-RUN — would launch a ${host} conductor "${slug}" ` +
|
|
4217
4217
|
`for ${request.workspaceId} (${request.stalled.length} stalled item(s)). ` +
|
|
4218
4218
|
`Re-run with --confirm to launch.\n`);
|
|
4219
4219
|
return { launched: false, detail: `dry-run (${host})` };
|
|
@@ -4258,18 +4258,18 @@ export async function main(argv) {
|
|
|
4258
4258
|
: `launch-failed: ${result.error}`);
|
|
4259
4259
|
if (!result.started) {
|
|
4260
4260
|
advanceJob(slug, "failed");
|
|
4261
|
-
process.stderr.write(`[
|
|
4261
|
+
process.stderr.write(`[h2a] conductor-launch: FAILED to launch ${slug}: ${result.error}\n`);
|
|
4262
4262
|
return { launched: false, detail: result.error };
|
|
4263
4263
|
}
|
|
4264
|
-
process.stderr.write(`[
|
|
4265
|
-
`[
|
|
4264
|
+
process.stderr.write(`[h2a] conductor-launch: launched ${host} conductor ${slug} for ${request.workspaceId} in ${result.detail}\n` +
|
|
4265
|
+
`[h2a] supervise: remote jobs status ${slug} attach: remote jobs attach ${slug}\n`);
|
|
4266
4266
|
return { launched: true, detail: `${host} ${slug}` };
|
|
4267
4267
|
};
|
|
4268
4268
|
/** One pass: handle the freshest unprocessed launch envelope (or report none). */
|
|
4269
4269
|
const launchPass = async (confirm, cooldownMs) => {
|
|
4270
4270
|
const fresh = freshestLaunchEnvelope(readLaunchEnvelopes());
|
|
4271
4271
|
if (!fresh) {
|
|
4272
|
-
process.stderr.write("[
|
|
4272
|
+
process.stderr.write("[h2a] conductor-launch: no unprocessed conductor-launch-request in the h2a inbox\n");
|
|
4273
4273
|
return { started: 0, finished: 0 };
|
|
4274
4274
|
}
|
|
4275
4275
|
const r = await processLaunchRequest(fresh, confirm, cooldownMs);
|
|
@@ -4277,7 +4277,7 @@ export async function main(argv) {
|
|
|
4277
4277
|
};
|
|
4278
4278
|
program
|
|
4279
4279
|
.command("conductor-launch")
|
|
4280
|
-
.description("Handle an h2a `conductor-launch-request` envelope: when h2a reports stalled work and no live conductor, launch one (claude/codex/agy, first available on PATH) via the same delegation path as `
|
|
4280
|
+
.description("Handle an h2a `conductor-launch-request` envelope: when h2a reports stalled work and no live conductor, launch one (claude/codex/agy, first available on PATH) via the same delegation path as `h2a delegate`, instructed to claim the conductor role at boot. DRY-RUN unless --confirm. Idempotent (skips when a conductor is already alive) and rate-limited per workspace (cooldown). One pass by default; --watch loops in the FOREGROUND (no daemon; Ctrl-C to stop).")
|
|
4281
4281
|
.option("--confirm", "actually launch (default: dry-run — decide and print what WOULD launch, launch nothing)")
|
|
4282
4282
|
.option("--watch <minutes>", "loop in the foreground every <minutes> (whole number >= 1); without it, runs a single pass and exits")
|
|
4283
4283
|
.option("--cooldown <min>", "minimum minutes between launches for the SAME workspace (default 30)")
|
|
@@ -4290,13 +4290,13 @@ export async function main(argv) {
|
|
|
4290
4290
|
const cooldownMs = cooldownMin * 60_000;
|
|
4291
4291
|
const minutes = opts.watch === undefined ? undefined : parseWatchMinutes(opts.watch);
|
|
4292
4292
|
if (!confirm) {
|
|
4293
|
-
process.stderr.write("[
|
|
4293
|
+
process.stderr.write("[h2a] conductor-launch: DRY-RUN (no --confirm) — nothing will be launched\n");
|
|
4294
4294
|
}
|
|
4295
4295
|
if (minutes === undefined) {
|
|
4296
4296
|
await launchPass(confirm, cooldownMs);
|
|
4297
4297
|
return;
|
|
4298
4298
|
}
|
|
4299
|
-
process.stderr.write(`[
|
|
4299
|
+
process.stderr.write(`[h2a] conductor-launch watching (every ${minutes} min, cooldown ${cooldownMin} min${confirm ? "" : ", DRY-RUN"}) — Ctrl-C to stop\n`);
|
|
4300
4300
|
process.exitCode = await conductLoop(minutes, () => launchPass(confirm, cooldownMs));
|
|
4301
4301
|
});
|
|
4302
4302
|
// ---------------------------------------------------------------------------
|
|
@@ -4417,12 +4417,12 @@ export async function main(argv) {
|
|
|
4417
4417
|
const lastWake = readWakeStampMs(localRoot, target);
|
|
4418
4418
|
if (lastWake !== undefined &&
|
|
4419
4419
|
now - lastWake < WAKE_REQUEST_IDEMPOTENCE_MS) {
|
|
4420
|
-
process.stderr.write(`[
|
|
4420
|
+
process.stderr.write(`[h2a] wake-request: skipping ${target} (already woken ${Math.round((now - lastWake) / 1000)}s ago)\n`);
|
|
4421
4421
|
continue;
|
|
4422
4422
|
}
|
|
4423
4423
|
const pane = resolveAgentPaneForInstance(target);
|
|
4424
4424
|
if (!pane) {
|
|
4425
|
-
process.stderr.write(`[
|
|
4425
|
+
process.stderr.write(`[h2a] wake-request: no agent pane for ${target} — not launched by remote, skipping (reason: ${reason || "none"})\n`);
|
|
4426
4426
|
continue;
|
|
4427
4427
|
}
|
|
4428
4428
|
// Guard (h2a 0.74.0 parity): defer send-keys if a human was active in the
|
|
@@ -4441,7 +4441,7 @@ export async function main(argv) {
|
|
|
4441
4441
|
.map((s) => Number.parseInt(s.trim(), 10) * 1000)
|
|
4442
4442
|
.filter((n) => !Number.isNaN(n)));
|
|
4443
4443
|
if (maxActivity > now - 4000) {
|
|
4444
|
-
process.stderr.write(`[
|
|
4444
|
+
process.stderr.write(`[h2a] wake-request: deferring ${target} — human active ${Math.round((now - maxActivity) / 1000)}s ago, retrying next pass\n`);
|
|
4445
4445
|
continue;
|
|
4446
4446
|
}
|
|
4447
4447
|
}
|
|
@@ -4466,7 +4466,7 @@ export async function main(argv) {
|
|
|
4466
4466
|
markLaunchEnvelopeProcessed(envelopePath, `woke ${target}`);
|
|
4467
4467
|
writeWakeStampMs(localRoot, target, now);
|
|
4468
4468
|
woken += 1;
|
|
4469
|
-
process.stderr.write(`[
|
|
4469
|
+
process.stderr.write(`[h2a] wake-request: woke ${target} → pane ${pane} (reason: ${reason || "none"})\n`);
|
|
4470
4470
|
}
|
|
4471
4471
|
return woken;
|
|
4472
4472
|
};
|
|
@@ -4482,7 +4482,7 @@ export async function main(argv) {
|
|
|
4482
4482
|
.option("--interval <seconds>", "poll interval in seconds (default: 30)", "30")
|
|
4483
4483
|
.action(async (opts) => {
|
|
4484
4484
|
if (!tmuxAvailable()) {
|
|
4485
|
-
process.stderr.write("[
|
|
4485
|
+
process.stderr.write("[h2a] tmux is not installed locally\n");
|
|
4486
4486
|
process.exitCode = 1;
|
|
4487
4487
|
return;
|
|
4488
4488
|
}
|
|
@@ -4490,10 +4490,10 @@ export async function main(argv) {
|
|
|
4490
4490
|
const intervalMs = Math.max(1, Number.parseInt(opts.interval ?? "30", 10)) * 1000;
|
|
4491
4491
|
if (!opts.watch) {
|
|
4492
4492
|
const woken = wakeRequestPass(localRoot);
|
|
4493
|
-
process.stderr.write(`[
|
|
4493
|
+
process.stderr.write(`[h2a] wake-request: ${woken} pane(s) woken this pass\n`);
|
|
4494
4494
|
return;
|
|
4495
4495
|
}
|
|
4496
|
-
process.stderr.write(`[
|
|
4496
|
+
process.stderr.write(`[h2a] wake-request watching (every ${Math.round(intervalMs / 1000)}s) — Ctrl-C to stop\n`);
|
|
4497
4497
|
let stopped = false;
|
|
4498
4498
|
const onSigint = () => {
|
|
4499
4499
|
stopped = true;
|
|
@@ -4502,7 +4502,7 @@ export async function main(argv) {
|
|
|
4502
4502
|
try {
|
|
4503
4503
|
while (!stopped) {
|
|
4504
4504
|
const woken = wakeRequestPass(localRoot);
|
|
4505
|
-
process.stderr.write(`[
|
|
4505
|
+
process.stderr.write(`[h2a] wake-request: ${woken} pane(s) woken — ${new Date().toISOString()}\n`);
|
|
4506
4506
|
if (stopped)
|
|
4507
4507
|
break;
|
|
4508
4508
|
await new Promise((resolve) => {
|
|
@@ -4517,7 +4517,7 @@ export async function main(argv) {
|
|
|
4517
4517
|
finally {
|
|
4518
4518
|
process.removeListener("SIGINT", onSigint);
|
|
4519
4519
|
}
|
|
4520
|
-
process.stderr.write("[
|
|
4520
|
+
process.stderr.write("[h2a] wake-request watch stopped\n");
|
|
4521
4521
|
});
|
|
4522
4522
|
// ---------------------------------------------------------------------------
|
|
4523
4523
|
// relaunch — bring idle local sessions back in situ, each resuming its OWN conv
|
|
@@ -4528,7 +4528,7 @@ export async function main(argv) {
|
|
|
4528
4528
|
.option("--apply", "actually relaunch (default: dry-run, just print the plan)")
|
|
4529
4529
|
.action((filter, opts) => {
|
|
4530
4530
|
if (!tmuxAvailable()) {
|
|
4531
|
-
process.stderr.write("[
|
|
4531
|
+
process.stderr.write("[h2a] tmux is not installed locally\n");
|
|
4532
4532
|
process.exitCode = 1;
|
|
4533
4533
|
return;
|
|
4534
4534
|
}
|
|
@@ -4549,14 +4549,14 @@ export async function main(argv) {
|
|
|
4549
4549
|
: {}),
|
|
4550
4550
|
})));
|
|
4551
4551
|
if (plan.actions.length === 0) {
|
|
4552
|
-
process.stderr.write(`[
|
|
4552
|
+
process.stderr.write(`[h2a] nothing to relaunch${filter ? ` matching "${filter}"` : ""} (${plan.skipped.length} skipped)\n`);
|
|
4553
4553
|
for (const s of plan.skipped) {
|
|
4554
4554
|
process.stderr.write(` - ${s.slug}: ${s.reason}\n`);
|
|
4555
4555
|
}
|
|
4556
4556
|
return;
|
|
4557
4557
|
}
|
|
4558
4558
|
if (!opts.apply) {
|
|
4559
|
-
process.stderr.write(`[
|
|
4559
|
+
process.stderr.write(`[h2a] would relaunch ${plan.actions.length} session(s) — dry-run, pass --apply:\n`);
|
|
4560
4560
|
for (const a of plan.actions) {
|
|
4561
4561
|
process.stderr.write(` ${a.slug}: ${a.cmd}\n`);
|
|
4562
4562
|
}
|
|
@@ -4569,13 +4569,13 @@ export async function main(argv) {
|
|
|
4569
4569
|
for (const a of plan.actions) {
|
|
4570
4570
|
if (relaunchInSession(a.name, a.cmd)) {
|
|
4571
4571
|
ok += 1;
|
|
4572
|
-
process.stderr.write(`[
|
|
4572
|
+
process.stderr.write(`[h2a] relaunched ${a.slug}: ${a.cmd}\n`);
|
|
4573
4573
|
}
|
|
4574
4574
|
else {
|
|
4575
|
-
process.stderr.write(`[
|
|
4575
|
+
process.stderr.write(`[h2a] FAILED to relaunch ${a.slug}\n`);
|
|
4576
4576
|
}
|
|
4577
4577
|
}
|
|
4578
|
-
process.stderr.write(`[
|
|
4578
|
+
process.stderr.write(`[h2a] relaunched ${ok}/${plan.actions.length}${plan.skipped.length ? `, ${plan.skipped.length} skipped` : ""}\n`);
|
|
4579
4579
|
});
|
|
4580
4580
|
// ---------------------------------------------------------------------------
|
|
4581
4581
|
// Reliability slice 2 (throttle phase 2) — auto-resume RATE-LIMITED INTERACTIVE
|
|
@@ -4675,17 +4675,17 @@ export async function main(argv) {
|
|
|
4675
4675
|
// already excluded attached panes, but re-read in case a human just
|
|
4676
4676
|
// attached between capture and nudge — belt and braces on a live pane).
|
|
4677
4677
|
if (!isDetachedNow(r.name)) {
|
|
4678
|
-
process.stderr.write(`[
|
|
4678
|
+
process.stderr.write(`[h2a] ${r.name} (${r.type}) just got attached — skipping the nudge\n`);
|
|
4679
4679
|
continue;
|
|
4680
4680
|
}
|
|
4681
4681
|
const ok = relaunchInSession(r.name, interactiveResumeNudge(r.type));
|
|
4682
4682
|
if (ok) {
|
|
4683
4683
|
nudged += 1;
|
|
4684
4684
|
throttleState.set(r.name, r.next);
|
|
4685
|
-
process.stderr.write(`[
|
|
4685
|
+
process.stderr.write(`[h2a] nudged ${r.name} (${r.type}) — continue (attempt ${r.next.attempts})\n`);
|
|
4686
4686
|
}
|
|
4687
4687
|
else {
|
|
4688
|
-
process.stderr.write(`[
|
|
4688
|
+
process.stderr.write(`[h2a] FAILED to nudge ${r.name}\n`);
|
|
4689
4689
|
}
|
|
4690
4690
|
}
|
|
4691
4691
|
}
|
|
@@ -4699,7 +4699,7 @@ export async function main(argv) {
|
|
|
4699
4699
|
.option("--max-concurrent <n>", "AIMD cap: max sessions nudged per pass (default: config maxConcurrent / REMOTE_MAX_CONCURRENT / 16)")
|
|
4700
4700
|
.action(async (filter, opts) => {
|
|
4701
4701
|
if (!tmuxAvailable()) {
|
|
4702
|
-
process.stderr.write("[
|
|
4702
|
+
process.stderr.write("[h2a] tmux is not installed locally\n");
|
|
4703
4703
|
process.exitCode = 1;
|
|
4704
4704
|
return;
|
|
4705
4705
|
}
|
|
@@ -4719,11 +4719,11 @@ export async function main(argv) {
|
|
|
4719
4719
|
for (const line of plan.advisories)
|
|
4720
4720
|
process.stderr.write(`${line}\n`);
|
|
4721
4721
|
if (!apply) {
|
|
4722
|
-
process.stderr.write(`[
|
|
4722
|
+
process.stderr.write(`[h2a] DRY-RUN — would resume ${plan.toResume.length} session(s) ` +
|
|
4723
4723
|
`(${plan.throttled.length} throttled, cap ${cap}). Pass --apply to nudge them.\n`);
|
|
4724
4724
|
}
|
|
4725
4725
|
else {
|
|
4726
|
-
process.stderr.write(`[
|
|
4726
|
+
process.stderr.write(`[h2a] resume-throttled: nudged ${nudged}/${plan.toResume.length} ` +
|
|
4727
4727
|
`(${plan.throttled.length} throttled, cap ${cap})\n`);
|
|
4728
4728
|
}
|
|
4729
4729
|
return { failed: 0 };
|
|
@@ -4732,7 +4732,7 @@ export async function main(argv) {
|
|
|
4732
4732
|
runOnce();
|
|
4733
4733
|
return;
|
|
4734
4734
|
}
|
|
4735
|
-
process.stderr.write(`[
|
|
4735
|
+
process.stderr.write(`[h2a] resume-throttled watching (cap ${cap}, every ${minutes} min) — Ctrl-C to stop\n`);
|
|
4736
4736
|
process.exitCode = await watchRefreshLoop(minutes, async () => runOnce());
|
|
4737
4737
|
});
|
|
4738
4738
|
// ---------------------------------------------------------------------------
|
|
@@ -4756,14 +4756,14 @@ export async function main(argv) {
|
|
|
4756
4756
|
cwd: process.cwd(),
|
|
4757
4757
|
...(opts.root !== undefined ? { localRoot: opts.root } : {}),
|
|
4758
4758
|
});
|
|
4759
|
-
process.stderr.write(`[
|
|
4759
|
+
process.stderr.write(`[h2a] h2a ping ${ping.written ? "queued" : "already queued"} for ${instance}: ${ping.path}\n`);
|
|
4760
4760
|
if (opts.bridge === undefined)
|
|
4761
4761
|
return;
|
|
4762
4762
|
const sessionId = typeof opts.bridge === "string"
|
|
4763
4763
|
? opts.bridge
|
|
4764
4764
|
: remoteSessionIdFromInstance(instance);
|
|
4765
4765
|
if (!sessionId) {
|
|
4766
|
-
process.stderr.write("[
|
|
4766
|
+
process.stderr.write("[h2a] --bridge needs a session id, or an instance shaped like <tool>:remote:<sessionId>\n");
|
|
4767
4767
|
process.exitCode = 1;
|
|
4768
4768
|
return;
|
|
4769
4769
|
}
|
|
@@ -4774,13 +4774,13 @@ export async function main(argv) {
|
|
|
4774
4774
|
const { bridgeSession } = await import("./h2a-bridge.js");
|
|
4775
4775
|
try {
|
|
4776
4776
|
const r = await bridgeSession(sessionId, { profile });
|
|
4777
|
-
process.stderr.write(`[
|
|
4777
|
+
process.stderr.write(`[h2a] h2a bridge ${sessionId} (${profile}) pulled=${r.pulled} pushed=${r.pushed} skipped=${r.skipped}` +
|
|
4778
4778
|
`${r.failed > 0 ? ` failed=${r.failed}` : ""}\n`);
|
|
4779
4779
|
if (r.failed > 0)
|
|
4780
4780
|
process.exitCode = 1;
|
|
4781
4781
|
}
|
|
4782
4782
|
catch (error) {
|
|
4783
|
-
process.stderr.write(`[
|
|
4783
|
+
process.stderr.write(`[h2a] h2a bridge ${sessionId} failed: ${(error instanceof Error ? error.message : String(error)).slice(0, 200)}\n`);
|
|
4784
4784
|
process.exitCode = 1;
|
|
4785
4785
|
}
|
|
4786
4786
|
});
|
|
@@ -4821,7 +4821,7 @@ export async function main(argv) {
|
|
|
4821
4821
|
profile: s.profile,
|
|
4822
4822
|
}));
|
|
4823
4823
|
if (sessions.length === 0) {
|
|
4824
|
-
process.stderr.write("[
|
|
4824
|
+
process.stderr.write("[h2a] no live remote sessions to bridge\n");
|
|
4825
4825
|
return { failed: 0 };
|
|
4826
4826
|
}
|
|
4827
4827
|
let failed = 0;
|
|
@@ -4841,13 +4841,13 @@ export async function main(argv) {
|
|
|
4841
4841
|
const r = await bridgeSession(s.id, { profile: s.profile });
|
|
4842
4842
|
if (r.failed > 0)
|
|
4843
4843
|
failed += 1;
|
|
4844
|
-
process.stderr.write(`[
|
|
4844
|
+
process.stderr.write(`[h2a] h2a bridge ${s.id} (${s.profile}) pulled=${r.pulled} pushed=${r.pushed} skipped=${r.skipped}` +
|
|
4845
4845
|
`${r.failed > 0 ? ` failed=${r.failed}` : ""}` +
|
|
4846
4846
|
`${r.scaffolded ? " (pod .h2a scaffolded)" : ""}\n`);
|
|
4847
4847
|
}
|
|
4848
4848
|
catch (error) {
|
|
4849
4849
|
failed += 1;
|
|
4850
|
-
process.stderr.write(`[
|
|
4850
|
+
process.stderr.write(`[h2a] h2a bridge ${s.id} failed: ${(error instanceof Error ? error.message : String(error)).slice(0, 200)}\n`);
|
|
4851
4851
|
}
|
|
4852
4852
|
}
|
|
4853
4853
|
return { failed };
|
|
@@ -4862,7 +4862,7 @@ export async function main(argv) {
|
|
|
4862
4862
|
});
|
|
4863
4863
|
program
|
|
4864
4864
|
.command("restore [group]")
|
|
4865
|
-
.description('Relance les sessions dev dans leur layout (fenêtre par groupe, onglet par session). Sans argument: tous les groupes. Avec [group]: ce lot seulement (ex: `
|
|
4865
|
+
.description('Relance les sessions dev dans leur layout (fenêtre par groupe, onglet par session). Sans argument: tous les groupes. Avec [group]: ce lot seulement (ex: `h2a restore "full remote"`). Groupes LOCAUX = claude/codex sous ~/src/* (tmux via `h2a run`); groupes REMOTE = sessions SCW (`h2a attach <id> --exec`). Layout: champ `layout` de la config.')
|
|
4866
4866
|
.option("--dry-run", "affiche le layout calculé sans ouvrir de terminaux")
|
|
4867
4867
|
.option("--reattach", "ouvre aussi un onglet pour les sessions déjà actives en tmux (ré-attache)")
|
|
4868
4868
|
.option("--llm-gateway", "force TOUTES les sessions sur la gateway llm-mesh (ignore le pin par instance; relance les sessions vivantes via --replace)")
|
|
@@ -4871,7 +4871,7 @@ export async function main(argv) {
|
|
|
4871
4871
|
.option("--no-gw", "alias de --no-llm-gateway")
|
|
4872
4872
|
.action(async (group, opts) => {
|
|
4873
4873
|
if (!tmuxAvailable()) {
|
|
4874
|
-
process.stderr.write("[
|
|
4874
|
+
process.stderr.write("[h2a] tmux requis pour restore (sudo apt install tmux)\n");
|
|
4875
4875
|
process.exitCode = 1;
|
|
4876
4876
|
return;
|
|
4877
4877
|
}
|
|
@@ -4896,7 +4896,7 @@ export async function main(argv) {
|
|
|
4896
4896
|
forceGateway = mode;
|
|
4897
4897
|
}
|
|
4898
4898
|
catch (error) {
|
|
4899
|
-
process.stderr.write(`[
|
|
4899
|
+
process.stderr.write(`[h2a] ${error.message}.\n`);
|
|
4900
4900
|
process.exitCode = 1;
|
|
4901
4901
|
return;
|
|
4902
4902
|
}
|
|
@@ -4912,7 +4912,7 @@ export async function main(argv) {
|
|
|
4912
4912
|
return { id: s.id, label: projectName(s), cwd };
|
|
4913
4913
|
});
|
|
4914
4914
|
if (restoreOpts.remoteTabs.length === 0) {
|
|
4915
|
-
process.stderr.write("[
|
|
4915
|
+
process.stderr.write("[h2a] aucune session SCW (h2a ls vide) pour le groupe remote\n");
|
|
4916
4916
|
}
|
|
4917
4917
|
}
|
|
4918
4918
|
if (opts.reattach)
|
|
@@ -4924,10 +4924,10 @@ export async function main(argv) {
|
|
|
4924
4924
|
}
|
|
4925
4925
|
const { total } = restoreLayout(restoreOpts);
|
|
4926
4926
|
if (total === 0) {
|
|
4927
|
-
process.stderr.write(`[
|
|
4927
|
+
process.stderr.write(`[h2a] rien à relancer${group ? ` pour le groupe "${group}"` : ""}\n`);
|
|
4928
4928
|
}
|
|
4929
4929
|
else {
|
|
4930
|
-
process.stderr.write(`[
|
|
4930
|
+
process.stderr.write(`[h2a] ${total} onglet(s)${opts.dryRun ? " (dry-run, rien ouvert)" : " relancé(s)"}\n`);
|
|
4931
4931
|
}
|
|
4932
4932
|
});
|
|
4933
4933
|
// ---------------------------------------------------------------------------
|
|
@@ -4935,11 +4935,11 @@ export async function main(argv) {
|
|
|
4935
4935
|
// ---------------------------------------------------------------------------
|
|
4936
4936
|
program
|
|
4937
4937
|
.command("enroll")
|
|
4938
|
-
.description("Plumbing for the live-session registry (feeds `
|
|
4938
|
+
.description("Plumbing for the live-session registry (feeds `h2a ls`/`h2a restore`). " +
|
|
4939
4939
|
"Hook mode (--hook claude-start|claude-end) is wired by --install-hooks into " +
|
|
4940
4940
|
"~/.claude/settings.json (idempotent; backs up settings.json.bak.<epoch>) and " +
|
|
4941
4941
|
"always exits 0 so it can never break the host claude session. " +
|
|
4942
|
-
"codex has no reliable session hook: codex sessions are enrolled by `
|
|
4942
|
+
"codex has no reliable session hook: codex sessions are enrolled by `h2a run` " +
|
|
4943
4943
|
"and by the restore filesystem-scan fallback. Manual mode: --tool/--cwd/--conv/--pid/--label.")
|
|
4944
4944
|
.option("--hook <name>", "hook mode: claude-start | claude-end (reads the Claude Code hook JSON on stdin)")
|
|
4945
4945
|
.option("--install-hooks", "merge the SessionStart/SessionEnd enroll hooks into Claude Code's settings.json (idempotent)")
|
|
@@ -4953,13 +4953,13 @@ export async function main(argv) {
|
|
|
4953
4953
|
if (opts.installHooks) {
|
|
4954
4954
|
const result = installClaudeHooks(opts.settings);
|
|
4955
4955
|
if (!result.changed) {
|
|
4956
|
-
process.stderr.write(`[
|
|
4956
|
+
process.stderr.write(`[h2a] enroll hooks already installed in ${result.settingsPath}\n`);
|
|
4957
4957
|
return;
|
|
4958
4958
|
}
|
|
4959
4959
|
if (result.backupPath) {
|
|
4960
|
-
process.stderr.write(`[
|
|
4960
|
+
process.stderr.write(`[h2a] backup: ${result.backupPath}\n`);
|
|
4961
4961
|
}
|
|
4962
|
-
process.stderr.write(`[
|
|
4962
|
+
process.stderr.write(`[h2a] installed ${result.installed.join(" + ")} enroll hooks in ${result.settingsPath}\n`);
|
|
4963
4963
|
return;
|
|
4964
4964
|
}
|
|
4965
4965
|
if (opts.hook) {
|
|
@@ -4968,11 +4968,11 @@ export async function main(argv) {
|
|
|
4968
4968
|
const raw = await readStdin();
|
|
4969
4969
|
const result = handleClaudeHook(opts.hook, raw);
|
|
4970
4970
|
if (!result.ok) {
|
|
4971
|
-
process.stderr.write(`[
|
|
4971
|
+
process.stderr.write(`[h2a] enroll hook ignored: ${result.error}\n`);
|
|
4972
4972
|
}
|
|
4973
4973
|
}
|
|
4974
4974
|
catch (error) {
|
|
4975
|
-
process.stderr.write(`[
|
|
4975
|
+
process.stderr.write(`[h2a] enroll hook ignored: ${String(error)}\n`);
|
|
4976
4976
|
}
|
|
4977
4977
|
return;
|
|
4978
4978
|
}
|
|
@@ -4986,21 +4986,21 @@ export async function main(argv) {
|
|
|
4986
4986
|
});
|
|
4987
4987
|
if (!result.ok)
|
|
4988
4988
|
throw new Error(result.error ?? "enroll failed");
|
|
4989
|
-
process.stderr.write(`[
|
|
4989
|
+
process.stderr.write(`[h2a] enrolled (${opts.tool})\n`);
|
|
4990
4990
|
return;
|
|
4991
4991
|
}
|
|
4992
|
-
process.stderr.write("[
|
|
4992
|
+
process.stderr.write("[h2a] enroll: pass --hook <name>, --install-hooks, or --tool <tool> (see --help)\n");
|
|
4993
4993
|
});
|
|
4994
4994
|
const layoutCommand = program
|
|
4995
4995
|
.command("layout")
|
|
4996
|
-
.description("Layout auto-enregistré par `
|
|
4996
|
+
.description("Layout auto-enregistré par `h2a restore` (layout-last.json)");
|
|
4997
4997
|
layoutCommand
|
|
4998
4998
|
.command("show")
|
|
4999
4999
|
.description("Affiche le dernier layout lancé (fenêtres, onglets, commandes)")
|
|
5000
5000
|
.action(() => {
|
|
5001
5001
|
const last = readLastLayout();
|
|
5002
5002
|
if (!last) {
|
|
5003
|
-
process.stderr.write("[
|
|
5003
|
+
process.stderr.write("[h2a] aucun layout enregistré (lance `h2a restore` d'abord)\n");
|
|
5004
5004
|
return;
|
|
5005
5005
|
}
|
|
5006
5006
|
process.stdout.write(`at: ${last.at}\n`);
|
|
@@ -5023,13 +5023,22 @@ export async function main(argv) {
|
|
|
5023
5023
|
// Local tmux session? (unless an explicit URL/sessionId pair is given).
|
|
5024
5024
|
if (second === undefined && !looksLikeUrl(first)) {
|
|
5025
5025
|
const local = findLocalSession(first);
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5026
|
+
// A session started by `remote run` is enrolled in the registry as
|
|
5027
|
+
// kind:"local-tmux". Trust that durable record even when a transient
|
|
5028
|
+
// `tmux list-sessions` miss hides the live session — otherwise a purely
|
|
5029
|
+
// LOCAL session is mis-routed to a k8s Pod (kubectl exec session-<name>
|
|
5030
|
+
// → NotFound → reconnect loop) as soon as a tunnel is configured.
|
|
5031
|
+
// `remote ls` already merges registry + tmux; attach must not be less
|
|
5032
|
+
// reliable. When the registry says local but tmux can't reach it,
|
|
5033
|
+
// attachLocalSession fails with a clear LOCAL error (never a Pod).
|
|
5034
|
+
const localName = local?.name ?? localTmuxSessionForName(first, loadRegistry());
|
|
5035
|
+
if (opts.local || localName) {
|
|
5036
|
+
if (!localName) {
|
|
5037
|
+
process.stderr.write(`[h2a] no local session "${first}" (see: h2a ls)\n`);
|
|
5029
5038
|
process.exitCode = 1;
|
|
5030
5039
|
return;
|
|
5031
5040
|
}
|
|
5032
|
-
process.exitCode = attachLocalSession(
|
|
5041
|
+
process.exitCode = attachLocalSession(localName);
|
|
5033
5042
|
return;
|
|
5034
5043
|
}
|
|
5035
5044
|
}
|
|
@@ -5052,15 +5061,15 @@ export async function main(argv) {
|
|
|
5052
5061
|
// fallback. Force either path with --exec / --ws.
|
|
5053
5062
|
if (!opts.ws && (opts.exec || tunnel)) {
|
|
5054
5063
|
if (!tunnel) {
|
|
5055
|
-
process.stderr.write("[
|
|
5064
|
+
process.stderr.write("[h2a] --exec needs a tunnel configured (h2a config tunnel …)\n");
|
|
5056
5065
|
process.exitCode = 1;
|
|
5057
5066
|
return;
|
|
5058
5067
|
}
|
|
5059
|
-
process.stderr.write(`[
|
|
5068
|
+
process.stderr.write(`[h2a] exec-attaching into Pod tmux for ${sessionId} (Ctrl-b d to detach)\n`);
|
|
5060
5069
|
process.exitCode = attachPodTmux(tunnel, sessionId);
|
|
5061
5070
|
return;
|
|
5062
5071
|
}
|
|
5063
|
-
process.stderr.write(`[
|
|
5072
|
+
process.stderr.write(`[h2a] attaching to ${url}/sessions/${sessionId} (WS)\n`);
|
|
5064
5073
|
const session = await attach({ baseUrl: url, sessionId });
|
|
5065
5074
|
await session.finished;
|
|
5066
5075
|
});
|
|
@@ -5068,7 +5077,7 @@ export async function main(argv) {
|
|
|
5068
5077
|
.command("refresh [urlOrSessionId] [sessionId]")
|
|
5069
5078
|
.description("Re-bundle the local CLI credentials and push them to an existing remote session's Secret. The session's Pod is restarted by the control-plane so the new tokens take effect. Profile is auto-detected from the session.")
|
|
5070
5079
|
.option("--profile <profile>", "override the auto-detected profile (rarely needed)")
|
|
5071
|
-
.option("--name <name>", "set the session's display name (shown as PROJECT in `
|
|
5080
|
+
.option("--name <name>", "set the session's display name (shown as PROJECT in `h2a ls`); applied on the hard refresh (Pod recreate), ignored with --soft")
|
|
5072
5081
|
.option("--no-auth", "skip bundling local credentials")
|
|
5073
5082
|
.option("--no-auth-refresh", "skip local auth status preflight before bundling credentials")
|
|
5074
5083
|
.option("--soft", "push fresh creds INTO the running Pod + relaunch the CLI in place (no Pod recreate; keeps HOME + conversation; fixes the ~8h token logout)")
|
|
@@ -5077,12 +5086,12 @@ export async function main(argv) {
|
|
|
5077
5086
|
.addHelpText("after", [
|
|
5078
5087
|
"",
|
|
5079
5088
|
"Examples:",
|
|
5080
|
-
" $
|
|
5081
|
-
" $
|
|
5082
|
-
" $
|
|
5089
|
+
" $ h2a refresh sess-abc --soft refresh one session's creds in place",
|
|
5090
|
+
" $ h2a refresh --soft --all refresh every live session once",
|
|
5091
|
+
" $ h2a refresh --all --watch 30 foreground loop: every 30 min, all sessions;",
|
|
5083
5092
|
" unchanged creds = no-op (no CLI respawn)",
|
|
5084
5093
|
" Run the watch in a dedicated tmux window, e.g.:",
|
|
5085
|
-
" tmux new-window -n creds '
|
|
5094
|
+
" tmux new-window -n creds 'h2a refresh --all --watch 30'",
|
|
5086
5095
|
"",
|
|
5087
5096
|
].join("\n"))
|
|
5088
5097
|
.action(async (first, second, opts) => {
|
|
@@ -5116,7 +5125,7 @@ export async function main(argv) {
|
|
|
5116
5125
|
return;
|
|
5117
5126
|
}
|
|
5118
5127
|
if (first === undefined) {
|
|
5119
|
-
throw new Error("Missing session id. Usage:
|
|
5128
|
+
throw new Error("Missing session id. Usage: h2a refresh [url] <sessionId> [--soft] (or --soft --all).");
|
|
5120
5129
|
}
|
|
5121
5130
|
const { url, sessionId } = resolveUrlAndSessionId(first, second);
|
|
5122
5131
|
if (watchMinutes !== undefined) {
|
|
@@ -5137,7 +5146,7 @@ export async function main(argv) {
|
|
|
5137
5146
|
if (opts.authRefresh !== false) {
|
|
5138
5147
|
const fresh = await ensureProfileAuthFresh(resolved);
|
|
5139
5148
|
if (fresh.checked)
|
|
5140
|
-
process.stderr.write(`[
|
|
5149
|
+
process.stderr.write(`[h2a] auth status ok: ${fresh.command}\n`);
|
|
5141
5150
|
}
|
|
5142
5151
|
await softRefreshSession(sessionId, resolved);
|
|
5143
5152
|
return;
|
|
@@ -5164,13 +5173,13 @@ export async function main(argv) {
|
|
|
5164
5173
|
}
|
|
5165
5174
|
if (opts.local) {
|
|
5166
5175
|
if (local.length === 0)
|
|
5167
|
-
process.stderr.write("[
|
|
5176
|
+
process.stderr.write("[h2a] no local sessions\n");
|
|
5168
5177
|
return;
|
|
5169
5178
|
}
|
|
5170
5179
|
const remote = url ?? getConfiguredRemoteOptional();
|
|
5171
5180
|
if (!remote) {
|
|
5172
5181
|
if (local.length === 0) {
|
|
5173
|
-
process.stderr.write("[
|
|
5182
|
+
process.stderr.write("[h2a] no local sessions and no remote configured\n");
|
|
5174
5183
|
}
|
|
5175
5184
|
return;
|
|
5176
5185
|
}
|
|
@@ -5178,7 +5187,7 @@ export async function main(argv) {
|
|
|
5178
5187
|
const sessions = await listRemoteSessions(remote);
|
|
5179
5188
|
if (sessions.length === 0) {
|
|
5180
5189
|
if (local.length === 0)
|
|
5181
|
-
process.stderr.write("[
|
|
5190
|
+
process.stderr.write("[h2a] no sessions\n");
|
|
5182
5191
|
return;
|
|
5183
5192
|
}
|
|
5184
5193
|
if (local.length > 0)
|
|
@@ -5208,7 +5217,7 @@ export async function main(argv) {
|
|
|
5208
5217
|
const local = findLocalSession(first);
|
|
5209
5218
|
if (local) {
|
|
5210
5219
|
const ok = killLocalSession(local.name);
|
|
5211
|
-
process.stderr.write(`[
|
|
5220
|
+
process.stderr.write(`[h2a] local session ${local.slug} ${ok ? "killed" : "could not be killed"}\n`);
|
|
5212
5221
|
if (!ok)
|
|
5213
5222
|
process.exitCode = 1;
|
|
5214
5223
|
return;
|
|
@@ -5216,7 +5225,7 @@ export async function main(argv) {
|
|
|
5216
5225
|
}
|
|
5217
5226
|
const { url, sessionId } = resolveUrlAndSessionId(first, second);
|
|
5218
5227
|
const result = await stopRemoteSession(url, sessionId, opts.reason);
|
|
5219
|
-
process.stderr.write(`[
|
|
5228
|
+
process.stderr.write(`[h2a] stop ${result.accepted ? "accepted" : "rejected"} for ${sessionId}\n`);
|
|
5220
5229
|
});
|
|
5221
5230
|
program
|
|
5222
5231
|
.command("rename <slugOrId> <newName>")
|
|
@@ -5229,7 +5238,7 @@ export async function main(argv) {
|
|
|
5229
5238
|
const local = findLocalSession(slugOrId);
|
|
5230
5239
|
if (local) {
|
|
5231
5240
|
setLocalSessionDisplayName(local.name, newName);
|
|
5232
|
-
process.stderr.write(`[
|
|
5241
|
+
process.stderr.write(`[h2a] local session display name set to "${newName}"\n`);
|
|
5233
5242
|
}
|
|
5234
5243
|
// Rename on the control-plane.
|
|
5235
5244
|
const url = getConfiguredRemote(opts.remote);
|
|
@@ -5251,7 +5260,7 @@ export async function main(argv) {
|
|
|
5251
5260
|
}
|
|
5252
5261
|
}
|
|
5253
5262
|
const result = await renameRemoteSession(url, sessionId, newName);
|
|
5254
|
-
process.stderr.write(`[
|
|
5263
|
+
process.stderr.write(`[h2a] session ${result.sessionId} renamed to "${result.displayName}" (accepted: ${String(result.accepted)})\n`);
|
|
5255
5264
|
});
|
|
5256
5265
|
// ---------------------------------------------------------------------------
|
|
5257
5266
|
// account — WP16 Layer-C: local LLM account pool (enroll / ls / rm / select)
|
|
@@ -5275,7 +5284,7 @@ export async function main(argv) {
|
|
|
5275
5284
|
.action((opts) => {
|
|
5276
5285
|
const provider = opts.provider;
|
|
5277
5286
|
if (provider !== "claude-code" && provider !== "codex") {
|
|
5278
|
-
process.stderr.write(`[
|
|
5287
|
+
process.stderr.write(`[h2a] account enroll: unknown provider "${opts.provider}" (known: claude-code, codex)\n`);
|
|
5279
5288
|
process.exitCode = 1;
|
|
5280
5289
|
return;
|
|
5281
5290
|
}
|
|
@@ -5285,18 +5294,18 @@ export async function main(argv) {
|
|
|
5285
5294
|
? readClaudeCredential(opts.configDir)
|
|
5286
5295
|
: readCodexCredential(opts.configDir);
|
|
5287
5296
|
if (!result.ok) {
|
|
5288
|
-
process.stderr.write(`[
|
|
5297
|
+
process.stderr.write(`[h2a] account enroll: ${result.error}\n`);
|
|
5289
5298
|
process.exitCode = 1;
|
|
5290
5299
|
return;
|
|
5291
5300
|
}
|
|
5292
5301
|
accessToken = result.accessToken;
|
|
5293
|
-
process.stderr.write(`[
|
|
5302
|
+
process.stderr.write(`[h2a] account enroll: read token from ${provider === "claude-code" ? "~/.claude/.credentials.json" : "~/.codex/auth.json"}\n`);
|
|
5294
5303
|
}
|
|
5295
5304
|
else {
|
|
5296
5305
|
accessToken = process.env.REMOTE_ACCOUNT_TOKEN ?? "";
|
|
5297
5306
|
if (!accessToken.trim()) {
|
|
5298
|
-
process.stderr.write("[
|
|
5299
|
-
"[
|
|
5307
|
+
process.stderr.write("[h2a] account enroll: REMOTE_ACCOUNT_TOKEN env var is not set or empty\n" +
|
|
5308
|
+
"[h2a] (tip: use --from-credentials to read from the CLI's local config)\n");
|
|
5300
5309
|
process.exitCode = 1;
|
|
5301
5310
|
return;
|
|
5302
5311
|
}
|
|
@@ -5312,11 +5321,11 @@ export async function main(argv) {
|
|
|
5312
5321
|
: {}),
|
|
5313
5322
|
});
|
|
5314
5323
|
if (!result.ok) {
|
|
5315
|
-
process.stderr.write(`[
|
|
5324
|
+
process.stderr.write(`[h2a] account enroll: ${result.error}\n`);
|
|
5316
5325
|
process.exitCode = 1;
|
|
5317
5326
|
return;
|
|
5318
5327
|
}
|
|
5319
|
-
process.stderr.write(`[
|
|
5328
|
+
process.stderr.write(`[h2a] account enrolled: ${result.descriptor.id} (${result.descriptor.provider}) "${result.descriptor.label}"\n`);
|
|
5320
5329
|
});
|
|
5321
5330
|
accountCommand
|
|
5322
5331
|
.command("ls")
|
|
@@ -5329,7 +5338,7 @@ export async function main(argv) {
|
|
|
5329
5338
|
return;
|
|
5330
5339
|
}
|
|
5331
5340
|
if (accounts.length === 0) {
|
|
5332
|
-
process.stderr.write("[
|
|
5341
|
+
process.stderr.write("[h2a] no accounts enrolled\n");
|
|
5333
5342
|
return;
|
|
5334
5343
|
}
|
|
5335
5344
|
const hasConfigDir = accounts.some((a) => a.configDir !== undefined);
|
|
@@ -5359,11 +5368,11 @@ export async function main(argv) {
|
|
|
5359
5368
|
.action((id) => {
|
|
5360
5369
|
const result = removeAccount(id);
|
|
5361
5370
|
if (!result.ok) {
|
|
5362
|
-
process.stderr.write(`[
|
|
5371
|
+
process.stderr.write(`[h2a] account rm: ${result.error}\n`);
|
|
5363
5372
|
process.exitCode = 1;
|
|
5364
5373
|
return;
|
|
5365
5374
|
}
|
|
5366
|
-
process.stderr.write(`[
|
|
5375
|
+
process.stderr.write(`[h2a] account removed: ${result.id}\n`);
|
|
5367
5376
|
});
|
|
5368
5377
|
accountCommand
|
|
5369
5378
|
.command("exhausted <id>")
|
|
@@ -5385,7 +5394,7 @@ export async function main(argv) {
|
|
|
5385
5394
|
})();
|
|
5386
5395
|
const rec = markExhausted(id, windowMs, opts.reason);
|
|
5387
5396
|
const resetsAt = new Date(new Date(rec.exhaustedAt).getTime() + windowMs).toISOString();
|
|
5388
|
-
process.stderr.write(`[
|
|
5397
|
+
process.stderr.write(`[h2a] account ${id} marked exhausted — resets at ${resetsAt}` +
|
|
5389
5398
|
(opts.reason ? ` (reason: ${opts.reason})` : "") +
|
|
5390
5399
|
"\n");
|
|
5391
5400
|
});
|
|
@@ -5395,7 +5404,7 @@ export async function main(argv) {
|
|
|
5395
5404
|
"becomes immediately available for selection again).")
|
|
5396
5405
|
.action((id) => {
|
|
5397
5406
|
clearExhaustion(id);
|
|
5398
|
-
process.stderr.write(`[
|
|
5407
|
+
process.stderr.write(`[h2a] account ${id} quota cleared — available for selection\n`);
|
|
5399
5408
|
});
|
|
5400
5409
|
accountCommand
|
|
5401
5410
|
.command("select")
|
|
@@ -5409,14 +5418,14 @@ export async function main(argv) {
|
|
|
5409
5418
|
if (opts.fallback) {
|
|
5410
5419
|
if (!opts.provider ||
|
|
5411
5420
|
(opts.provider !== "claude-code" && opts.provider !== "codex")) {
|
|
5412
|
-
process.stderr.write("[
|
|
5421
|
+
process.stderr.write("[h2a] account select --fallback requires --provider claude-code|codex\n");
|
|
5413
5422
|
process.exitCode = 1;
|
|
5414
5423
|
return;
|
|
5415
5424
|
}
|
|
5416
5425
|
const provider = opts.provider;
|
|
5417
5426
|
const sel = selectAccountWithFallback(provider, opts.affinityKey);
|
|
5418
5427
|
if (!sel.candidate) {
|
|
5419
|
-
process.stderr.write("[
|
|
5428
|
+
process.stderr.write("[h2a] account select: all accounts exhausted — no usable account available\n");
|
|
5420
5429
|
process.exitCode = 1;
|
|
5421
5430
|
return;
|
|
5422
5431
|
}
|
|
@@ -5434,7 +5443,7 @@ export async function main(argv) {
|
|
|
5434
5443
|
const candidates = loadCandidates(provider).map(({ accessToken: _t, ...d }) => d);
|
|
5435
5444
|
const pick = selectAccount(candidates, opts.lastUsed);
|
|
5436
5445
|
if (!pick) {
|
|
5437
|
-
process.stderr.write("[
|
|
5446
|
+
process.stderr.write("[h2a] account select: no candidates available\n");
|
|
5438
5447
|
return;
|
|
5439
5448
|
}
|
|
5440
5449
|
process.stdout.write(JSON.stringify({ selected: pick, totalCandidates: candidates.length }, null, 2) + "\n");
|
|
@@ -5454,15 +5463,15 @@ export async function main(argv) {
|
|
|
5454
5463
|
? opts.exportS3
|
|
5455
5464
|
: process.env.REMOTE_ACCOUNT_LOG_S3;
|
|
5456
5465
|
if (!uri) {
|
|
5457
|
-
process.stderr.write("[
|
|
5466
|
+
process.stderr.write("[h2a] account log --export-s3: no S3 URI provided and REMOTE_ACCOUNT_LOG_S3 is not set\n");
|
|
5458
5467
|
process.exit(1);
|
|
5459
5468
|
}
|
|
5460
5469
|
try {
|
|
5461
5470
|
await exportSessionLogToS3(uri);
|
|
5462
|
-
process.stdout.write(`[
|
|
5471
|
+
process.stdout.write(`[h2a] session log exported to ${uri}\n`);
|
|
5463
5472
|
}
|
|
5464
5473
|
catch (err) {
|
|
5465
|
-
process.stderr.write(`[
|
|
5474
|
+
process.stderr.write(`[h2a] account log --export-s3: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
5466
5475
|
process.exit(1);
|
|
5467
5476
|
}
|
|
5468
5477
|
return;
|
|
@@ -5473,7 +5482,7 @@ export async function main(argv) {
|
|
|
5473
5482
|
raw = readFileSync(logFile, "utf8");
|
|
5474
5483
|
}
|
|
5475
5484
|
catch {
|
|
5476
|
-
process.stderr.write("[
|
|
5485
|
+
process.stderr.write("[h2a] account log: no session log yet (no jobs launched with accounts enrolled)\n");
|
|
5477
5486
|
return;
|
|
5478
5487
|
}
|
|
5479
5488
|
const lines = raw.split("\n").filter(Boolean);
|
|
@@ -5484,7 +5493,7 @@ export async function main(argv) {
|
|
|
5484
5493
|
return;
|
|
5485
5494
|
}
|
|
5486
5495
|
if (tail.length === 0) {
|
|
5487
|
-
process.stderr.write("[
|
|
5496
|
+
process.stderr.write("[h2a] account log: empty\n");
|
|
5488
5497
|
return;
|
|
5489
5498
|
}
|
|
5490
5499
|
process.stdout.write([
|
|
@@ -5522,7 +5531,7 @@ export async function main(argv) {
|
|
|
5522
5531
|
.action((affinityKey) => {
|
|
5523
5532
|
const existing = lookupBinding(affinityKey);
|
|
5524
5533
|
if (!existing) {
|
|
5525
|
-
process.stderr.write(`[
|
|
5534
|
+
process.stderr.write(`[h2a] no binding for "${affinityKey}"\n`);
|
|
5526
5535
|
process.exitCode = 1;
|
|
5527
5536
|
return;
|
|
5528
5537
|
}
|
|
@@ -5530,7 +5539,7 @@ export async function main(argv) {
|
|
|
5530
5539
|
const accounts = listAccounts();
|
|
5531
5540
|
const label = accounts.find((a) => a.id === existing.accountId)?.label ??
|
|
5532
5541
|
existing.accountId.slice(0, 8);
|
|
5533
|
-
process.stderr.write(`[
|
|
5542
|
+
process.stderr.write(`[h2a] binding removed: ${affinityKey} → ${label} (${existing.provider})\n`);
|
|
5534
5543
|
});
|
|
5535
5544
|
accountCommand
|
|
5536
5545
|
.command("bindings")
|
|
@@ -5544,7 +5553,7 @@ export async function main(argv) {
|
|
|
5544
5553
|
return;
|
|
5545
5554
|
}
|
|
5546
5555
|
if (bindings.length === 0) {
|
|
5547
|
-
process.stderr.write("[
|
|
5556
|
+
process.stderr.write("[h2a] no active bindings (no jobs launched with accounts enrolled)\n");
|
|
5548
5557
|
return;
|
|
5549
5558
|
}
|
|
5550
5559
|
const accounts = listAccounts();
|
|
@@ -5581,7 +5590,7 @@ export async function main(argv) {
|
|
|
5581
5590
|
.action(async (opts) => {
|
|
5582
5591
|
const accounts = listAccountsWithTokens();
|
|
5583
5592
|
if (accounts.length === 0) {
|
|
5584
|
-
process.stderr.write("[
|
|
5593
|
+
process.stderr.write("[h2a] no enrolled accounts with tokens found — run `h2a account enroll` first\n");
|
|
5585
5594
|
process.exit(1);
|
|
5586
5595
|
}
|
|
5587
5596
|
// Build the gateway-format JSON (token field, not accessToken)
|
|
@@ -5616,10 +5625,10 @@ export async function main(argv) {
|
|
|
5616
5625
|
encoding: "utf-8",
|
|
5617
5626
|
});
|
|
5618
5627
|
if (result.status !== 0) {
|
|
5619
|
-
process.stderr.write(`[
|
|
5628
|
+
process.stderr.write(`[h2a] kubectl apply failed (exit ${result.status ?? "?"}) — try --dry-run to inspect the YAML\n`);
|
|
5620
5629
|
process.exit(result.status ?? 1);
|
|
5621
5630
|
}
|
|
5622
|
-
process.stdout.write(`[
|
|
5631
|
+
process.stdout.write(`[h2a] pushed ${accounts.length} account(s) to ${opts.namespace}/${opts.secretName}\n`);
|
|
5623
5632
|
});
|
|
5624
5633
|
void accountPushClusterCommand; // suppress unused-var lint
|
|
5625
5634
|
// ---------------------------------------------------------------------------
|
|
@@ -5642,10 +5651,10 @@ export async function main(argv) {
|
|
|
5642
5651
|
.action(async (provider, opts) => {
|
|
5643
5652
|
let account;
|
|
5644
5653
|
if (provider === "claude") {
|
|
5645
|
-
process.stderr.write(`[
|
|
5654
|
+
process.stderr.write(`[h2a] llm-mesh: Claude Code OAuth is not a supported upstream transport yet.\n` +
|
|
5646
5655
|
` Do not import ~/.claude/.credentials.json as an Anthropic API credential.\n` +
|
|
5647
5656
|
` Use the existing gateway token with codex/openai, or enroll a real Anthropic API key via:\n` +
|
|
5648
|
-
`
|
|
5657
|
+
` h2a llm-mesh enroll anthropic --token sk-ant-api...\n`);
|
|
5649
5658
|
process.exitCode = 1;
|
|
5650
5659
|
return;
|
|
5651
5660
|
}
|
|
@@ -5658,8 +5667,8 @@ export async function main(argv) {
|
|
|
5658
5667
|
}
|
|
5659
5668
|
else if (provider === "openai") {
|
|
5660
5669
|
if (!opts.token) {
|
|
5661
|
-
process.stderr.write(`[
|
|
5662
|
-
` Example:
|
|
5670
|
+
process.stderr.write(`[h2a] llm-mesh: --token <sk-...> is required for provider "openai"\n` +
|
|
5671
|
+
` Example: h2a llm-mesh enroll openai --token sk-proj-...\n`);
|
|
5663
5672
|
process.exitCode = 1;
|
|
5664
5673
|
return;
|
|
5665
5674
|
}
|
|
@@ -5672,12 +5681,12 @@ export async function main(argv) {
|
|
|
5672
5681
|
}
|
|
5673
5682
|
else if (provider === "anthropic") {
|
|
5674
5683
|
if (!opts.token) {
|
|
5675
|
-
process.stderr.write(`[
|
|
5684
|
+
process.stderr.write(`[h2a] llm-mesh: --token <sk-ant-api...> is required for provider "anthropic"\n`);
|
|
5676
5685
|
process.exitCode = 1;
|
|
5677
5686
|
return;
|
|
5678
5687
|
}
|
|
5679
5688
|
if (opts.token.startsWith("sk-ant-oat")) {
|
|
5680
|
-
process.stderr.write(`[
|
|
5689
|
+
process.stderr.write(`[h2a] llm-mesh: refused Claude Code OAuth token for provider "anthropic".\n` +
|
|
5681
5690
|
` Claude Code OAuth uses a separate Claude Code transport, not Anthropic /v1/messages.\n`);
|
|
5682
5691
|
process.exitCode = 1;
|
|
5683
5692
|
return;
|
|
@@ -5690,7 +5699,7 @@ export async function main(argv) {
|
|
|
5690
5699
|
};
|
|
5691
5700
|
}
|
|
5692
5701
|
else {
|
|
5693
|
-
process.stderr.write(`[
|
|
5702
|
+
process.stderr.write(`[h2a] llm-mesh: unsupported provider "${provider}".\n` +
|
|
5694
5703
|
` Supported: codex (OAuth), openai (API key), anthropic (API key)\n`);
|
|
5695
5704
|
process.exitCode = 1;
|
|
5696
5705
|
return;
|
|
@@ -5699,18 +5708,18 @@ export async function main(argv) {
|
|
|
5699
5708
|
const accounts = existing.accounts.filter((a) => a.id !== account.id);
|
|
5700
5709
|
accounts.push(account);
|
|
5701
5710
|
writeLlmMeshConfig({ ...existing, accounts });
|
|
5702
|
-
process.stdout.write(`[
|
|
5711
|
+
process.stdout.write(`[h2a] llm-mesh: enrolled ${account.label} (id: ${account.id}, provider: ${account.provider})\n`);
|
|
5703
5712
|
if ("expiresAt" in account && account.expiresAt) {
|
|
5704
5713
|
const exp = new Date(account.expiresAt);
|
|
5705
5714
|
const minsLeft = Math.round((exp.getTime() - Date.now()) / 60_000);
|
|
5706
|
-
process.stdout.write(`[
|
|
5715
|
+
process.stdout.write(`[h2a] llm-mesh: token expires ${account.expiresAt} (${minsLeft > 0 ? `in ${minsLeft}min` : "EXPIRED"})\n`);
|
|
5707
5716
|
}
|
|
5708
5717
|
if (provider === "codex") {
|
|
5709
|
-
process.stdout.write(`[
|
|
5718
|
+
process.stdout.write(`[h2a] NOTE: Codex OAuth token works locally via codex app-server,\n` +
|
|
5710
5719
|
` but NOT with api.openai.com (missing model.request scope).\n` +
|
|
5711
|
-
` For a fully working gateway, use:
|
|
5720
|
+
` For a fully working gateway, use: h2a llm-mesh enroll openai --token sk-...\n`);
|
|
5712
5721
|
}
|
|
5713
|
-
process.stdout.write(`[
|
|
5722
|
+
process.stdout.write(`[h2a] Run \`h2a llm-mesh start\` to launch the gateway.\n`);
|
|
5714
5723
|
});
|
|
5715
5724
|
llmMeshCommand
|
|
5716
5725
|
.command("start")
|
|
@@ -5719,23 +5728,23 @@ export async function main(argv) {
|
|
|
5719
5728
|
.action(async (opts) => {
|
|
5720
5729
|
const config = readLlmMeshConfig();
|
|
5721
5730
|
if (!config || config.accounts.length === 0) {
|
|
5722
|
-
process.stderr.write(`[
|
|
5731
|
+
process.stderr.write(`[h2a] llm-mesh: no accounts configured. Run \`h2a llm-mesh enroll codex\` first.\n`);
|
|
5723
5732
|
process.exitCode = 1;
|
|
5724
5733
|
return;
|
|
5725
5734
|
}
|
|
5726
5735
|
const existing = readGatewayPid();
|
|
5727
5736
|
if (existing) {
|
|
5728
5737
|
setLlmMeshRuntimeConfig({ enabled: true });
|
|
5729
|
-
process.stdout.write(`[
|
|
5730
|
-
process.stdout.write(`[
|
|
5738
|
+
process.stdout.write(`[h2a] llm-mesh: already running (pid ${existing})\n`);
|
|
5739
|
+
process.stdout.write(`[h2a] llm-mesh: restore config enabled\n`);
|
|
5731
5740
|
return;
|
|
5732
5741
|
}
|
|
5733
|
-
process.stdout.write(`[
|
|
5742
|
+
process.stdout.write(`[h2a] llm-mesh: starting gateway…\n`);
|
|
5734
5743
|
const result = await startGateway(config, { verbose: opts.verbose });
|
|
5735
5744
|
setLlmMeshRuntimeConfig({ enabled: true });
|
|
5736
5745
|
const port = result.port;
|
|
5737
|
-
process.stdout.write(`[
|
|
5738
|
-
process.stdout.write(`[
|
|
5746
|
+
process.stdout.write(`[h2a] llm-mesh: gateway started (pid ${result.pid}, port ${port})\n`);
|
|
5747
|
+
process.stdout.write(`[h2a] llm-mesh: restore config enabled\n`);
|
|
5739
5748
|
process.stdout.write(`\nTo use with Claude Code:\n`);
|
|
5740
5749
|
process.stdout.write(` export ANTHROPIC_BASE_URL=http://localhost:${port}\n`);
|
|
5741
5750
|
process.stdout.write(` export ANTHROPIC_AUTH_TOKEN=${result.gatewayToken}\n`);
|
|
@@ -5745,14 +5754,14 @@ export async function main(argv) {
|
|
|
5745
5754
|
.description("Enable llm-mesh auto-reactivation for local sessions and remote restore")
|
|
5746
5755
|
.action(() => {
|
|
5747
5756
|
setLlmMeshRuntimeConfig({ enabled: true });
|
|
5748
|
-
process.stdout.write(`[
|
|
5757
|
+
process.stdout.write(`[h2a] llm-mesh: restore/local auto-reactivation enabled\n`);
|
|
5749
5758
|
});
|
|
5750
5759
|
llmMeshCommand
|
|
5751
5760
|
.command("disable")
|
|
5752
5761
|
.description("Disable llm-mesh auto-reactivation for local sessions and remote restore")
|
|
5753
5762
|
.action(() => {
|
|
5754
5763
|
setLlmMeshRuntimeConfig({ enabled: false });
|
|
5755
|
-
process.stdout.write(`[
|
|
5764
|
+
process.stdout.write(`[h2a] llm-mesh: restore/local auto-reactivation disabled\n`);
|
|
5756
5765
|
});
|
|
5757
5766
|
llmMeshCommand
|
|
5758
5767
|
.command("stop")
|
|
@@ -5760,10 +5769,10 @@ export async function main(argv) {
|
|
|
5760
5769
|
.action(() => {
|
|
5761
5770
|
const res = stopGateway();
|
|
5762
5771
|
if (res.stopped) {
|
|
5763
|
-
process.stdout.write(`[
|
|
5772
|
+
process.stdout.write(`[h2a] llm-mesh: stopped (pid ${res.pid})\n`);
|
|
5764
5773
|
}
|
|
5765
5774
|
else {
|
|
5766
|
-
process.stdout.write(`[
|
|
5775
|
+
process.stdout.write(`[h2a] llm-mesh: not running\n`);
|
|
5767
5776
|
}
|
|
5768
5777
|
});
|
|
5769
5778
|
llmMeshCommand
|
|
@@ -5773,23 +5782,23 @@ export async function main(argv) {
|
|
|
5773
5782
|
.action(async (opts) => {
|
|
5774
5783
|
const config = readLlmMeshConfig();
|
|
5775
5784
|
if (!config || config.accounts.length === 0) {
|
|
5776
|
-
process.stderr.write(`[
|
|
5785
|
+
process.stderr.write(`[h2a] llm-mesh: no accounts configured. Run \`h2a llm-mesh enroll codex\` first.\n`);
|
|
5777
5786
|
process.exitCode = 1;
|
|
5778
5787
|
return;
|
|
5779
5788
|
}
|
|
5780
5789
|
const stopped = stopGateway();
|
|
5781
5790
|
if (stopped.stopped) {
|
|
5782
|
-
process.stdout.write(`[
|
|
5791
|
+
process.stdout.write(`[h2a] llm-mesh: stopped gateway (pid ${stopped.pid})\n`);
|
|
5783
5792
|
}
|
|
5784
5793
|
else {
|
|
5785
|
-
process.stdout.write(`[
|
|
5794
|
+
process.stdout.write(`[h2a] llm-mesh: gateway was not running\n`);
|
|
5786
5795
|
}
|
|
5787
|
-
process.stdout.write(`[
|
|
5796
|
+
process.stdout.write(`[h2a] llm-mesh: starting gateway…\n`);
|
|
5788
5797
|
const result = await startGateway(config, { verbose: opts.verbose });
|
|
5789
5798
|
setLlmMeshRuntimeConfig({ enabled: true });
|
|
5790
|
-
process.stdout.write(`[
|
|
5791
|
-
process.stdout.write(`[
|
|
5792
|
-
process.stdout.write(`[
|
|
5799
|
+
process.stdout.write(`[h2a] llm-mesh: gateway restarted (pid ${result.pid}, port ${result.port})\n`);
|
|
5800
|
+
process.stdout.write(`[h2a] llm-mesh: restore config enabled\n`);
|
|
5801
|
+
process.stdout.write(`[h2a] llm-mesh: no tmux/Claude session was restarted; active sessions may keep their old gateway token until explicitly relaunched.\n`);
|
|
5793
5802
|
process.stdout.write(`\nTo use with new Claude Code processes:\n`);
|
|
5794
5803
|
process.stdout.write(` export ANTHROPIC_BASE_URL=http://localhost:${result.port}\n`);
|
|
5795
5804
|
process.stdout.write(` export ANTHROPIC_AUTH_TOKEN=${result.gatewayToken}\n`);
|
|
@@ -5804,10 +5813,10 @@ export async function main(argv) {
|
|
|
5804
5813
|
const pid = readGatewayPid();
|
|
5805
5814
|
const port = config?.port ?? 3002;
|
|
5806
5815
|
if (pid) {
|
|
5807
|
-
process.stdout.write(`[
|
|
5816
|
+
process.stdout.write(`[h2a] llm-mesh: running (pid ${pid}, port ${port})\n`);
|
|
5808
5817
|
}
|
|
5809
5818
|
else {
|
|
5810
|
-
process.stdout.write(`[
|
|
5819
|
+
process.stdout.write(`[h2a] llm-mesh: stopped\n`);
|
|
5811
5820
|
}
|
|
5812
5821
|
process.stdout.write(` restore/local auto-reactivation: ${runtime.enabled ? "enabled" : "disabled"}\n`);
|
|
5813
5822
|
if (config) {
|
|
@@ -5825,7 +5834,7 @@ export async function main(argv) {
|
|
|
5825
5834
|
}
|
|
5826
5835
|
}
|
|
5827
5836
|
else {
|
|
5828
|
-
process.stdout.write(` no config. Run \`
|
|
5837
|
+
process.stdout.write(` no config. Run \`h2a llm-mesh enroll codex\` first.\n`);
|
|
5829
5838
|
}
|
|
5830
5839
|
});
|
|
5831
5840
|
llmMeshCommand
|
|
@@ -5839,7 +5848,7 @@ export async function main(argv) {
|
|
|
5839
5848
|
stdio: "inherit",
|
|
5840
5849
|
});
|
|
5841
5850
|
if (tail.error) {
|
|
5842
|
-
process.stderr.write(`[
|
|
5851
|
+
process.stderr.write(`[h2a] llm-mesh: log tail failed: ${String(tail.error)}\n`);
|
|
5843
5852
|
process.exitCode = 1;
|
|
5844
5853
|
}
|
|
5845
5854
|
});
|
|
@@ -5855,7 +5864,7 @@ export async function main(argv) {
|
|
|
5855
5864
|
.description("Suspend the local incarnation for a lineage: writes a sentinel file that prevents the agent from starting a new turn. Does NOT kill the process.")
|
|
5856
5865
|
.action((id) => {
|
|
5857
5866
|
suspendLocalIncarnation(id);
|
|
5858
|
-
process.stderr.write(`[
|
|
5867
|
+
process.stderr.write(`[h2a] lineage ${id} suspended\n`);
|
|
5859
5868
|
});
|
|
5860
5869
|
lineageCommand
|
|
5861
5870
|
.command("resume <id>")
|
|
@@ -5864,8 +5873,8 @@ export async function main(argv) {
|
|
|
5864
5873
|
const wasSuspended = isIncarnationSuspended(id);
|
|
5865
5874
|
resumeLocalIncarnation(id);
|
|
5866
5875
|
process.stderr.write(wasSuspended
|
|
5867
|
-
? `[
|
|
5868
|
-
: `[
|
|
5876
|
+
? `[h2a] lineage ${id} resumed\n`
|
|
5877
|
+
: `[h2a] lineage ${id} was not suspended (no-op)\n`);
|
|
5869
5878
|
});
|
|
5870
5879
|
await program.parseAsync([...argv]);
|
|
5871
5880
|
const code = process.exitCode;
|
|
@@ -5889,7 +5898,7 @@ if (isEntryPoint()) {
|
|
|
5889
5898
|
console.error(error.message);
|
|
5890
5899
|
}
|
|
5891
5900
|
else {
|
|
5892
|
-
console.error("[
|
|
5901
|
+
console.error("[h2a] fatal:", error);
|
|
5893
5902
|
}
|
|
5894
5903
|
process.exitCode = 1;
|
|
5895
5904
|
});
|