@sentropic/h2a-runtime 0.90.1 → 0.90.3
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/index.d.ts +16 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +199 -39
- package/dist/index.js.map +1 -1
- package/dist/proc-cpu.d.ts +44 -0
- package/dist/proc-cpu.d.ts.map +1 -1
- package/dist/proc-cpu.js +84 -2
- package/dist/proc-cpu.js.map +1 -1
- package/dist/prompt-delivery.d.ts +9 -0
- package/dist/prompt-delivery.d.ts.map +1 -1
- package/dist/prompt-delivery.js +30 -3
- package/dist/prompt-delivery.js.map +1 -1
- package/dist/relaunch.d.ts +101 -6
- package/dist/relaunch.d.ts.map +1 -1
- package/dist/relaunch.js +200 -5
- package/dist/relaunch.js.map +1 -1
- package/dist/restore.d.ts +17 -22
- package/dist/restore.d.ts.map +1 -1
- package/dist/restore.js +62 -33
- package/dist/restore.js.map +1 -1
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +0 -3
- package/dist/run.js.map +1 -1
- package/dist/session-class.d.ts +12 -0
- package/dist/session-class.d.ts.map +1 -1
- package/dist/session-class.js +15 -0
- package/dist/session-class.js.map +1 -1
- package/dist/session-lease.d.ts +83 -4
- package/dist/session-lease.d.ts.map +1 -1
- package/dist/session-lease.js +197 -6
- package/dist/session-lease.js.map +1 -1
- package/dist/tmux.d.ts +51 -2
- package/dist/tmux.d.ts.map +1 -1
- package/dist/tmux.js +130 -3
- package/dist/tmux.js.map +1 -1
- package/package.json +2 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { projectStatusForH2a } from "./status-projection.js";
|
|
3
|
+
import type { ProcView } from "./proc-cpu.js";
|
|
3
4
|
import { type RegistryEntry } from "./registry.js";
|
|
4
|
-
import { reclaimProposals, type SessionLease } from "./session-lease.js";
|
|
5
|
+
import { reclaimProposals, type SessionLease, type SessionLeaseFleetAudit, type SessionLeaseWorker } from "./session-lease.js";
|
|
5
6
|
import { projectRemoteAgents } from "./agents-projection.js";
|
|
6
7
|
export declare const packageName = "@sentropic/h2a-runtime";
|
|
7
8
|
export declare const H2A_RUNTIME_CLI_API_VERSION = 1;
|
|
@@ -93,6 +94,7 @@ export declare function acquireSessionLease(sessionId: string, opts: {
|
|
|
93
94
|
holder: string;
|
|
94
95
|
workspace?: string;
|
|
95
96
|
agentPane?: string;
|
|
97
|
+
worker?: SessionLeaseWorker;
|
|
96
98
|
}): SessionLease | undefined;
|
|
97
99
|
/** What one supervising pass over the session leases did. */
|
|
98
100
|
export type SessionLeaseSupervision = {
|
|
@@ -113,6 +115,19 @@ export type SessionLeaseSupervision = {
|
|
|
113
115
|
* Its only output beyond the store is a list of PROPOSALS.
|
|
114
116
|
*/
|
|
115
117
|
export declare function superviseSessionLeases(sessions: ReadonlyArray<RegistryEntry>, now?: string): SessionLeaseSupervision;
|
|
118
|
+
/** Relaunch one managed session while its lease carries the bounded protection. */
|
|
119
|
+
export declare function relaunchSessionWithLease(input: {
|
|
120
|
+
sessionId: string;
|
|
121
|
+
tmuxSession: string;
|
|
122
|
+
command: string;
|
|
123
|
+
resolvingBoundMs?: number;
|
|
124
|
+
}): boolean;
|
|
125
|
+
/** One on-demand fleet-wide read-back audit; it never runs from rendering. */
|
|
126
|
+
export declare function auditLiveSessionLeases(sessions?: ReadonlyArray<RegistryEntry>, now?: string): SessionLeaseFleetAudit & {
|
|
127
|
+
reaped: string[];
|
|
128
|
+
liveLeaseCount: number;
|
|
129
|
+
procView: ProcView;
|
|
130
|
+
};
|
|
116
131
|
/**
|
|
117
132
|
* Reliability slice 1 — RESUME a throttled HEADLESS LOCAL job. Relaunch the SAME
|
|
118
133
|
* job in the SAME `runCwd` it already ran in (its recorded `cwd` — NOT a fresh
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -15,17 +15,18 @@ import { transmittedSecrets, secretsSummary } from "./secrets.js";
|
|
|
15
15
|
import { localConvStat, remoteConvStat, alignment } from "./convsync.js";
|
|
16
16
|
import { gitAlignment, localAncestry, localGitStat, remoteGitStat, } from "./gitdiff.js";
|
|
17
17
|
import { syncConversation } from "./sync.js";
|
|
18
|
-
import { attachLocalSession, attachPodTmux, capturePane, capturePaneVisible, clearPaneComposer, conductorRunning, currentTmuxSessionName, currentTmuxSessionIs, ensureManagedTmuxProfile, ensureHeadlessTerminal, existingLocalSessionSlugs, fanoutLabels, findLocalSession, killLocalSession, localSessionGatewayEnvStatus, listLocalSessions, listLocalSessionsWithDiagnostics, localSessionIdle, localSessionPanePid, localSessionName, paneTreeCpuMs, paneWorkerPid, pasteLiteralBlock, submitPane, installH2aStatusSurface, managedSessionCandidates, parseManagedSessionName, openH2aStatusWindow, persistLaunchContext, readLaunchContext, relaunchInSession, resolveAgentPane, resolveAgentPaneForInstance, resolveLocalSession, runLocalCliForeground, sendKeysLiteral, sessionAttached, sessionAttachedCount, setLocalSessionDisplayName, slugify, cleanupHeadlessPromptFile, startH2aWindow, startH2aWindowVerified, startHeadlessSession, startLocalSession, tmuxAvailable, uninstallH2aStatusSurface, } from "./tmux.js";
|
|
18
|
+
import { attachLocalSession, attachPodTmux, capturePane, capturePaneVisible, clearPaneComposer, conductorRunning, currentTmuxSessionName, currentTmuxSessionIs, ensureManagedTmuxProfile, ensureHeadlessTerminal, existingLocalSessionSlugs, fanoutLabels, findLocalSession, killLocalSession, localSessionGatewayEnvStatus, listLocalSessions, listH2aStatusSurfaces, listLocalSessionsWithDiagnostics, localSessionIdle, localSessionPanePid, localSessionName, paneProcessObservation, paneTreeCpuMs, paneWorkerAttribution, paneWorkerAttributionFromView, paneWorkerPid, pasteLiteralBlock, submitPane, installH2aStatusSurface, managedSessionCandidates, parseManagedSessionName, openH2aStatusWindow, persistLaunchContext, readLaunchContext, readFleetProcView, relaunchInSession, resolveAgentPane, resolveAgentPaneForInstance, resolveLocalSession, sessionRelaunchSafety, runLocalCliForeground, sendKeysLiteral, sessionAttached, sessionAttachedCount, setLocalSessionDisplayName, slugify, cleanupHeadlessPromptFile, startH2aWindow, startH2aWindowVerified, startHeadlessSession, startLocalSession, tmuxAvailable, uninstallH2aStatusSurface, } from "./tmux.js";
|
|
19
19
|
import { deliverInitialPrompt, sleepSync, } from "./prompt-delivery.js";
|
|
20
20
|
import { buildLaunchContext } from "./launch-context.js";
|
|
21
21
|
import { migrateTmuxNames } from "./tmux-name-migration.js";
|
|
22
22
|
import { projectStatusForH2a } from "./status-projection.js";
|
|
23
23
|
import { buildAgentLaunchArgs, isAgentLaunchEffort, isAgentLaunchProfile, } from "./agent-launch-args.js";
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
24
|
+
import { deriveSessionClass } from "./session-class.js";
|
|
25
|
+
import { isRelaunchKillable, planRelaunch, relaunchContinuationPrompt, wakeRelaunchedSession, } from "./relaunch.js";
|
|
26
|
+
import { isHumanFacingSession, legacySessionEvidence, readConversationCustomTitle, readLastLayout, reconcileRunConvIds, restore as restoreLayout, } from "./restore.js";
|
|
26
27
|
import { getLayoutConfig } from "./config.js";
|
|
27
28
|
import { advanceJob, coerceRegistryTool, enroll, enrollFromRun, isLive, listJobs, listLocalForLs, loadRegistry, loadRegistryWithDiagnostics, resolveLocalTmuxSessionForName, tryClaimSlot, withRegistryLock, resolveRegistryPath, } from "./registry.js";
|
|
28
|
-
import { SessionLeaseStore, decideSessionBeat, isSessionLeaseAbandoned, planLeaseSupervision, reclaimProposals, resolveSessionLeasePath, } from "./session-lease.js";
|
|
29
|
+
import { auditSessionLeaseFleet, countLiveSessionLeases, SessionLeaseStore, decideSessionBeat, isSessionLeaseAlive, isSessionLeaseAbandoned, isSessionLeaseDeadBoot, planLeaseSupervision, reclaimProposals, resolveSessionLeasePath, } from "./session-lease.js";
|
|
29
30
|
import { aimdEffectiveCap, assertDelegateEffort, 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";
|
|
30
31
|
import { detectThrottle, THROTTLE_TAIL_LINES } from "./throttle-signatures.js";
|
|
31
32
|
import { interactiveResumeNudge, planInteractiveResume, } from "./interactive-throttle.js";
|
|
@@ -1014,12 +1015,15 @@ function sessionLeaseStore() {
|
|
|
1014
1015
|
* to compute a rate against.
|
|
1015
1016
|
*/
|
|
1016
1017
|
export function acquireSessionLease(sessionId, opts) {
|
|
1017
|
-
|
|
1018
|
+
// CPU baseline and worker identity share one /proc walk at acquire time.
|
|
1019
|
+
const observation = opts.agentPane ? paneProcessObservation(opts.agentPane) : undefined;
|
|
1020
|
+
const worker = opts.worker ?? observation?.worker;
|
|
1018
1021
|
return sessionLeaseStore().acquire({
|
|
1019
1022
|
sessionId,
|
|
1020
1023
|
holder: opts.holder,
|
|
1021
1024
|
...(opts.workspace !== undefined ? { workspace: opts.workspace } : {}),
|
|
1022
|
-
...(cpuMs !== undefined ? { cpuMs } : {}),
|
|
1025
|
+
...(observation?.cpuMs !== undefined ? { cpuMs: observation.cpuMs } : {}),
|
|
1026
|
+
...(worker !== undefined ? { worker } : {}),
|
|
1023
1027
|
});
|
|
1024
1028
|
}
|
|
1025
1029
|
/**
|
|
@@ -1040,6 +1044,9 @@ export function superviseSessionLeases(sessions, now = new Date().toISOString())
|
|
|
1040
1044
|
unreadable: [],
|
|
1041
1045
|
proposals: [],
|
|
1042
1046
|
};
|
|
1047
|
+
const procView = readFleetProcView();
|
|
1048
|
+
// R3: prior-boot corpses are reaped silently before any proposal pass.
|
|
1049
|
+
store.reapDeadBootLeases(procView);
|
|
1043
1050
|
const leases = store.readAll();
|
|
1044
1051
|
const byId = new Map(leases.map((lease) => [lease.sessionId, lease]));
|
|
1045
1052
|
for (const step of planLeaseSupervision(leases, sessions)) {
|
|
@@ -1055,27 +1062,77 @@ export function superviseSessionLeases(sessions, now = new Date().toISOString())
|
|
|
1055
1062
|
if (step.action === "leave")
|
|
1056
1063
|
continue;
|
|
1057
1064
|
const pane = step.tmuxSession ? resolveAgentPane(step.tmuxSession) : undefined;
|
|
1065
|
+
let currentLease = lease;
|
|
1066
|
+
// R1 lazy migration: old leases remain alive until this pass can resolve
|
|
1067
|
+
// their worker. Resolve from the pass-wide snapshot, never by spawning a
|
|
1068
|
+
// /proc walk for every lease.
|
|
1069
|
+
if (currentLease.worker === undefined && pane !== undefined) {
|
|
1070
|
+
const worker = paneWorkerAttributionFromView(pane, procView);
|
|
1071
|
+
if (worker !== undefined) {
|
|
1072
|
+
currentLease = store.populateWorker({
|
|
1073
|
+
sessionId: currentLease.sessionId,
|
|
1074
|
+
token: currentLease.token,
|
|
1075
|
+
worker,
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
if (!isSessionLeaseAlive(currentLease, procView, now)) {
|
|
1080
|
+
result.unreadable.push(currentLease.sessionId);
|
|
1081
|
+
continue;
|
|
1082
|
+
}
|
|
1058
1083
|
const cpuMsNow = pane ? paneTreeCpuMs(pane) : undefined;
|
|
1059
|
-
const decision = decideSessionBeat({ lease, cpuMsNow, now });
|
|
1084
|
+
const decision = decideSessionBeat({ lease: currentLease, cpuMsNow, now });
|
|
1060
1085
|
if (decision.action === "beat" && cpuMsNow !== undefined) {
|
|
1061
|
-
store.heartbeat({ sessionId:
|
|
1062
|
-
result.beaten.push(
|
|
1086
|
+
store.heartbeat({ sessionId: currentLease.sessionId, token: currentLease.token, cpuMs: cpuMsNow });
|
|
1087
|
+
result.beaten.push(currentLease.sessionId);
|
|
1063
1088
|
}
|
|
1064
1089
|
else if (decision.action === "observe" && cpuMsNow !== undefined) {
|
|
1065
|
-
store.observe({ sessionId:
|
|
1066
|
-
result.observed.push(
|
|
1090
|
+
store.observe({ sessionId: currentLease.sessionId, token: currentLease.token, cpuMs: cpuMsNow });
|
|
1091
|
+
result.observed.push(currentLease.sessionId);
|
|
1067
1092
|
}
|
|
1068
1093
|
else {
|
|
1069
|
-
result.unreadable.push(
|
|
1094
|
+
result.unreadable.push(currentLease.sessionId);
|
|
1070
1095
|
}
|
|
1071
1096
|
}
|
|
1072
1097
|
catch {
|
|
1073
1098
|
// a rejected write (raced re-acquisition, torn store) must not stop the pass
|
|
1074
1099
|
}
|
|
1075
1100
|
}
|
|
1076
|
-
result.proposals = reclaimProposals(store.readAll(), now);
|
|
1101
|
+
result.proposals = reclaimProposals(store.readAll(), now, procView);
|
|
1077
1102
|
return result;
|
|
1078
1103
|
}
|
|
1104
|
+
/** Relaunch one managed session while its lease carries the bounded protection. */
|
|
1105
|
+
export function relaunchSessionWithLease(input) {
|
|
1106
|
+
const store = sessionLeaseStore();
|
|
1107
|
+
const lease = store.forSession(input.sessionId);
|
|
1108
|
+
if (lease === undefined)
|
|
1109
|
+
return relaunchInSession(input.tmuxSession, input.command);
|
|
1110
|
+
const pane = resolveAgentPane(input.tmuxSession);
|
|
1111
|
+
const outcome = store.relaunch({
|
|
1112
|
+
sessionId: lease.sessionId,
|
|
1113
|
+
token: lease.token,
|
|
1114
|
+
...(input.resolvingBoundMs !== undefined
|
|
1115
|
+
? { resolvingBoundMs: input.resolvingBoundMs }
|
|
1116
|
+
: {}),
|
|
1117
|
+
act: () => relaunchInSession(input.tmuxSession, input.command),
|
|
1118
|
+
resolveWorker: () => (pane ? paneWorkerAttribution(pane) : undefined),
|
|
1119
|
+
});
|
|
1120
|
+
return outcome.succeeded;
|
|
1121
|
+
}
|
|
1122
|
+
/** One on-demand fleet-wide read-back audit; it never runs from rendering. */
|
|
1123
|
+
export function auditLiveSessionLeases(sessions = listJobs(), now = new Date().toISOString()) {
|
|
1124
|
+
const store = sessionLeaseStore();
|
|
1125
|
+
const leases = store.readAll();
|
|
1126
|
+
const procView = readFleetProcView();
|
|
1127
|
+
const report = auditSessionLeaseFleet(sessions.map((session) => ({
|
|
1128
|
+
sessionId: session.id,
|
|
1129
|
+
...(session.tmuxSession !== undefined ? { tmuxSession: session.tmuxSession } : {}),
|
|
1130
|
+
...(session.jobState !== undefined ? { jobState: session.jobState } : {}),
|
|
1131
|
+
})), leases, listH2aStatusSurfaces(), procView, now);
|
|
1132
|
+
const liveLeaseCount = countLiveSessionLeases(leases, procView, now);
|
|
1133
|
+
const reaped = store.reapDeadBootLeases(procView);
|
|
1134
|
+
return { ...report, reaped, liveLeaseCount, procView };
|
|
1135
|
+
}
|
|
1079
1136
|
/** Human-readable duration for lease reporting ("41m", "2h13m"). */
|
|
1080
1137
|
function formatIdle(ms) {
|
|
1081
1138
|
const minutes = Math.floor(ms / 60_000);
|
|
@@ -1449,13 +1506,14 @@ function registryEntriesForLocalTmuxTarget(target, local, entries = loadRegistry
|
|
|
1449
1506
|
});
|
|
1450
1507
|
}
|
|
1451
1508
|
function registryEntryForResumeTarget(target, local) {
|
|
1509
|
+
const evidence = legacySessionEvidence(homedir(), local === undefined ? listLocalSessions() : [local]);
|
|
1452
1510
|
const matches = registryEntriesForLocalTmuxTarget(target, local).filter((e) => {
|
|
1453
1511
|
// Resume is a human-facing operation, not a promotion path. Share the
|
|
1454
1512
|
// durable class test with restore so legacy human rows remain resumable,
|
|
1455
1513
|
// while delegated jobs and explicit background launches stay excluded.
|
|
1456
1514
|
if (e.role !== undefined)
|
|
1457
1515
|
return false;
|
|
1458
|
-
return isHumanFacingSession(e);
|
|
1516
|
+
return isHumanFacingSession(e, evidence);
|
|
1459
1517
|
});
|
|
1460
1518
|
const ids = new Set(matches.map((e) => e.id));
|
|
1461
1519
|
if (ids.size !== 1)
|
|
@@ -3339,6 +3397,15 @@ export async function main(argv) {
|
|
|
3339
3397
|
process.exitCode = 1;
|
|
3340
3398
|
return;
|
|
3341
3399
|
}
|
|
3400
|
+
let gatewayMode;
|
|
3401
|
+
try {
|
|
3402
|
+
gatewayMode = gatewayModeFromOptions(opts);
|
|
3403
|
+
}
|
|
3404
|
+
catch (error) {
|
|
3405
|
+
process.stderr.write(`[h2a] ${error.message}.\n`);
|
|
3406
|
+
process.exitCode = 1;
|
|
3407
|
+
return;
|
|
3408
|
+
}
|
|
3342
3409
|
const target = slug ?? slugify(process.cwd());
|
|
3343
3410
|
const localResolution = resolveLocalSession(target);
|
|
3344
3411
|
if (localResolution.kind === "ambiguous") {
|
|
@@ -3404,15 +3471,6 @@ export async function main(argv) {
|
|
|
3404
3471
|
return;
|
|
3405
3472
|
}
|
|
3406
3473
|
const profile = entry.tool;
|
|
3407
|
-
let gatewayMode;
|
|
3408
|
-
try {
|
|
3409
|
-
gatewayMode = gatewayModeFromOptions(opts);
|
|
3410
|
-
}
|
|
3411
|
-
catch (error) {
|
|
3412
|
-
process.stderr.write(`[h2a] ${error.message}.\n`);
|
|
3413
|
-
process.exitCode = 1;
|
|
3414
|
-
return;
|
|
3415
|
-
}
|
|
3416
3474
|
if (gatewayMode === "gateway" && !profileUsesLlmMeshGateway(profile)) {
|
|
3417
3475
|
process.stderr.write(`[h2a] --llm-gateway/--gw is unsupported for ${profile}; this gateway is Anthropic-compatible and only Claude profiles consume it.\n`);
|
|
3418
3476
|
process.exitCode = 2;
|
|
@@ -3535,13 +3593,19 @@ export async function main(argv) {
|
|
|
3535
3593
|
}
|
|
3536
3594
|
}
|
|
3537
3595
|
gateway = await injectLlmMeshGatewayEnv(effectiveGatewayMode, true, localSessionName(resumeSlug));
|
|
3538
|
-
const
|
|
3596
|
+
const sessionClass = deriveSessionClass({
|
|
3597
|
+
background: opts.attach !== true,
|
|
3598
|
+
humanTerminal: opts.attach === true &&
|
|
3599
|
+
process.stdin.isTTY === true &&
|
|
3600
|
+
process.stdout.isTTY === true,
|
|
3601
|
+
});
|
|
3602
|
+
const { name } = startLocalSession(profile, command, entry.cwd, args, resumeSlug, undefined, { sessionClass, attachedTerminal: true });
|
|
3539
3603
|
enrollFromRun({
|
|
3540
3604
|
profile,
|
|
3541
3605
|
slug: resumeSlug,
|
|
3542
3606
|
tmuxSession: name,
|
|
3543
3607
|
cwd: entry.cwd,
|
|
3544
|
-
sessionClass
|
|
3608
|
+
sessionClass,
|
|
3545
3609
|
...(entry.convId ? { convId: entry.convId } : {}),
|
|
3546
3610
|
...(gatewayMode !== "auto" ? { gatewayMode } : {}),
|
|
3547
3611
|
});
|
|
@@ -3736,7 +3800,11 @@ export async function main(argv) {
|
|
|
3736
3800
|
// A detached/background or run-once launch is a worker, even when its
|
|
3737
3801
|
// agent later emits a Claude SessionStart hook. Stamp this through tmux
|
|
3738
3802
|
// so that hook cannot reclassify it as a human session.
|
|
3739
|
-
const sessionClass =
|
|
3803
|
+
const sessionClass = deriveSessionClass({
|
|
3804
|
+
background: opts.background === true,
|
|
3805
|
+
headless: opts.headless === true,
|
|
3806
|
+
humanTerminal: opts.attachedTerminal !== false,
|
|
3807
|
+
});
|
|
3740
3808
|
let activeGateway;
|
|
3741
3809
|
const h2a = getH2aConfig();
|
|
3742
3810
|
const h2aSidecar = opts.h2a ?? h2a.enabled;
|
|
@@ -4553,11 +4621,14 @@ export async function main(argv) {
|
|
|
4553
4621
|
.command("leases")
|
|
4554
4622
|
.description("Session slot leases (machine-scoped): who holds a concurrency slot, when it last proved it was working, and which slots are reclaimable. READ-ONLY — it kills nothing and frees nothing.")
|
|
4555
4623
|
.option("--probe", "before reporting, run one supervising pass: beat the leases of sessions whose pane is burning CPU, record the others")
|
|
4624
|
+
.option("--audit", "run one on-demand fleet-wide read-back audit (one tmux FORMAT call and one /proc scan)")
|
|
4556
4625
|
.action((opts) => {
|
|
4557
4626
|
const now = new Date().toISOString();
|
|
4558
4627
|
if (opts.probe === true)
|
|
4559
4628
|
superviseSessionLeases(listJobs(), now);
|
|
4560
|
-
const
|
|
4629
|
+
const audit = opts.audit === true ? auditLiveSessionLeases(listJobs(), now) : undefined;
|
|
4630
|
+
const store = sessionLeaseStore();
|
|
4631
|
+
const leases = store.readAll();
|
|
4561
4632
|
if (leases.length === 0) {
|
|
4562
4633
|
process.stdout.write("no session leases\n");
|
|
4563
4634
|
return;
|
|
@@ -4568,17 +4639,33 @@ export async function main(argv) {
|
|
|
4568
4639
|
.sort((a, b) => Date.parse(a.heartbeatAt) - Date.parse(b.heartbeatAt))
|
|
4569
4640
|
.map((lease) => {
|
|
4570
4641
|
const idle = nowMs - Date.parse(lease.heartbeatAt);
|
|
4571
|
-
const state =
|
|
4642
|
+
const state = audit
|
|
4643
|
+
? isSessionLeaseAlive(lease, audit.procView, now)
|
|
4644
|
+
? "held"
|
|
4645
|
+
: "RECLAIMABLE"
|
|
4646
|
+
: isSessionLeaseAbandoned(lease, now)
|
|
4647
|
+
? "RECLAIMABLE"
|
|
4648
|
+
: "held";
|
|
4572
4649
|
return (`${lease.sessionId.padEnd(24)} ${state.padEnd(12)} ` +
|
|
4573
4650
|
`idle ${formatIdle(idle).padEnd(7)} ttl ${formatIdle(lease.ttlMs).padEnd(6)} ` +
|
|
4574
4651
|
`holder ${lease.holder}`);
|
|
4575
4652
|
});
|
|
4576
4653
|
process.stdout.write(`${rows.join("\n")}\n`);
|
|
4577
|
-
const reclaimable =
|
|
4654
|
+
const reclaimable = audit
|
|
4655
|
+
? reclaimProposals(leases, now, audit.procView).length
|
|
4656
|
+
: reclaimProposals(leases, now).length;
|
|
4578
4657
|
if (reclaimable > 0) {
|
|
4579
4658
|
process.stderr.write(`[h2a] ${reclaimable} slot(s) reclaimable — free one with: h2a jobs reclaim <id> ` +
|
|
4580
4659
|
"(the session's tmux window is left running and can still be attached)\n");
|
|
4581
4660
|
}
|
|
4661
|
+
if (audit) {
|
|
4662
|
+
if (audit.reaped.length > 0) {
|
|
4663
|
+
process.stderr.write(`[h2a] audit reaped prior-boot lease(s): ${audit.reaped.join(", ")}\n`);
|
|
4664
|
+
}
|
|
4665
|
+
if (audit.missingStatusSurface.length > 0) {
|
|
4666
|
+
process.stderr.write(`[h2a] audit status-surface drift: ${audit.missingStatusSurface.join(", ")}\n`);
|
|
4667
|
+
}
|
|
4668
|
+
}
|
|
4582
4669
|
});
|
|
4583
4670
|
jobsCommand
|
|
4584
4671
|
.command("reclaim <id>")
|
|
@@ -4586,15 +4673,21 @@ export async function main(argv) {
|
|
|
4586
4673
|
.action((id) => {
|
|
4587
4674
|
const now = new Date().toISOString();
|
|
4588
4675
|
const store = sessionLeaseStore();
|
|
4589
|
-
|
|
4676
|
+
let lease = store.forSession(id);
|
|
4590
4677
|
if (!lease) {
|
|
4591
4678
|
process.stderr.write(`[h2a] no session lease for "${id}" — nothing to reclaim (see: h2a jobs leases)\n`);
|
|
4592
4679
|
process.exitCode = 1;
|
|
4593
4680
|
return;
|
|
4594
4681
|
}
|
|
4682
|
+
const procView = readFleetProcView();
|
|
4683
|
+
if (isSessionLeaseDeadBoot(lease, procView)) {
|
|
4684
|
+
store.reapDeadBootLeases(procView);
|
|
4685
|
+
process.stdout.write(`reaped ${id} — lease belonged to a prior boot\n`);
|
|
4686
|
+
return;
|
|
4687
|
+
}
|
|
4595
4688
|
// The one guard that matters: a session still proving it works keeps its
|
|
4596
4689
|
// slot, whatever the operator believes. Reclaiming is not arbitration.
|
|
4597
|
-
if (
|
|
4690
|
+
if (isSessionLeaseAlive(lease, procView, now)) {
|
|
4598
4691
|
const idle = Date.parse(now) - Date.parse(lease.heartbeatAt);
|
|
4599
4692
|
process.stderr.write(`[h2a] refusing to reclaim ${id}: its lease still beats (last proof of work ` +
|
|
4600
4693
|
`${formatIdle(idle)} ago, TTL ${formatIdle(lease.ttlMs)}). It is working.\n`);
|
|
@@ -5477,7 +5570,10 @@ export async function main(argv) {
|
|
|
5477
5570
|
})(),
|
|
5478
5571
|
slug: s.slug,
|
|
5479
5572
|
name: s.name,
|
|
5480
|
-
|
|
5573
|
+
...(() => {
|
|
5574
|
+
const safety = sessionRelaunchSafety(s.name);
|
|
5575
|
+
return { ...safety, livenessReason: safety.reason };
|
|
5576
|
+
})(),
|
|
5481
5577
|
})), {
|
|
5482
5578
|
...(forced ? { force: true } : {}),
|
|
5483
5579
|
...(opts.all && !opts.includeAgents
|
|
@@ -5510,7 +5606,15 @@ export async function main(argv) {
|
|
|
5510
5606
|
if (!forced) {
|
|
5511
5607
|
let ok = 0;
|
|
5512
5608
|
for (const action of plan.actions) {
|
|
5513
|
-
|
|
5609
|
+
const sessionId = entryByTmuxSession.get(action.name)?.id;
|
|
5610
|
+
const relaunched = sessionId !== undefined
|
|
5611
|
+
? relaunchSessionWithLease({
|
|
5612
|
+
sessionId,
|
|
5613
|
+
tmuxSession: action.name,
|
|
5614
|
+
command: action.cmd,
|
|
5615
|
+
})
|
|
5616
|
+
: relaunchInSession(action.name, action.cmd);
|
|
5617
|
+
if (relaunched) {
|
|
5514
5618
|
ok += 1;
|
|
5515
5619
|
process.stderr.write(`[h2a] relaunched ${action.slug}: ${action.cmd}\n`);
|
|
5516
5620
|
}
|
|
@@ -5576,16 +5680,38 @@ export async function main(argv) {
|
|
|
5576
5680
|
process.exitCode = 1;
|
|
5577
5681
|
return;
|
|
5578
5682
|
}
|
|
5683
|
+
const finalReady = [];
|
|
5684
|
+
for (const item of ready) {
|
|
5685
|
+
const safety = sessionRelaunchSafety(item.action.name);
|
|
5686
|
+
if (!isRelaunchKillable(safety)) {
|
|
5687
|
+
process.stderr.write(`[h2a] skipped ${item.action.slug}: ${safety.reason}\n`);
|
|
5688
|
+
continue;
|
|
5689
|
+
}
|
|
5690
|
+
finalReady.push(item);
|
|
5691
|
+
}
|
|
5579
5692
|
let ok = 0;
|
|
5580
|
-
for (const { action, entry, sessionClass } of
|
|
5581
|
-
|
|
5582
|
-
|
|
5693
|
+
for (const { action, entry, sessionClass } of finalReady) {
|
|
5694
|
+
const safety = sessionRelaunchSafety(action.name);
|
|
5695
|
+
if (!isRelaunchKillable(safety)) {
|
|
5696
|
+
process.stderr.write(`[h2a] skipped ${action.slug}: became live before kill: ${safety.reason}\n`);
|
|
5697
|
+
continue;
|
|
5698
|
+
}
|
|
5699
|
+
if (!safety.identity) {
|
|
5700
|
+
process.stderr.write(`[h2a] skipped ${action.slug}: liveness identity was unreadable immediately before kill.\n`);
|
|
5701
|
+
continue;
|
|
5702
|
+
}
|
|
5703
|
+
if (!killLocalSession(action.name, safety.identity)) {
|
|
5704
|
+
process.stderr.write(`[h2a] skipped ${action.slug}: tmux session ${action.name} changed or was no longer proven dead immediately before kill.\n`);
|
|
5583
5705
|
continue;
|
|
5584
5706
|
}
|
|
5585
5707
|
try {
|
|
5586
5708
|
const gatewayMode = gatewayModeForProfile(action.profile, entry.gatewayMode ?? "auto");
|
|
5587
5709
|
await injectLlmMeshGatewayEnv(gatewayMode, true, localSessionName(action.slug));
|
|
5588
|
-
const { name } = startLocalSession(action.profile, action.command, entry.cwd, action.args, action.slug, undefined, {
|
|
5710
|
+
const { name, agentPane } = startLocalSession(action.profile, action.command, entry.cwd, action.args, action.slug, undefined, {
|
|
5711
|
+
sessionClass,
|
|
5712
|
+
resumeId: action.convId,
|
|
5713
|
+
attachedTerminal: true,
|
|
5714
|
+
});
|
|
5589
5715
|
enrollFromRun({
|
|
5590
5716
|
profile: action.profile,
|
|
5591
5717
|
slug: action.slug,
|
|
@@ -5597,8 +5723,32 @@ export async function main(argv) {
|
|
|
5597
5723
|
? { gatewayMode: entry.gatewayMode }
|
|
5598
5724
|
: {}),
|
|
5599
5725
|
});
|
|
5726
|
+
const wake = wakeRelaunchedSession(relaunchContinuationPrompt(entry.task), {
|
|
5727
|
+
deliverContinuation: (prompt) => deliverInitialPrompt(agentPane, prompt, {
|
|
5728
|
+
capturePane: capturePaneVisible,
|
|
5729
|
+
clearComposer: clearPaneComposer,
|
|
5730
|
+
pasteBlock: pasteLiteralBlock,
|
|
5731
|
+
submit: submitPane,
|
|
5732
|
+
cpuMs: paneTreeCpuMs,
|
|
5733
|
+
sleep: sleepSync,
|
|
5734
|
+
now: () => Date.now(),
|
|
5735
|
+
}),
|
|
5736
|
+
submitConfirmation: () => submitPane(agentPane),
|
|
5737
|
+
capturePane: () => capturePaneVisible(agentPane),
|
|
5738
|
+
sleep: sleepSync,
|
|
5739
|
+
now: () => Date.now(),
|
|
5740
|
+
});
|
|
5741
|
+
if (wake.confirmation === "auto-passed") {
|
|
5742
|
+
process.stderr.write(`[h2a] ${action.slug}: Claude stale-session confirmation auto-passed once\n`);
|
|
5743
|
+
}
|
|
5744
|
+
if (wake.state !== "working") {
|
|
5745
|
+
process.stderr.write(`[h2a] FAILED to wake ${action.slug} after resume: ${wake.reason}\n` +
|
|
5746
|
+
(wake.capture ? `[h2a] last screen:\n${wake.capture}\n` : ""));
|
|
5747
|
+
continue;
|
|
5748
|
+
}
|
|
5600
5749
|
ok += 1;
|
|
5601
|
-
process.stderr.write(`[h2a] force-restarted ${action.slug}: ${action.cmd}
|
|
5750
|
+
process.stderr.write(`[h2a] force-restarted ${action.slug}: ${action.cmd}; objective re-injected; agent WORKING ` +
|
|
5751
|
+
`(${Math.round(wake.delivery.cpuDeltaMs)}ms CPU, ${wake.delivery.evidence})\n`);
|
|
5602
5752
|
}
|
|
5603
5753
|
catch (error) {
|
|
5604
5754
|
process.stderr.write(`[h2a] FAILED to force-restart ${action.slug} after killing ${action.name}: ${error.message}\n`);
|
|
@@ -5701,6 +5851,9 @@ export async function main(argv) {
|
|
|
5701
5851
|
});
|
|
5702
5852
|
let nudged = 0;
|
|
5703
5853
|
if (apply) {
|
|
5854
|
+
const leaseIdByTmuxSession = new Map(loadRegistry()
|
|
5855
|
+
.filter((entry) => entry.tmuxSession !== undefined)
|
|
5856
|
+
.map((entry) => [entry.tmuxSession, entry.id]));
|
|
5704
5857
|
for (const r of plan.toResume) {
|
|
5705
5858
|
// DOUBLE-CHECK the attached guard at the moment of action (the pure plan
|
|
5706
5859
|
// already excluded attached panes, but re-read in case a human just
|
|
@@ -5709,7 +5862,14 @@ export async function main(argv) {
|
|
|
5709
5862
|
process.stderr.write(`[h2a] ${r.name} (${r.type}) just got attached — skipping the nudge\n`);
|
|
5710
5863
|
continue;
|
|
5711
5864
|
}
|
|
5712
|
-
const
|
|
5865
|
+
const sessionId = leaseIdByTmuxSession.get(r.name);
|
|
5866
|
+
const ok = sessionId !== undefined
|
|
5867
|
+
? relaunchSessionWithLease({
|
|
5868
|
+
sessionId,
|
|
5869
|
+
tmuxSession: r.name,
|
|
5870
|
+
command: interactiveResumeNudge(r.type),
|
|
5871
|
+
})
|
|
5872
|
+
: relaunchInSession(r.name, interactiveResumeNudge(r.type));
|
|
5713
5873
|
if (ok) {
|
|
5714
5874
|
nudged += 1;
|
|
5715
5875
|
throttleState.set(r.name, r.next);
|