@kybernesis/create 0.15.4 → 0.15.6
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/doctor.js +16 -0
- package/dist/run-store.d.ts +22 -0
- package/dist/run-store.js +63 -0
- package/dist/upgrade.js +20 -2
- package/package.json +1 -1
package/dist/doctor.js
CHANGED
|
@@ -6,6 +6,7 @@ import { bold, capture, dim, green, parseEnv, red, yellow } from "./util.js";
|
|
|
6
6
|
import { diagnoseManageRestart, findMatchingAgentServiceUnit } from "./systemd.js";
|
|
7
7
|
import { inspectEveAgent } from "./agent-limits.js";
|
|
8
8
|
import { classifyModelReach } from "./model-reach.js";
|
|
9
|
+
import { countAbandonedRuns } from "./run-store.js";
|
|
9
10
|
import { checkSelfVersion } from "./self-version.js";
|
|
10
11
|
const MARK = {
|
|
11
12
|
pass: green("✓"),
|
|
@@ -125,6 +126,21 @@ export async function doctor() {
|
|
|
125
126
|
if (selfVersion.kind === "stale") {
|
|
126
127
|
add("warn", selfVersion.message, selfVersion.fix);
|
|
127
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Durable runs nothing will ever finish. They are replayed at every boot and
|
|
131
|
+
* eve removes none of them, so this only ever grows until something retires
|
|
132
|
+
* them. Reported rather than fixed: the remedy restarts the agent, which is
|
|
133
|
+
* not something a diagnostic should do behind someone's back.
|
|
134
|
+
*/
|
|
135
|
+
const runs = countAbandonedRuns(cwd);
|
|
136
|
+
if (runs) {
|
|
137
|
+
if (runs.abandoned === 0) {
|
|
138
|
+
add("pass", `durable run store is clean (${runs.total} runs, ${runs.held} held by a hook)`);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
add("warn", `${runs.abandoned} abandoned durable run(s) of ${runs.total} — replayed at every boot, never removed`, "scripts/safe-restart.sh, or reclaimAbandonedRuns() from @kybernesis/exe. Runs a hook still holds (routine sessions) are never retired.");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
128
144
|
// ── project shape ──────────────────────────────────────────────────────
|
|
129
145
|
if (!existsSync(join(cwd, "agent"))) {
|
|
130
146
|
console.error(red("Not an eve agent project (no agent/ directory). Run inside the agent repo."));
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How many durable runs this host is carrying that nothing will ever finish
|
|
3
|
+
* (KYB-546).
|
|
4
|
+
*
|
|
5
|
+
* eve leaves a run in `running` when a session, turn or task is abandoned, and
|
|
6
|
+
* ships nothing that retires them. They accumulate — four figures per host after
|
|
7
|
+
* a month — and every one is replayed at boot. After a durable-runtime change
|
|
8
|
+
* those replays diverge, are declared CORRUPTED_EVENT_LOG, and the conversation
|
|
9
|
+
* is dead for good.
|
|
10
|
+
*
|
|
11
|
+
* Reimplemented here rather than imported from @kybernesis/exe: doctor runs in
|
|
12
|
+
* repos that do not depend on exe, and a diagnostic that cannot run where the
|
|
13
|
+
* problem is is not a diagnostic. It only counts — retiring is exe's job.
|
|
14
|
+
*/
|
|
15
|
+
export interface RunStoreCount {
|
|
16
|
+
abandoned: number;
|
|
17
|
+
/** Held by a hook token: routine sessions live here and must never be retired. */
|
|
18
|
+
held: number;
|
|
19
|
+
total: number;
|
|
20
|
+
}
|
|
21
|
+
/** Count abandoned runs under an agent's `.eve/.workflow-data`, or null when there is no store. */
|
|
22
|
+
export declare function countAbandonedRuns(cwd: string, olderThanDays?: number, now?: Date): RunStoreCount | null;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
/** Count abandoned runs under an agent's `.eve/.workflow-data`, or null when there is no store. */
|
|
4
|
+
export function countAbandonedRuns(cwd, olderThanDays = 2, now = new Date()) {
|
|
5
|
+
const data = join(cwd, ".eve", ".workflow-data");
|
|
6
|
+
const runsDir = join(data, "runs");
|
|
7
|
+
let names;
|
|
8
|
+
try {
|
|
9
|
+
names = readdirSync(runsDir).filter((n) => n.endsWith(".json"));
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
const held = new Set();
|
|
15
|
+
const walk = (dir) => {
|
|
16
|
+
let entries;
|
|
17
|
+
try {
|
|
18
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
for (const e of entries) {
|
|
24
|
+
const full = join(dir, e.name);
|
|
25
|
+
if (e.isDirectory()) {
|
|
26
|
+
walk(full);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (!e.name.endsWith(".json"))
|
|
30
|
+
continue;
|
|
31
|
+
try {
|
|
32
|
+
const d = JSON.parse(readFileSync(full, "utf8"));
|
|
33
|
+
if (typeof d.runId === "string" && d.runId !== "")
|
|
34
|
+
held.add(d.runId);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// Unreadable hook: count nothing rather than guess a run is unheld.
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
walk(join(data, "hooks"));
|
|
42
|
+
const threshold = olderThanDays * 86_400_000;
|
|
43
|
+
const out = { abandoned: 0, held: 0, total: names.length };
|
|
44
|
+
for (const name of names) {
|
|
45
|
+
let run;
|
|
46
|
+
try {
|
|
47
|
+
run = JSON.parse(readFileSync(join(runsDir, name), "utf8"));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (run.status !== "running")
|
|
53
|
+
continue;
|
|
54
|
+
if (typeof run.runId === "string" && held.has(run.runId)) {
|
|
55
|
+
out.held += 1;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const updated = typeof run.updatedAt === "string" ? Date.parse(run.updatedAt) : NaN;
|
|
59
|
+
if (Number.isFinite(updated) && now.getTime() - updated >= threshold)
|
|
60
|
+
out.abandoned += 1;
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
package/dist/upgrade.js
CHANGED
|
@@ -977,10 +977,28 @@ export async function upgrade(options = {}) {
|
|
|
977
977
|
// reason rewriteManifest pins it: npm would save a caret by default.
|
|
978
978
|
const eveChange = changed.find((spec) => spec.startsWith("eve@"));
|
|
979
979
|
const others = changed.filter((spec) => spec !== eveChange);
|
|
980
|
+
/**
|
|
981
|
+
* A third-party extension whose declared peer range trails the certified
|
|
982
|
+
* eve (the Vercel-owned @github-tools/eve-extension and @agent-browser/eve
|
|
983
|
+
* both do) makes npm's default resolver ERESOLVE before it will write a
|
|
984
|
+
* tree at all. That is not a reason to abort the upgrade: retry the same
|
|
985
|
+
* install with --legacy-peer-deps so the tree is written, then let
|
|
986
|
+
* assessPeerTree below judge WHO actually objects. A @kybernesis or root
|
|
987
|
+
* conflict still stops everything; only a stale third-party edge is
|
|
988
|
+
* tolerated (same principle as the KYB-548 peer-tree gate). KYB-561.
|
|
989
|
+
*/
|
|
990
|
+
const npmInstall = (args) => {
|
|
991
|
+
let result = commandResult("npm", args, cwd);
|
|
992
|
+
if (result.status !== 0 && /ERESOLVE/i.test(`${result.stdout}\n${result.stderr}`)) {
|
|
993
|
+
console.log(yellow("\n ! npm reported a peer conflict. Retrying with --legacy-peer-deps so the installed peer tree can be assessed."));
|
|
994
|
+
result = commandResult("npm", [...args, "--legacy-peer-deps"], cwd);
|
|
995
|
+
}
|
|
996
|
+
return result;
|
|
997
|
+
};
|
|
980
998
|
const installArgs = cleanInstall ? ["install"] : ["install", ...others];
|
|
981
|
-
let installed = cleanInstall || others.length > 0 ?
|
|
999
|
+
let installed = cleanInstall || others.length > 0 ? npmInstall(installArgs) : { status: 0, stdout: "", stderr: "" };
|
|
982
1000
|
if (installed.status === 0 && !cleanInstall && eveChange) {
|
|
983
|
-
installed =
|
|
1001
|
+
installed = npmInstall(["install", "--save-exact", eveChange]);
|
|
984
1002
|
}
|
|
985
1003
|
if (installed.status !== 0) {
|
|
986
1004
|
const output = `${installed.stdout}\n${installed.stderr}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kybernesis/create",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.6",
|
|
4
4
|
"description": "The Kybernesis agent scaffolder and FDE toolkit: one command to a governed, remembering, multiplayer, self-testing eve agent — plus doctor and upgrade.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|