byollm 0.1.0-alpha.74 → 0.1.0-alpha.76

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 CHANGED
@@ -1,5 +1,5 @@
1
1
  > [!WARNING]
2
- > **Alpha (`0.1.0-alpha.74`) — under active development. Don't use this yet.**
2
+ > **Alpha (`0.1.0-alpha.76`) — under active development. Don't use this yet.**
3
3
  >
4
4
  > Install it deliberately: `npx byollm@alpha`, or `npm install byollm@alpha`.
5
5
  >
package/dist/bin.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  main
4
- } from "./chunk-YFCNYJLR.js";
4
+ } from "./chunk-MSRBBLRQ.js";
5
5
 
6
6
  // src/bin.ts
7
7
  process.exitCode = await main(process.argv.slice(2));
@@ -53,6 +53,32 @@ function asHttpOrigin(candidate) {
53
53
  // src/cli.ts
54
54
  import { access } from "fs/promises";
55
55
 
56
+ // src/emphasis.ts
57
+ var REVERSE = "\x1B[7m";
58
+ var BOLD = "\x1B[1m";
59
+ var RESET = "\x1B[0m";
60
+ function emphasisAllowed(context2) {
61
+ if (context2.env["NO_COLOR"] !== void 0) return false;
62
+ if (context2.env["FORCE_COLOR"] !== void 0) return true;
63
+ return context2.tty;
64
+ }
65
+ function emphasise(value, context2) {
66
+ return emphasisAllowed(context2) ? `${REVERSE}${BOLD} ${value} ${RESET}` : value;
67
+ }
68
+ function terminalContext() {
69
+ return {
70
+ // `@types/node` declares `isTTY` as `boolean`, and Node sets it to
71
+ // `undefined` when the stream is not a terminal. So this comparison is
72
+ // load-bearing even though the type says it cannot be — the linter is
73
+ // reasoning from a declaration that is wrong about its own runtime, and
74
+ // deleting it puts `undefined` into a field typed `boolean`. The same
75
+ // note stands over `setup.ts`, which met this first.
76
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-boolean-literal-compare
77
+ tty: process.stdout.isTTY === true,
78
+ env: process.env
79
+ };
80
+ }
81
+
56
82
  // src/model.ts
57
83
  import { readFile, writeFile } from "fs/promises";
58
84
 
@@ -218,8 +244,8 @@ function until(message, now) {
218
244
  if (Number.isNaN(parsed)) return void 0;
219
245
  return parsed > now ? parsed : void 0;
220
246
  }
221
- function quotaBlock(message, now) {
222
- if (!OBSERVED.some((seen) => seen.pattern.test(message))) return void 0;
247
+ function quotaBlock(message, now, corpus = OBSERVED) {
248
+ if (!corpus.some((seen) => seen.pattern.test(message))) return void 0;
223
249
  const at = until(message, now);
224
250
  return { detail: message, ...at === void 0 ? {} : { until: at } };
225
251
  }
@@ -234,7 +260,6 @@ async function runProcessJob(job) {
234
260
  ok: false,
235
261
  code: "backend-error",
236
262
  message: "no time limit was set for this call",
237
- retryable: false,
238
263
  durationMs: 0
239
264
  };
240
265
  }
@@ -253,7 +278,6 @@ function spawnIn(job, scratch) {
253
278
  ok: false,
254
279
  code: "canceled",
255
280
  message: "the job was canceled before it started",
256
- retryable: false,
257
281
  durationMs: Date.now() - started
258
282
  });
259
283
  return;
@@ -316,7 +340,6 @@ function spawnIn(job, scratch) {
316
340
  ok: false,
317
341
  code: "backend-unreachable",
318
342
  message: `could not start ${displayName}: ${error.message}`,
319
- retryable: false,
320
343
  durationMs: Date.now() - started
321
344
  });
322
345
  });
@@ -328,7 +351,6 @@ function spawnIn(job, scratch) {
328
351
  ok: false,
329
352
  code: "canceled",
330
353
  message: "the job was canceled",
331
- retryable: false,
332
354
  durationMs
333
355
  });
