neurastream 0.1.7 → 0.1.8
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/README.md +1 -0
- package/package.json +1 -1
- package/src/adapters/omp/index.ts +2 -1
- package/src/adapters/omp/sessions.ts +11 -12
- package/src/core/agent-ledger.ts +8 -0
- package/src/core/main-start.ts +62 -18
package/README.md
CHANGED
|
@@ -51,6 +51,7 @@ The switch is never automatic:
|
|
|
51
51
|
- The owner's next plain message then closes the session and starts a fresh one. The fresh session opens with the note verbatim, the generated `main-start` state (including the owner holds), and then the owner's message. The previous session file stays where it is.
|
|
52
52
|
- While own jobs (subagents included) are still running, the switch waits: a rollover would cancel them, so the owner's message stays in the current session until they finish.
|
|
53
53
|
- `/main-rollover <message>` does the same by hand. It refuses below the threshold, without a fresh note, or while jobs run.
|
|
54
|
+
- `/main-rollover` shows a notice at once and gathers `main-start` in the background, so omp keeps repainting and reading keys until the new session opens.
|
|
54
55
|
- `rollover.enabled: false` turns the feature off.
|
|
55
56
|
|
|
56
57
|
## Changing a script
|
package/package.json
CHANGED
|
@@ -432,7 +432,8 @@ export function createOmpAdapter(pi: ExtensionHost, cfg: Config): void {
|
|
|
432
432
|
c.notify(`main-rollover: own jobs are still running (${c.runningJobs().join(", ")}); a rollover would cancel them.`);
|
|
433
433
|
return;
|
|
434
434
|
}
|
|
435
|
-
|
|
435
|
+
c.notify("main-rollover: collecting main-start state for the handoff (this can take a minute)…");
|
|
436
|
+
const generated = [await mainStartText([], cfg, { inheritStdin: false }), holdsBlock(q.ownerHolds())].join("\n");
|
|
436
437
|
const handoff = buildHandoff({
|
|
437
438
|
generated,
|
|
438
439
|
note: readFileSync(path, "utf8"),
|
|
@@ -54,17 +54,10 @@ function costOf(m: Record<string, unknown>): number {
|
|
|
54
54
|
return c && typeof c.total === "number" ? c.total : 0;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
run: Run;
|
|
59
|
-
mtime: number;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function build(file: string, cfg: Config): Built | null {
|
|
57
|
+
function build(file: string, cfg: Config, mtime: number): Run | null {
|
|
63
58
|
let text: string;
|
|
64
|
-
let mtime: number;
|
|
65
59
|
try {
|
|
66
60
|
text = readFileSync(file, "utf8");
|
|
67
|
-
mtime = statSync(file).mtimeMs / 1000;
|
|
68
61
|
} catch {
|
|
69
62
|
return null;
|
|
70
63
|
}
|
|
@@ -202,7 +195,7 @@ function build(file: string, cfg: Config): Built | null {
|
|
|
202
195
|
scoreSurvived: done && !scoreErr,
|
|
203
196
|
scoreEnd: scoreErr || (done ? "done" : "no final message"),
|
|
204
197
|
};
|
|
205
|
-
return
|
|
198
|
+
return run;
|
|
206
199
|
}
|
|
207
200
|
|
|
208
201
|
function readMain(file: string): MainSession {
|
|
@@ -253,9 +246,15 @@ export function ompRunSource(root: string, cfg: Config): RunSource {
|
|
|
253
246
|
const files = [...jsonlAt(root, 1), ...jsonlAt(root, 2)];
|
|
254
247
|
const runs: Run[] = [];
|
|
255
248
|
for (const file of files) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
249
|
+
let mtime: number;
|
|
250
|
+
try {
|
|
251
|
+
mtime = statSync(file).mtimeMs / 1000;
|
|
252
|
+
} catch {
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (mtime < since) continue;
|
|
256
|
+
const run = build(file, cfg, mtime);
|
|
257
|
+
if (run) runs.push(run);
|
|
259
258
|
}
|
|
260
259
|
return runs;
|
|
261
260
|
},
|
package/src/core/agent-ledger.ts
CHANGED
|
@@ -64,13 +64,21 @@ export function agentLedger(args: string[], cfg: Config, source: RunSource): num
|
|
|
64
64
|
|
|
65
65
|
if (!args.includes("--cap")) return 0;
|
|
66
66
|
const live: Record<string, string[]> = {};
|
|
67
|
+
const notLive: string[] = [];
|
|
67
68
|
for (const r of rows) {
|
|
68
69
|
if ((r.statusDetail.startsWith("RUNNING") || r.statusDetail.startsWith("WAITING")) && !r.id.startsWith("MAIN")) {
|
|
70
|
+
if ((nowSec() - r.lastWriteAt) / 60 >= cfg.ledger.idle_not_live_min) {
|
|
71
|
+
notLive.push(r.id);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
69
74
|
const provider = r.model.split("/")[0] ?? "";
|
|
70
75
|
(live[provider] ??= []).push(r.id);
|
|
71
76
|
}
|
|
72
77
|
}
|
|
73
78
|
process.stdout.write(`running per provider: ${pyDictInt(Object.fromEntries(Object.entries(live).map(([k, v]) => [k, v.length])))}\n`);
|
|
79
|
+
if (notLive.length) {
|
|
80
|
+
process.stdout.write(`NOT LIVE (no write for ≥ ${cfg.ledger.idle_not_live_min}m, not counted): ${notLive.join(", ")}\n`);
|
|
81
|
+
}
|
|
74
82
|
let full = Object.entries(live).filter(([, v]) => v.length >= cfg.ledger.max_running_per_provider).map(([k]) => k);
|
|
75
83
|
for (const k of full) process.stdout.write(`FULL ${k}: ${live[k]!.join(", ")}\n`);
|
|
76
84
|
const over = rows.filter(r => r.statusDetail.includes("OVER-BUDGET")).map(r => r.id);
|
package/src/core/main-start.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import type { Config } from "./config.ts";
|
|
@@ -8,39 +8,83 @@ function bin(name: string, cfg: Config): string {
|
|
|
8
8
|
return join(cfg.paths.home, ".local/bin", name);
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
interface RunOpts {
|
|
12
|
+
stdin?: "inherit" | "ignore" | "pipe";
|
|
13
|
+
/** Written to the child's stdin when stdin is "pipe". */
|
|
14
|
+
input?: string;
|
|
15
|
+
stderr?: "inherit" | "pipe" | "ignore";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface RunResult {
|
|
19
|
+
stdout: string;
|
|
20
|
+
stderr: string;
|
|
21
|
+
/** Set when the child could not start (e.g. ENOENT). */
|
|
22
|
+
error?: Error;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Runs a child without blocking the caller's event loop. Resolves once the child has closed its
|
|
26
|
+
// streams, or immediately when it fails to start. The handoff runs inside a live agent session,
|
|
27
|
+
// which must keep handling events while Main's state is gathered.
|
|
28
|
+
function run(file: string, args: string[], opts: RunOpts = {}): Promise<RunResult> {
|
|
29
|
+
const { promise, resolve } = Promise.withResolvers<RunResult>();
|
|
30
|
+
const child = spawn(file, args, {
|
|
31
|
+
stdio: [opts.stdin ?? "ignore", "pipe", opts.stderr ?? "pipe"],
|
|
32
|
+
});
|
|
33
|
+
let stdout = "";
|
|
34
|
+
let stderr = "";
|
|
35
|
+
child.stdout?.setEncoding("utf8").on("data", chunk => {
|
|
36
|
+
stdout += chunk;
|
|
37
|
+
});
|
|
38
|
+
child.stderr?.setEncoding("utf8").on("data", chunk => {
|
|
39
|
+
stderr += chunk;
|
|
40
|
+
});
|
|
41
|
+
// A filter such as awk may exit before it has read all its input.
|
|
42
|
+
child.stdin?.on("error", () => {});
|
|
43
|
+
if (opts.stdin === "pipe") child.stdin?.end(opts.input ?? "");
|
|
44
|
+
child.on("error", error => resolve({ stdout, stderr, error }));
|
|
45
|
+
child.on("close", () => resolve({ stdout, stderr }));
|
|
46
|
+
return promise;
|
|
47
|
+
}
|
|
48
|
+
|
|
11
49
|
// Main's session-start preflight as one text. The CLI prints it; the session rollover handoff
|
|
12
50
|
// (src/core/rollover.ts) carries the same text into the new session. Children's stdout is
|
|
13
51
|
// collected in order, so the printed output matches the original script line for line.
|
|
14
|
-
|
|
52
|
+
// inheritStdin: the CLI passes its terminal to the children that ask the operator something;
|
|
53
|
+
// an agent host passes false, because its stdin is the session's input.
|
|
54
|
+
// stderr: the CLI lets a child's stderr reach the terminal. An agent host captures it and appends it
|
|
55
|
+
// after that child's stdout, because a write to the terminal would draw over the session's UI.
|
|
56
|
+
export async function mainStartText(args: string[], cfg: Config, opts: { inheritStdin: boolean }): Promise<string> {
|
|
15
57
|
const out: string[] = [];
|
|
16
58
|
const say = (line: string) => out.push(`${line}\n`);
|
|
17
59
|
const h = args[0] ?? String(cfg.health.default_hours);
|
|
60
|
+
const childStdin = opts.inheritStdin ? "inherit" : "ignore";
|
|
61
|
+
const childStderr = opts.inheritStdin ? "inherit" : "pipe";
|
|
18
62
|
say("=== machine");
|
|
19
|
-
const mg =
|
|
20
|
-
const mgLines =
|
|
63
|
+
const mg = await run(bin("machine-guard", cfg), [], { stderr: "ignore" });
|
|
64
|
+
const mgLines = mg.stdout.trim().split("\n");
|
|
21
65
|
say(mgLines.at(-1) ?? "");
|
|
22
66
|
say(readFileSync("/proc/loadavg", "utf8").trim());
|
|
23
67
|
say("");
|
|
24
68
|
say(`=== agents (last ${h}h; DIED = check its worktree, relaunch FRESH with its progress file)`);
|
|
25
|
-
const ledger =
|
|
26
|
-
const awk =
|
|
69
|
+
const ledger = await run(bin("agent-ledger", cfg), ["--hours", h], { stderr: "ignore" });
|
|
70
|
+
const awk = await run(
|
|
27
71
|
"awk",
|
|
28
72
|
["NR==1 || /DIED/ {print; next} {ok[++n]=$0} END {for (i=1;i<=n;i++) print ok[i]}"],
|
|
29
|
-
{ input: ledger.stdout
|
|
73
|
+
{ stdin: "pipe", input: ledger.stdout, stderr: "ignore" },
|
|
30
74
|
);
|
|
31
|
-
out.push(awk.stdout
|
|
32
|
-
const cap =
|
|
33
|
-
for (const line of
|
|
75
|
+
out.push(awk.stdout);
|
|
76
|
+
const cap = await run(bin("agent-ledger", cfg), ["--cap"], { stderr: "ignore" });
|
|
77
|
+
for (const line of cap.stdout.split("\n")) {
|
|
34
78
|
if (/^(PRIORITY IDLE|FULL)/.test(line)) say(line);
|
|
35
79
|
}
|
|
36
80
|
say("");
|
|
37
81
|
say("=== main thread");
|
|
38
|
-
const health =
|
|
39
|
-
out.push(health.stdout
|
|
82
|
+
const health = await run(bin("main-health", cfg), [], { stdin: childStdin, stderr: childStderr });
|
|
83
|
+
out.push(health.stdout + health.stderr);
|
|
40
84
|
say("");
|
|
41
85
|
say("=== queue");
|
|
42
|
-
const check =
|
|
43
|
-
out.push(check.stdout
|
|
86
|
+
const check = await run(bin("queue-check", cfg), [], { stdin: childStdin, stderr: childStderr });
|
|
87
|
+
out.push(check.stdout + check.stderr);
|
|
44
88
|
say("");
|
|
45
89
|
say("=== heavy-run lock");
|
|
46
90
|
try {
|
|
@@ -52,14 +96,14 @@ export function mainStartText(args: string[], cfg: Config): string {
|
|
|
52
96
|
// The CLI file itself (shebang `#!/usr/bin/env bun`), never process.execPath: inside a host
|
|
53
97
|
// harness that is the harness binary, which would start a new agent session instead.
|
|
54
98
|
const cli = join(dirname(fileURLToPath(import.meta.url)), "../cli/main.ts");
|
|
55
|
-
const doctor =
|
|
56
|
-
const text = `${doctor.stdout
|
|
99
|
+
const doctor = await run(cli, ["doctor"]);
|
|
100
|
+
const text = `${doctor.stdout}${doctor.stderr}`;
|
|
57
101
|
out.push(text.endsWith("\n") || text === "" ? text : `${text}\n`);
|
|
58
102
|
if (doctor.error) say(doctor.error.message);
|
|
59
103
|
return out.join("");
|
|
60
104
|
}
|
|
61
105
|
|
|
62
|
-
export function mainStart(args: string[], cfg: Config): number {
|
|
63
|
-
process.stdout.write(mainStartText(args, cfg));
|
|
106
|
+
export async function mainStart(args: string[], cfg: Config): Promise<number> {
|
|
107
|
+
process.stdout.write(await mainStartText(args, cfg, { inheritStdin: true }));
|
|
64
108
|
return 0;
|
|
65
109
|
}
|