@kybernesis/create 0.15.5 → 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 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kybernesis/create",
3
- "version": "0.15.5",
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",