334
356
  return;
@@ -338,7 +360,6 @@ function spawnIn(job, scratch) {
338
360
  ok: false,
339
361
  code: "timeout",
340
362
  message: `the model did not answer within ${String(request.timeoutMs)}ms`,
341
- retryable: true,
342
363
  durationMs
343
364
  });
344
365
  return;
@@ -348,7 +369,6 @@ function spawnIn(job, scratch) {
348
369
  ok: false,
349
370
  code: "output-too-large",
350
371
  message: `the model produced more than ${String(request.maxOutputBytes)} bytes`,
351
- retryable: false,
352
372
  durationMs
353
373
  });
354
374
  return;
@@ -356,13 +376,20 @@ function spawnIn(job, scratch) {
356
376
  if (code !== 0) {
357
377
  const said = `${stdout}
358
378
  ${stderr}`;
359
- const blocked = quotaBlock(said, Date.now());
379
+ const blocked = quotaBlock(said, (job.now ?? Date.now)(), job.corpus);
360
380
  const authFailed = blocked === void 0 && isAuthFailure(said);
361
381
  finish({
362
382
  ok: false,
363
383
  code: blocked !== void 0 ? "quota-exhausted" : authFailed ? "unauthorized" : "backend-error",
364
384
  ...blocked?.until === void 0 ? {} : { until: blocked.until },
365
- message: blocked !== void 0 ? `${displayName} is out of quota for now` : authFailed ? `${displayName} is not signed in` : stderr.trim() !== "" ? `${displayName} failed: ${firstLine(stderr)}` : (
385
+ message: blocked !== void 0 ? (
386
+ /* The CLI's own words, as the Codex path already carries —
387
+ M2, 2026-09-04. This said only "out of quota for now",
388
+ throwing away the sentence that names the time, on the one
389
+ surface entitled to it. Owner-only: the class is all a site
390
+ ever sees. */
391
+ `${displayName} is out of quota: ${firstLine(blocked.detail)}`
392
+ ) : authFailed ? `${displayName} is not signed in` : stderr.trim() !== "" ? `${displayName} failed: ${firstLine(stderr)}` : (
366
393
  /* Nothing on stderr does not mean nothing was said. An
367
394
  agentic CLI writes its errors to stdout — that is where
368
395
  "Failed to authenticate" arrived — and reporting a bare
@@ -371,7 +398,6 @@ ${stderr}`;
371
398
  goes to the site on success. */
372
399
  stdout.trim() !== "" ? `${displayName} failed: ${firstLine(stdout)}` : `${displayName} exited with status ${String(code)}`
373
400
  ),
374
- retryable: false,
375
401
  durationMs
376
402
  });
377
403
  return;
@@ -658,16 +684,20 @@ function failureMessage(event) {
658
684
  const error = record(event["error"]);
659
685
  return diagnostic(event["message"]) ?? diagnostic(error?.["message"]) ?? "codex reported an error";
660
686
  }
661
- function classifyFailure(message) {
662
- if (quotaBlock(message, Date.now()) !== void 0) {
663
- return { code: "quota-exhausted", retryable: true };
687
+ function classifyFailure(message, now, corpus) {
688
+ const blocked = quotaBlock(message, now, corpus);
689
+ if (blocked !== void 0) {
690
+ return {
691
+ code: "quota-exhausted",
692
+ ...blocked.until === void 0 ? {} : { until: blocked.until }
693
+ };
664
694
  }
665
695
  if (isAuthFailure(message)) {
666
- return { code: "unauthorized", retryable: false };
696
+ return { code: "unauthorized" };
667
697
  }
668
- return { code: "backend-error", retryable: false };
698
+ return { code: "backend-error" };
669
699
  }
670
- function parseCodexOutput(output) {
700
+ function parseCodexOutput(output, now = Date.now(), corpus) {
671
701
  const messages = [];
672
702
  for (const line of output.split(/\r?\n/u)) {
673
703
  if (line.trim() === "") continue;
@@ -689,7 +719,7 @@ function parseCodexOutput(output) {
689
719
  }
690
720
  if (type === "error" || type === "turn.failed") {
691
721
  const detail = failureMessage(event);
692
- const classified = classifyFailure(detail);
722
+ const classified = classifyFailure(detail, now, corpus);
693
723
  return {
694
724
  ok: false,
695
725
  ...classified,
@@ -703,8 +733,7 @@ function parseCodexOutput(output) {
703
733
  return {
704
734
  ok: false,
705
735
  code: "backend-error",
706
- message: "the codex CLI ended without a terminal event",
707
- retryable: false
736
+ message: "the codex CLI ended without a terminal event"
708
737
  };
709
738
  }
710
739
  var CodexCliBackend = class {
@@ -953,7 +982,6 @@ var OpenAiHttpBackend = class {
953
982
  ok: false,
954
983
  code: "backend-error",
955
984
  message: "no time limit was set for this call",
956
- retryable: false,
957
985
  durationMs: 0
958
986
  };
959
987
  }
@@ -978,7 +1006,6 @@ var OpenAiHttpBackend = class {
978
1006
  return this.#fail(
979
1007
  "unauthorized",
980
1008
  "the model server rejected our credentials",
981
- false,
982
1009
  started
983
1010
  );
984
1011
  }
@@ -986,7 +1013,6 @@ var OpenAiHttpBackend = class {
986
1013
  return this.#fail(
987
1014
  "model-not-found",
988
1015
  `the model server does not know "${request.model}"`,
989
- false,
990
1016
  started
991
1017
  );
992
1018
  }
@@ -994,7 +1020,6 @@ var OpenAiHttpBackend = class {
994
1020
  return this.#fail(
995
1021
  "backend-error",
996
1022
  `the model server returned HTTP ${String(response.status)}`,
997
- response.status >= 500,
998
1023
  started
999
1024
  );
1000
1025
  }
@@ -1003,7 +1028,6 @@ var OpenAiHttpBackend = class {
1003
1028
  return this.#fail(
1004
1029
  "output-too-large",
1005
1030
  `the model produced more than ${String(request.maxOutputBytes)} bytes`,
1006
- false,
1007
1031
  started
1008
1032
  );
1009
1033
  }
@@ -1012,37 +1036,43 @@ var OpenAiHttpBackend = class {
1012
1036
  return this.#fail(
1013
1037
  "backend-error",
1014
1038
  "the model server's response was not in OpenAI chat-completion shape",
1015
- false,
1016
1039
  started
1017
1040
  );
1018
1041
  }
1019
1042
  return { ok: true, text: content, durationMs: Date.now() - started };
1020
1043
  } catch (error) {
1021
1044
  if (request.signal.aborted) {
1022
- return this.#fail("canceled", "the job was canceled", false, started);
1045
+ return this.#fail("canceled", "the job was canceled", started);
1023
1046
  }
1024
1047
  if (isAbort(error)) {
1025
1048
  return this.#fail(
1026
1049
  "timeout",
1027
1050
  `the model did not answer within ${String(request.timeoutMs)}ms`,
1028
- true,
1029
1051
  started
1030
1052
  );
1031
1053
  }
1032
1054
  return this.#fail(
1033
1055
  "backend-unreachable",
1034
1056
  describeFetchError(error, this.#baseUrl.origin),
1035
- true,
1036
1057
  started
1037
1058
  );
1038
1059
  }
1039
1060
  }
1040
- #fail(code, message, retryable, started) {
1061
+ /*
1062
+ * No `retryable` argument any more — ruled 2026-09-04.
1063
+ *
1064
+ * This adapter used to decide it per failure, and it was the only one whose
1065
+ * rule was defensible: `true` for unreachable and for a 5xx. That is
1066
+ * exactly why removing it matters. The decision now lives once, in the
1067
+ * site-facing class table, and it was the *disagreement* between three
1068
+ * adapters that let a value only quota produced become readable as a fact
1069
+ * about somebody's account.
1070
+ */
1071
+ #fail(code, message, started) {
1041
1072
  return {
1042
1073
  ok: false,
1043
1074
  code,
1044
1075
  message,
1045
- retryable,
1046
1076
  durationMs: Date.now() - started
1047
1077
  };
1048
1078
  }
@@ -2435,7 +2465,20 @@ var REVOKED_RETURN = "the installed service returns by itself once you do \u2014
2435
2465
  function revokedRemedy(origin, returnsByItself = true) {
2436
2466
  return ` re-pair: byollm connect${origin === void 0 ? "" : ` ${origin}`}
2437
2467
  ` + (returnsByItself ? ` ${REVOKED_RETURN}.
2438
- ` : "");
2468
+ ` : (
2469
+ /* The restart step, on the one branch that needs one — rider closed
2470
+ 2026-09-04.
2471
+
2472
+ The original rider asked for `byollm install` back in the remedy.
2473
+ It is not the answer: a supervised daemon polls its mark and
2474
+ promotes itself, and a reinstall was work nobody needed to do. But
2475
+ the branch where nothing self-returns was left with a re-pair step
2476
+ and no next step at all — somebody running the daemon in their own
2477
+ terminal, whose process has already exited by the time they read
2478
+ this. Their restart is the command they typed, not an install. */
2479
+ ` then: byollm run
2480
+ `
2481
+ ));
2439
2482
  }
2440
2483
  async function revokedMark(healthPath) {
2441
2484
  return (await readHealth(healthPath))?.revoked;
@@ -3460,33 +3503,44 @@ var SERVICE_UNAVAILABLE = "a device service is unavailable \u2014 its owner has
3460
3503
  var FOR_SITE = Object.freeze({
3461
3504
  "backend-unreachable": {
3462
3505
  code: "service_unavailable",
3463
- message: SERVICE_UNAVAILABLE
3506
+ message: SERVICE_UNAVAILABLE,
3507
+ retryable: true
3464
3508
  },
3465
3509
  "backend-error": {
3466
3510
  code: "service_unavailable",
3467
- message: SERVICE_UNAVAILABLE
3511
+ message: SERVICE_UNAVAILABLE,
3512
+ retryable: true
3468
3513
  },
3469
3514
  "model-not-found": {
3470
3515
  code: "service_unavailable",
3471
- message: SERVICE_UNAVAILABLE
3516
+ message: SERVICE_UNAVAILABLE,
3517
+ retryable: true
3472
3518
  },
3473
3519
  "quota-exhausted": {
3474
3520
  code: "service_unavailable",
3475
- message: SERVICE_UNAVAILABLE
3521
+ message: SERVICE_UNAVAILABLE,
3522
+ retryable: true
3476
3523
  },
3477
3524
  unauthorized: {
3478
3525
  code: "service_unavailable",
3479
- message: SERVICE_UNAVAILABLE
3526
+ message: SERVICE_UNAVAILABLE,
3527
+ retryable: true
3480
3528
  },
3481
3529
  timeout: {
3482
3530
  code: "timeout",
3483
- message: "the device did not answer in time"
3531
+ message: "the device did not answer in time",
3532
+ retryable: true
3484
3533
  },
3485
3534
  "output-too-large": {
3486
3535
  code: "output-too-large",
3487
- message: "the answer was too large to return"
3536
+ message: "the answer was too large to return",
3537
+ retryable: false
3488
3538
  },
3489
- canceled: { code: "canceled", message: "the job was canceled" }
3539
+ canceled: {
3540
+ code: "canceled",
3541
+ message: "the job was canceled",
3542
+ retryable: false
3543
+ }
3490
3544
  });
3491
3545
  function outcomeForSite(code) {
3492
3546
  return FOR_SITE[code];
@@ -3601,6 +3655,18 @@ var Runner = class {
3601
3655
  * separately.
3602
3656
  */
3603
3657
  #blocked = /* @__PURE__ */ new Map();
3658
+ /**
3659
+ * The blocked services' own reports, kept so a rebuild can restore them.
3660
+ *
3661
+ * `serviceStates` is rebuilt from scratch on every detection pass and a
3662
+ * blocked service never reaches the probe that would fill its entry, so
3663
+ * without this the state survives one pass and disappears — and `byollm
3664
+ * status`, which is a different process reading the file, could never say a
3665
+ * service was blocked.
3666
+ */
3667
+ #blockedReport = /* @__PURE__ */ new Map();
3668
+ /** What was last handed to the writer, so an unchanged pass writes nothing. */
3669
+ #lastWritten;
3604
3670
  /**
3605
3671
  * The key grants are checked against — from the pairing, or adopted later.
3606
3672
  *
@@ -3716,8 +3782,14 @@ var Runner = class {
3716
3782
  if (this.#unauthenticated.has(route.service)) continue;
3717
3783
  const blockedUntil = this.#blocked.get(route.service);
3718
3784
  if (this.#blocked.has(route.service)) {
3719
- if (blockedUntil !== void 0 && this.#now() < blockedUntil) continue;
3785
+ if (blockedUntil !== void 0 && this.#now() < blockedUntil) {
3786
+ const kept = this.#blockedReport.get(route.service);
3787
+ if (kept !== void 0) this.serviceStates.set(route.service, kept);
3788
+ continue;
3789
+ }
3720
3790
  this.#blocked.delete(route.service);
3791
+ this.#blockedReport.delete(route.service);
3792
+ this.serviceStates.delete(route.service);
3721
3793
  }
3722
3794
  let usable = probed.get(route.service);
3723
3795
  if (usable === void 0) {
@@ -3780,6 +3852,7 @@ var Runner = class {
3780
3852
  });
3781
3853
  }
3782
3854
  this.#capabilities = capabilities;
3855
+ this.#writeServiceStates();
3783
3856
  return capabilities;
3784
3857
  }
3785
3858
  #backendFor(route) {
@@ -4115,6 +4188,9 @@ var Runner = class {
4115
4188
  outcome: "error",
4116
4189
  code: "no-capability",
4117
4190
  message: "this device has no route for that job kind",
4191
+ /* Not retryable, and not from the class table: this is the runner's
4192
+ own refusal rather than a backend failure. A device with no route
4193
+ for a kind will not grow one by being asked again. */
4118
4194
  retryable: false
4119
4195
  };
4120
4196
  }
@@ -4159,6 +4235,12 @@ var Runner = class {
4159
4235
  });
4160
4236
  if (!result.ok && result.code === "unauthorized") {
4161
4237
  this.#unauthenticated.add(route.service);
4238
+ this.#noteServiceState(route.service, {
4239
+ state: {
4240
+ kind: "signed-out",
4241
+ detail: result.message
4242
+ }
4243
+ });
4162
4244
  this.#options.onEvent?.({
4163
4245
  type: "service-not-signed-in",
4164
4246
  service: route.service,
@@ -4167,7 +4249,7 @@ var Runner = class {
4167
4249
  }
4168
4250
  if (!result.ok && result.code === "quota-exhausted") {
4169
4251
  this.#blocked.set(route.service, result.until);
4170
- this.serviceStates.set(route.service, {
4252
+ const report2 = this.#noteServiceState(route.service, {
4171
4253
  ...this.serviceStates.get(route.service),
4172
4254
  state: {
4173
4255
  kind: "blocked",
@@ -4175,6 +4257,7 @@ var Runner = class {
4175
4257
  ...result.until === void 0 ? {} : { until: result.until }
4176
4258
  }
4177
4259
  });
4260
+ this.#blockedReport.set(route.service, report2);
4178
4261
  this.#options.onEvent?.({
4179
4262
  type: "service-out-of-quota",
4180
4263
  service: route.service,
@@ -4200,13 +4283,15 @@ var Runner = class {
4200
4283
  * named its backend, so every failure leaked what every success
4201
4284
  * is careful to hide.
4202
4285
  *
4203
- * `retryable` still travels: whether to try again is the site's
4204
- * decision and says nothing about whose machine it was.
4286
+ * `retryable` comes from the class, not from this result —
4287
+ * ruled 2026-09-04. Taking it from the backend let one path
4288
+ * inside `service_unavailable` report `true` and made the pair
4289
+ * readable as "rate-limited". A flag a site can read is a channel
4290
+ * whether or not anybody meant it as one.
4205
4291
  */
4206
4292
  {
4207
4293
  outcome: "error",
4208
- ...outcomeForSite(result.code),
4209
- retryable: result.retryable
4294
+ ...outcomeForSite(result.code)
4210
4295
  }
4211
4296
  );
4212
4297
  await this.#options.ingress.recordOutcome({
@@ -4282,6 +4367,34 @@ var Runner = class {
4282
4367
  await sleep(heartbeatMs * (0.85 + Math.random() * 0.3), signal);
4283
4368
  }
4284
4369
  }
4370
+ /**
4371
+ * Record a service's state, and tell whoever writes the file — S-2.
4372
+ *
4373
+ * One door, because the bug was two doors and only one of them wrote. The
4374
+ * quota path wrote on block and not on lift; the sign-out path never wrote
4375
+ * at all. Both are changes `byollm status` would report differently, and
4376
+ * status is a different process whose only source is that file.
4377
+ */
4378
+ #noteServiceState(service, report2) {
4379
+ this.serviceStates.set(service, report2);
4380
+ this.#writeServiceStates();
4381
+ return report2;
4382
+ }
4383
+ /**
4384
+ * Hand the current map to whoever persists it.
4385
+ *
4386
+ * Not awaited, and the rejection is swallowed on purpose: a job must not
4387
+ * fail — nor the runner die on an unhandled rejection — because a status
4388
+ * note could not be written. The note is evidence about the job, not part
4389
+ * of it.
4390
+ */
4391
+ #writeServiceStates() {
4392
+ const now = JSON.stringify([...this.serviceStates]);
4393
+ if (now === this.#lastWritten) return;
4394
+ this.#lastWritten = now;
4395
+ const noted = this.#options.onServiceStates?.(this.serviceStates);
4396
+ if (noted !== void 0) void noted.catch(() => void 0);
4397
+ }
4285
4398
  /**
4286
4399
  * Note a refusal that means the relationship is over.
4287
4400
  *
@@ -5850,7 +5963,7 @@ Connecting to ${origin}
5850
5963
  `
5851
5964
  Your steps:
5852
5965
  1) Open: ${info.verificationUrl}
5853
- 2) Enter code: ${info.userCode} (expires in ${String(minutes)} minutes)
5966
+ 2) Enter code: ${emphasise(info.userCode, terminalContext())} (expires in ${String(minutes)} minutes)
5854
5967
  3) Check the screen shows this device's fingerprint, then approve:
