@proagentstore/cli 0.4.93 → 0.4.95
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.
|
@@ -13,16 +13,35 @@ export class CodingRuntime {
|
|
|
13
13
|
reposBaseDir;
|
|
14
14
|
sessions = new Map();
|
|
15
15
|
/**
|
|
16
|
-
* Coding
|
|
16
|
+
* Coding turns that are running here (#896, #1007).
|
|
17
17
|
*
|
|
18
|
-
* A
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* A retained session is a conversation, not necessarily work in flight: a healthy engine remains
|
|
19
|
+
* available at `runState: "idle"` between turns. Runner updates must protect only turns that can
|
|
20
|
+
* be interrupted. If this runtime cannot observe a session's state, retain the conservative busy
|
|
21
|
+
* answer rather than restarting across work whose state it cannot prove is idle.
|
|
21
22
|
*/
|
|
22
23
|
liveWork() {
|
|
23
|
-
return [...this.sessions.
|
|
24
|
+
return [...this.sessions.entries()].flatMap(([sessionId, s]) => {
|
|
24
25
|
const engine = s.clientType ?? "";
|
|
25
|
-
|
|
26
|
+
const consequence = engine === "claude" ? "Claude conversation resumes after a restart" : `${engine || "coding"} turn is lost on a restart`;
|
|
27
|
+
try {
|
|
28
|
+
// `alive: false` is an observed inactive session. Any other answer (including a
|
|
29
|
+
// malformed older implementation) is unknown and must fail closed below.
|
|
30
|
+
const alive = s.alive;
|
|
31
|
+
if (alive === false)
|
|
32
|
+
return [];
|
|
33
|
+
const state = s.runState();
|
|
34
|
+
if (alive === true && state === "idle")
|
|
35
|
+
return [];
|
|
36
|
+
if (alive === true && (state === "thinking" || state === "responding"))
|
|
37
|
+
return [`coding session ${sessionId} (${state}): ${consequence}`];
|
|
38
|
+
// An unrecognised `alive`/`runState` pair is not proof that an engine is idle.
|
|
39
|
+
return [`coding session ${sessionId} (${typeof state === "string" ? state : "unknown"}): ${consequence}`];
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Do not turn a failed state probe into permission to restart the machine.
|
|
43
|
+
return [`coding session ${sessionId} (unknown): ${consequence}`];
|
|
44
|
+
}
|
|
26
45
|
});
|
|
27
46
|
}
|
|
28
47
|
/** Background cold-start clones, one per folder (#858). */
|
|
@@ -401,6 +401,19 @@ export class LocalArtifactRuntime {
|
|
|
401
401
|
return;
|
|
402
402
|
if (run.timer)
|
|
403
403
|
clearTimeout(run.timer);
|
|
404
|
+
// An uploaded source is a bounded, per-run transfer, not runner state. The durable
|
|
405
|
+
// result deliberately contains only its handle and hash; remove the materialized text as
|
|
406
|
+
// soon as the engine no longer needs it instead of retaining it with generic scratch data.
|
|
407
|
+
// `end` is the one terminal path (completion, refusal, timeout, cancellation and launch
|
|
408
|
+
// failure), so a source cannot linger just because a particular failure path was missed.
|
|
409
|
+
if (run.envelope.uploadedSources?.length) {
|
|
410
|
+
try {
|
|
411
|
+
rmSync(join(run.scratch, "sources"), { recursive: true, force: true });
|
|
412
|
+
}
|
|
413
|
+
catch { /* best-effort cleanup */ }
|
|
414
|
+
for (const source of run.envelope.uploadedSources)
|
|
415
|
+
source.text = "";
|
|
416
|
+
}
|
|
404
417
|
run.state = "ended";
|
|
405
418
|
run.endedAt = this.now();
|
|
406
419
|
run.result = {
|
package/dist/index.js
CHANGED
|
@@ -1269,6 +1269,19 @@ function restarterFrom(env) {
|
|
|
1269
1269
|
function supervisorNote(restarter) {
|
|
1270
1270
|
return restarter === "pags-up-child-only" ? "The `pags up` on this machine predates supervisor restarts (#860): the runner comes back on the new release, but the `pags up` window keeps running its own older code until it is restarted there once." : null;
|
|
1271
1271
|
}
|
|
1272
|
+
var MAX_DEFERRED_UPDATE_LEASE_MS = 5 * 6e4;
|
|
1273
|
+
function deferredUpdateLease(value, now = Date.now()) {
|
|
1274
|
+
if (!value || typeof value !== "object") return null;
|
|
1275
|
+
const raw = value;
|
|
1276
|
+
const operationId = typeof raw.operationId === "string" ? raw.operationId.trim() : "";
|
|
1277
|
+
const expiresAt = typeof raw.deferUntil === "number" ? raw.deferUntil : NaN;
|
|
1278
|
+
if (!operationId || operationId.length > 128 || !Number.isFinite(expiresAt)) return null;
|
|
1279
|
+
if (expiresAt <= now || expiresAt > now + MAX_DEFERRED_UPDATE_LEASE_MS) return null;
|
|
1280
|
+
return { operationId, expiresAt };
|
|
1281
|
+
}
|
|
1282
|
+
function deferredUpdateLeaseActive(lease, now = Date.now()) {
|
|
1283
|
+
return now < lease.expiresAt;
|
|
1284
|
+
}
|
|
1272
1285
|
function leaveForRestart(restarter, deps = { spawn: spawn3, exit: (code) => process.exit(code) }) {
|
|
1273
1286
|
if (restarter === "command") {
|
|
1274
1287
|
deps.spawn(process.env[RESTART_COMMAND_ENV], { shell: true, detached: true, stdio: "ignore", env: process.env }).unref();
|
|
@@ -1864,6 +1877,29 @@ var RelayMutationAdmission = class {
|
|
|
1864
1877
|
|
|
1865
1878
|
// src/commands/runner/relay.ts
|
|
1866
1879
|
var MEMBERSHIP_SYNC_PATH = "/pags/membership/sync";
|
|
1880
|
+
function observedRunnerWork(observation) {
|
|
1881
|
+
const sessions = observation.sessions;
|
|
1882
|
+
const work = observation.work;
|
|
1883
|
+
if (!Array.isArray(sessions) || !work || typeof work !== "object") return ["runner-work-observation-unavailable"];
|
|
1884
|
+
const codingTurns = typeof work.codingTurns === "number" && Number.isInteger(work.codingTurns) && work.codingTurns >= 0 ? work.codingTurns : null;
|
|
1885
|
+
const localRuns = typeof work.localRuns === "number" && Number.isInteger(work.localRuns) && work.localRuns >= 0 ? work.localRuns : null;
|
|
1886
|
+
const detail = Array.isArray(work.detail) && work.detail.every((item) => typeof item === "string") ? work.detail : null;
|
|
1887
|
+
if (codingTurns === null || localRuns === null || detail === null) return ["runner-work-observation-unavailable"];
|
|
1888
|
+
const activeCoding = sessions.flatMap((session) => {
|
|
1889
|
+
if (session?.alive === false) return [];
|
|
1890
|
+
const id = typeof session?.sessionId === "string" && session.sessionId ? session.sessionId : "unidentified-session";
|
|
1891
|
+
const state = typeof session?.runState === "string" && session.runState ? session.runState : "unknown";
|
|
1892
|
+
if (session?.alive === true && state === "idle") return [];
|
|
1893
|
+
return [`coding session ${id} (${state})`];
|
|
1894
|
+
});
|
|
1895
|
+
const unmatchedCodingTurns = Math.max(0, codingTurns - activeCoding.length);
|
|
1896
|
+
const localDetail = detail.slice(codingTurns);
|
|
1897
|
+
return [
|
|
1898
|
+
...activeCoding,
|
|
1899
|
+
...Array.from({ length: unmatchedCodingTurns }, (_v, i) => `coding-turn-unmatched-${i + 1}`),
|
|
1900
|
+
...Array.from({ length: localRuns }, (_v, i) => localDetail[i] || `local-run-${i + 1}`)
|
|
1901
|
+
];
|
|
1902
|
+
}
|
|
1867
1903
|
var CLI_CONTROL_PATHS = /* @__PURE__ */ new Set([MEMBERSHIP_SYNC_PATH, RUNNER_UPDATE_PATH]);
|
|
1868
1904
|
async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force = false, watchInstances = false) {
|
|
1869
1905
|
const apiBase = pagsApiBase(opts.apiBase).replace(/^http/, "ws");
|
|
@@ -1967,12 +2003,7 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
|
|
|
1967
2003
|
}),
|
|
1968
2004
|
requestRunner("GET", "/health", { url: localUrl, token: runnerToken, instanceId: instanceIds[0] })
|
|
1969
2005
|
]);
|
|
1970
|
-
const
|
|
1971
|
-
const sessions = sessionsResult.status === "fulfilled" ? sessionsResult.value : { sessions: [] };
|
|
1972
|
-
const health = healthResult.status === "fulfilled" ? healthResult.value : {};
|
|
1973
|
-
const activeCoding = (sessions.sessions ?? []).filter((s) => s.alive && s.runState && s.runState !== "idle").map((s) => s.sessionId);
|
|
1974
|
-
const localRuns = health.work?.localRuns ?? 0;
|
|
1975
|
-
const codingTurns = health.work?.codingTurns ?? 0;
|
|
2006
|
+
const busy = sessionsResult.status === "fulfilled" && healthResult.status === "fulfilled" ? observedRunnerWork({ sessions: sessionsResult.value.sessions, work: healthResult.value.work }) : ["runner-work-observation-unavailable"];
|
|
1976
2007
|
return {
|
|
1977
2008
|
current: CLI_VERSION,
|
|
1978
2009
|
latest: await latestPublishedVersion(),
|
|
@@ -1980,14 +2011,14 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
|
|
|
1980
2011
|
restarter: restarterFrom(process.env),
|
|
1981
2012
|
// A restart ends browser/application work too. Give these durable names to the same
|
|
1982
2013
|
// planner so neither manual nor automatic update can mistake them for an idle machine.
|
|
1983
|
-
busy
|
|
2014
|
+
busy
|
|
1984
2015
|
};
|
|
1985
2016
|
};
|
|
1986
2017
|
let updateInstallInFlight = false;
|
|
1987
2018
|
let installedPendingRestart = null;
|
|
1988
2019
|
const relayMutationAdmission = new RelayMutationAdmission();
|
|
1989
|
-
const installAndRestart = async (plan, automatic = false) => {
|
|
1990
|
-
if (updateInstallInFlight) return false;
|
|
2020
|
+
const installAndRestart = async (plan, automatic = false, lease) => {
|
|
2021
|
+
if (updateInstallInFlight || lease && !deferredUpdateLeaseActive(lease)) return false;
|
|
1991
2022
|
updateInstallInFlight = true;
|
|
1992
2023
|
if (automatic && (!await refreshAutomaticPolicy() || !autoUpdatePolicy.autoUpdate)) {
|
|
1993
2024
|
updateInstallInFlight = false;
|
|
@@ -2003,9 +2034,14 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
|
|
|
2003
2034
|
throw error;
|
|
2004
2035
|
}
|
|
2005
2036
|
}
|
|
2037
|
+
await relayMutationAdmission.drain();
|
|
2038
|
+
await new Promise((resolve5) => setTimeout(resolve5, 500));
|
|
2039
|
+
if (lease && !deferredUpdateLeaseActive(lease)) {
|
|
2040
|
+
relayMutationAdmission.resume();
|
|
2041
|
+
updateInstallInFlight = false;
|
|
2042
|
+
return false;
|
|
2043
|
+
}
|
|
2006
2044
|
if (automatic) {
|
|
2007
|
-
await relayMutationAdmission.drain();
|
|
2008
|
-
await new Promise((resolve5) => setTimeout(resolve5, 500));
|
|
2009
2045
|
if (!await refreshAutomaticPolicy() || !autoUpdatePolicy.autoUpdate) {
|
|
2010
2046
|
relayMutationAdmission.resume();
|
|
2011
2047
|
updateInstallInFlight = false;
|
|
@@ -2020,15 +2056,22 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
|
|
|
2020
2056
|
}
|
|
2021
2057
|
writeLine(`Installed ${plan.latest} \u2014 restarting; every agent re-attaches on the way back up.`);
|
|
2022
2058
|
for (const id of [...attached.keys()]) detach(id);
|
|
2023
|
-
leaveForRestart(plan.restarter);
|
|
2024
2059
|
return true;
|
|
2025
2060
|
};
|
|
2026
2061
|
let updateWaiting = false;
|
|
2027
|
-
|
|
2028
|
-
|
|
2062
|
+
let deferredManualLease = null;
|
|
2063
|
+
const updateWhenIdle = (automatic = false, lease) => {
|
|
2064
|
+
if (updateWaiting || !automatic && !lease) return false;
|
|
2029
2065
|
updateWaiting = true;
|
|
2030
|
-
|
|
2066
|
+
if (!automatic) deferredManualLease = lease;
|
|
2067
|
+
const until = automatic ? Date.now() + 60 * 6e4 : lease.expiresAt;
|
|
2031
2068
|
const tick = () => setTimeout(async () => {
|
|
2069
|
+
if (!automatic && (!deferredManualLease || !deferredUpdateLeaseActive(deferredManualLease))) {
|
|
2070
|
+
writeLine("runner_update: the deferred operation lease expired; no delayed restart was performed.");
|
|
2071
|
+
deferredManualLease = null;
|
|
2072
|
+
updateWaiting = false;
|
|
2073
|
+
return;
|
|
2074
|
+
}
|
|
2032
2075
|
if (automatic && (!await refreshAutomaticPolicy() || !autoUpdatePolicy.autoUpdate)) {
|
|
2033
2076
|
saveAutoUpdateStatus("offline", { reason: "The owner policy could not be refreshed; automatic update is deferred." });
|
|
2034
2077
|
updateWaiting = false;
|
|
@@ -2041,24 +2084,30 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
|
|
|
2041
2084
|
return;
|
|
2042
2085
|
}
|
|
2043
2086
|
if (automatic) saveAutoUpdateStatus("running", { latestVersion: plan.latest, reason: void 0 });
|
|
2044
|
-
const installed = await installAndRestart(plan, automatic).catch((e) => {
|
|
2087
|
+
const installed = await installAndRestart(plan, automatic, automatic ? void 0 : deferredManualLease ?? void 0).catch((e) => {
|
|
2045
2088
|
if (automatic) saveAutoUpdateStatus("failure", { reason: e instanceof Error ? e.message : String(e) });
|
|
2046
2089
|
writeError(`runner_update: install failed \u2014 ${e instanceof Error ? e.message : String(e)}`);
|
|
2047
2090
|
return false;
|
|
2048
2091
|
});
|
|
2049
|
-
if (
|
|
2092
|
+
if (!installed && Date.now() < until) {
|
|
2050
2093
|
saveAutoUpdateStatus("waiting-for-idle", { latestVersion: plan.latest });
|
|
2051
2094
|
tick();
|
|
2052
|
-
} else
|
|
2095
|
+
} else {
|
|
2096
|
+
deferredManualLease = null;
|
|
2097
|
+
updateWaiting = false;
|
|
2098
|
+
if (installed) leaveForRestart(plan.restarter);
|
|
2099
|
+
}
|
|
2053
2100
|
} else if (plan.action === "wait" && Date.now() < until) {
|
|
2054
2101
|
if (automatic) saveAutoUpdateStatus("waiting-for-idle", { latestVersion: plan.latest, reason: void 0 });
|
|
2055
2102
|
tick();
|
|
2056
2103
|
} else {
|
|
2057
2104
|
if (automatic && plan.action === "refused") saveAutoUpdateStatus("unsupported", { reason: plan.reason });
|
|
2105
|
+
deferredManualLease = null;
|
|
2058
2106
|
updateWaiting = false;
|
|
2059
2107
|
}
|
|
2060
2108
|
}, 15e3).unref();
|
|
2061
2109
|
tick();
|
|
2110
|
+
return true;
|
|
2062
2111
|
};
|
|
2063
2112
|
let automaticTimer = null;
|
|
2064
2113
|
let automaticChecking = false;
|
|
@@ -2138,7 +2187,11 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
|
|
|
2138
2187
|
plan: planRunnerUpdate,
|
|
2139
2188
|
// This bridge is deliberately the same function used by explicit runner_update. It owns npm,
|
|
2140
2189
|
// a fresh authoritative policy, the mutation drain, final local-work observation and restart.
|
|
2141
|
-
installAndRestart,
|
|
2190
|
+
installAndRestart: async (plan, automatic) => {
|
|
2191
|
+
const installed = await installAndRestart(plan, automatic);
|
|
2192
|
+
if (installed) setTimeout(() => leaveForRestart(plan.restarter), 0).unref();
|
|
2193
|
+
return installed;
|
|
2194
|
+
},
|
|
2142
2195
|
status: saveAutoUpdateStatus
|
|
2143
2196
|
});
|
|
2144
2197
|
scheduleAutomaticUpdate = (initial = false, cancel = false) => automaticUpdateController.onPolicy(cancel ? "cancel" : initial ? "start" : "keep");
|
|
@@ -2148,15 +2201,18 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
|
|
|
2148
2201
|
const plan = planRunnerUpdate(await updateFacts());
|
|
2149
2202
|
if (dryRun || plan.action === "up-to-date" || plan.action === "refused") return { status: 200, result: { ...plan, dryRun } };
|
|
2150
2203
|
if (plan.action === "wait") {
|
|
2151
|
-
|
|
2152
|
-
return { status:
|
|
2204
|
+
const lease = deferredUpdateLease(body);
|
|
2205
|
+
if (!lease) return { status: 400, result: { error: "A busy runner_update needs a current operation lease; no delayed restart was scheduled." } };
|
|
2206
|
+
if (!updateWhenIdle(false, lease)) return { status: 409, result: { error: "A runner update is already waiting for active work to finish." } };
|
|
2207
|
+
return { status: 200, result: { ...plan, operationId: lease.operationId, deferredUntil: lease.expiresAt, detail: "This operation may restart only before its recorded lease expires; no delayed restart survives an expired or terminal operation." } };
|
|
2153
2208
|
}
|
|
2154
2209
|
try {
|
|
2155
|
-
if (!await
|
|
2210
|
+
if (!await installAndRestart(plan)) return { status: 409, result: { error: "A runner update is already installing or restarting." } };
|
|
2156
2211
|
} catch (e) {
|
|
2157
2212
|
return { status: 500, result: { error: `npm could not install ${plan.latest}: ${e instanceof Error ? e.message : String(e)}` } };
|
|
2158
2213
|
}
|
|
2159
2214
|
const supervisor = supervisorNote(plan.restarter);
|
|
2215
|
+
setTimeout(() => leaveForRestart(plan.restarter), 25).unref();
|
|
2160
2216
|
return { status: 200, result: { action: "restarting", current: plan.current, latest: plan.latest, restartedBy: plan.restarter, ...supervisor ? { supervisor } : {} } };
|
|
2161
2217
|
};
|
|
2162
2218
|
const answerControl = async (path, body) => {
|