@opennous/cli 0.19.2 → 0.19.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/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # @opennous/cli changelog
2
2
 
3
+ ## 0.19.3
4
+
5
+ ### Changed: history reads go faster
6
+
7
+ - While your history is read, the runner keeps your Mac from idle-sleeping (closing the lid still
8
+ sleeps it), and lets go five minutes after the reading stops (GET-1353).
9
+ - A call cut off by the machine sleeping is read again instead of failing.
10
+ - Six calls are answered at once instead of three. `NOUS_RUNNER_WORKERS` sets it (1 to 8).
11
+
12
+ ## 0.19.2
13
+
14
+ ### Fixed: `runner install` keeps `--agent` and `--workspace`
15
+
16
+ `runner install --agent codex --workspace <id>` installed a runner for whichever agent you were
17
+ signed in to and the workspace of your saved key (GET-1345).
18
+
3
19
  ## 0.19.1
4
20
 
5
21
  ### Changed: the first history read goes as far back as your workspace chose
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opennous/cli",
3
- "version": "0.19.2",
3
+ "version": "0.19.3",
4
4
  "description": "Ownable CLI — set up Ownable, connect it to your coding agent, and read the Context API from your terminal",
5
5
  "type": "module",
6
6
  "bin": {
package/src/index.js CHANGED
@@ -664,7 +664,7 @@ const runnerCmd = program
664
664
  .option("--url <url>", "API base URL (default: https://api.opennous.cloud)")
665
665
  .action(async (opts) => {
666
666
  const { detectAgents, installedAgents, liveRun, NO_AGENT } = await import("./backfill.js");
667
- const { runRunner } = await import("./runner.js");
667
+ const { runRunner, keepAwake } = await import("./runner.js");
668
668
  const agent = await pickAgent(opts.agent, opts.apiKey ? installedAgents : detectAgents, NO_AGENT);
669
669
  const apiKey = opts.apiKey ? savedModelKey(agent) : null;
670
670
  if (opts.apiKey && !apiKey) {
@@ -679,6 +679,7 @@ const runnerCmd = program
679
679
  console.log(stamp(`Reading on your ${apiKey ? `${agent === "codex" ? "OpenAI" : "Anthropic"} API key` : `${agent} plan`}. ${service ? "" : "Stop with Ctrl-C; `nous runner install` keeps it going on its own."}`.trim()));
680
680
  await runRunner({
681
681
  agent, host, version, auth: apiKey ? "api_key" : "plan", run: liveRun({ agent, apiKey }), log: (l) => console.log(stamp(l)),
682
+ awake: keepAwake({ log: (l) => console.log(stamp(l)) }),
682
683
  sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
683
684
  api: {
684
685
  poll: (body) => api.softPost("/v2/runner/poll", body),
package/src/runner.js CHANGED
@@ -4,13 +4,60 @@
4
4
  // a call's prompt holds other people's words), and posts the answer back. The server decides what to
5
5
  // read; this only answers. `nous runner install` starts it at login, so it keeps going on its own.
6
6
 
7
- import { spawnSync } from "node:child_process";
7
+ import { spawn as spawnChild, spawnSync } from "node:child_process";
8
8
  import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
9
9
  import { homedir, hostname, platform } from "node:os";
10
10
  import { dirname, join } from "node:path";
11
11
  import { commandFor, parseAnswer, troubleOf, resetAt } from "./backfill.js";
12
12
 
13
- export const WORKERS = 3;
13
+ /** Calls answered at once (GET-1353). NOUS_RUNNER_WORKERS overrides; the server hands out at most 8 a poll. */
14
+ export const WORKERS = workersFrom(process.env.NOUS_RUNNER_WORKERS);
15
+ /** How long without work before the Mac may sleep again. */
16
+ export const AWAKE_IDLE_MS = 5 * 60_000;
17
+ /** A call whose wall clock ran this much past the machine's own clock spanned a sleep. */
18
+ export const SLEPT_MS = 60_000;
19
+
20
+ /** NOUS_RUNNER_WORKERS as a count: 1 to 8, else 6. Pure. */
21
+ export function workersFrom(value) {
22
+ const n = Math.floor(Number(value));
23
+ return Number.isFinite(n) && n >= 1 ? Math.min(n, 8) : 6;
24
+ }
25
+
26
+ /**
27
+ * Keeps a Mac from idle-sleeping while there is reading to do (GET-1353): a sleeping Mac answers no
28
+ * calls, and a three-month read took hours of naps. `caffeinate -i -w <pid>` holds off idle sleep
29
+ * only (closing the lid still sleeps) and lets go by itself when this process ends. `touch()` on
30
+ * work, `tick()` every loop: after AWAKE_IDLE_MS with no work it lets go. Not a Mac: does nothing.
31
+ */
32
+ export function keepAwake({ os = platform(), spawn = spawnChild, pid = process.pid, log = () => {}, now = Date.now, idleMs = AWAKE_IDLE_MS } = {}) {
33
+ let child = null, lastWork = 0;
34
+ const release = () => {
35
+ if (!child) return;
36
+ try { child.kill(); } catch { /* already gone */ }
37
+ child = null;
38
+ log("Reading is idle. This Mac may sleep again.");
39
+ };
40
+ return {
41
+ touch() {
42
+ lastWork = now();
43
+ if (child || os !== "darwin") return;
44
+ try {
45
+ child = spawn("/usr/bin/caffeinate", ["-i", "-w", String(pid)], { stdio: "ignore" });
46
+ child.on?.("error", () => { child = null; });
47
+ child.unref?.();
48
+ log("Keeping this Mac awake while your history is read.");
49
+ } catch { child = null; }
50
+ },
51
+ tick() { if (child && now() - lastWork >= idleMs) release(); },
52
+ release,
53
+ get holding() { return !!child; },
54
+ };
55
+ }
56
+
57
+ /** Is there reading to do: calls in hand, or a history read under way. Pure. */
58
+ export function hasWork(page) {
59
+ return (page?.calls?.length ?? 0) > 0 || !!(page?.reading && !page.reading.live && !page.reading.finished);
60
+ }
14
61
  /**
15
62
  * How long the server may hold a poll open waiting for a call (GET-1307). Idle, the runner checks in
16
63
  * about once a minute and still picks up a new call within seconds.
@@ -76,10 +123,13 @@ export function pauseLine(agent, reason, until, now = Date.now()) {
76
123
  * `stop()` → true ends it (tests; the real one runs until the process is stopped).
77
124
  */
78
125
  export async function runRunner({ api, agent, host = hostname(), version = null, auth = "plan", run, sleep, log, now = Date.now,
79
- workers = WORKERS, limitWaitMs = LIMIT_WAIT_MS, stop = () => false }) {
80
- let limitedUntil = 0, limitReason = null, answered = 0, lastLine = null, lastError = null, backfillAt = -Infinity;
126
+ workers = WORKERS, limitWaitMs = LIMIT_WAIT_MS, stop = () => false, awake = null, clock = () => performance.now() }) {
127
+ let limitedUntil = 0, limitReason = null, answered = 0, lastLine = null, lastError = null, backfillAt = -Infinity, sleptNoted = false;
81
128
  const answerOne = async (call, models) => {
82
129
  for (let attempt = 0; ; attempt++) {
130
+ // performance.now() stops while a Mac sleeps; the wall clock doesn't. A call that failed across
131
+ // a sleep (the CLI timed out, the network dropped) isn't the call's fault (GET-1353).
132
+ const wall0 = now(), mono0 = clock();
83
133
  try {
84
134
  const { text, usage } = parseAnswer(agent, await run(commandFor(agent, models[agent], call)));
85
135
  await api.answer(call.id, { answer: text, model: models[agent].model, usage });
@@ -90,6 +140,12 @@ export async function runRunner({ api, agent, host = hostname(), version = null,
90
140
  // The plan's limit, a sign-out or an outage (GET-1303): none of them is the call's fault, so
91
141
  // it is dropped, not failed (its lease runs out and it comes back), and nothing more is taken
92
142
  // until it passes. The heartbeat keeps going, so the app shows why.
143
+ if ((now() - wall0) - (clock() - mono0) > SLEPT_MS) {
144
+ // Dropped, not failed: its lease runs out and it comes back, the meeting stays unread.
145
+ if (!sleptNoted) log("This machine slept during a call. It's read again; nothing is lost.");
146
+ sleptNoted = true;
147
+ return;
148
+ }
93
149
  const reason = troubleOf(err.message);
94
150
  if (reason) {
95
151
  const until = reason === "limit" ? (resetAt(err.message, now()) ?? now() + limitWaitMs) : now() + RECHECK_MS;
@@ -129,6 +185,7 @@ export async function runRunner({ api, agent, host = hostname(), version = null,
129
185
  log(`Your history read can't start yet (${err.message}). Trying again in ${BACKFILL_RETRY_MS / 60000} minutes.`);
130
186
  }
131
187
  }
188
+ if (awake) { if (hasWork(page)) awake.touch(); else awake.tick(); }
132
189
  const line = readingLine(page.reading);
133
190
  if (line && line !== lastLine) { log(line); }
134
191
  lastLine = line;
@@ -140,7 +197,9 @@ export async function runRunner({ api, agent, host = hostname(), version = null,
140
197
  continue;
141
198
  }
142
199
  await Promise.all(calls.map((c) => answerOne(c, page.models)));
200
+ sleptNoted = false;
143
201
  }
202
+ awake?.release();
144
203
  return { answered };
145
204
  }
146
205