5855
5968
 
5856
5969
  ${fingerprint3(deviceIdentity.identity)}
@@ -6077,6 +6190,9 @@ async function runLoop(paths, origins, io, signal, supervised = !process.stdout.
6077
6190
  // write a count from one attempt, which says nothing about how the
6078
6191
  // daemon that actually runs is getting on.
6079
6192
  healthPath: paths.health,
6193
+ // Written whenever it changes, not only at start — S2. `byollm status`
6194
+ // is a different process and this file is the whole of what reaches it.
6195
+ onServiceStates: (states) => writeServiceStates(paths.serviceStates, states),
6080
6196
  identity: {
6081
6197
  keys: () => identity.load(Date.now()),
6082
6198
  // What was on disk. The runner replaces it from each heartbeat and
@@ -6913,7 +7029,7 @@ async function exists(path) {
6913
7029
  }
6914
7030
 
6915
7031
  // src/index.ts
6916
- var DAEMON_VERSION = "0.1.0-alpha.74";
7032
+ var DAEMON_VERSION = "0.1.0-alpha.76";
6917
7033
  function daemonVersion() {
6918
7034
  return {
6919
7035
  daemon: DAEMON_VERSION,
@@ -6965,4 +7081,4 @@ export {
6965
7081
  runCli,
6966
7082
  main
6967
7083
  };
6968
- //# sourceMappingURL=chunk-YFCNYJLR.js.map
7084
+ //# sourceMappingURL=chunk-MSRBBLRQ.js.map