@bridge4dev/runner 0.55.0 → 0.55.1

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/index.js CHANGED
@@ -1443,10 +1443,10 @@ async function cmdDoctor(args) {
1443
1443
  print(` ${devbridgeSliceOverridePath()}`);
1444
1444
  if (cage.mode === 'scope') {
1445
1445
  const mb = (bytes) => bytes === null ? 'infinity' : `${Math.round(bytes / 1024 / 1024)} MB`;
1446
- print(` per session MemoryHigh ${mb(cage.memoryHighBytes)} (brake: slows, never kills), MemoryMax ${mb(cage.memoryMaxBytes)} (wall), MemorySwapMax ${mb(cage.swapMaxBytes)}, TasksMax ${SESSION_TASKS_MAX}`);
1447
- print(` at the wall ${cage.oomContinue
1448
- ? 'OOMPolicy=continue — the kernel kills the hungriest process, the session stays up'
1449
- : 'this systemd does not take OOMPolicy on a scope (it is newer than the rest of the cage), so what happens at the wall is its own default — on systemd 253+ that is «stop the whole session»'}`);
1446
+ print(` per session share ${mb(cage.memoryHighBytes)} (MemoryHigh — allocations are throttled above it), ceiling ${mb(cage.memoryMaxBytes)} (MemoryMax), swap ${mb(cage.swapMaxBytes)} (MemorySwapMax), TasksMax ${SESSION_TASKS_MAX}`);
1447
+ print(` at the ceiling ${cage.oomContinue
1448
+ ? 'OOMPolicy=continue — the kernel stops the largest process in the session'
1449
+ : 'this systemd does not take OOMPolicy on a scope, so its own default applies — on systemd 253+ that stops the whole session'}`);
1450
1450
  // What systemd has IN FORCE on the slice, beside the paths of the files
1451
1451
  // that were supposed to put it there. «В файле 7680M, действует 6.0G» is the
1452
1452
  // lesson of §5.5, and the drop-in of a slice can fail to land in exactly the
@@ -1457,7 +1457,7 @@ async function cmdDoctor(args) {
1457
1457
  print(' NOT CAPPED — the drop-in above has not been applied.');
1458
1458
  print(` Apply with: ${systemctlHint('daemon-reload')}`);
1459
1459
  }
1460
- print(` service ceiling ${mb(cage.serviceMemoryMaxBytes)} (the per-session brake is half of the containing ceiling, capped at 2.5 GiB; the wall IS the containing ceiling)`);
1460
+ print(` service ceiling ${mb(cage.serviceMemoryMaxBytes)} (a session's share is a third of the containing ceiling, its own ceiling is that minus one share)`);
1461
1461
  print(` expand-env flag ${cage.expandEnvironmentFlag ? 'passed (systemd ≥ 254)' : 'not passed — this systemd does not know it, and --scope does not expand anyway'}`);
1462
1462
  }
1463
1463
  else {
@@ -433,6 +433,12 @@ export declare function parseScopeMemoryStatus(files: {
433
433
  */
434
434
  export declare function readScopeMemoryStatus(unit: string, readFile?: (p: string) => string): ScopeMemoryStatus | null;
435
435
  export declare function markScopeOomKillsSeen(id: string, oomKills: number): void;
436
+ /**
437
+ * «a process in it was stopped» / «3 processes in it were stopped», so the verb
438
+ * agrees. Shared with the supervisor's feed notices, which count the same
439
+ * thing and must not word it differently.
440
+ */
441
+ export declare function stoppedProcesses(count: number, where?: 'it' | 'this one'): string;
436
442
  /**
437
443
  * Snapshot the cgroup's verdict before systemd can take it away.
438
444
  *
@@ -891,6 +891,16 @@ const oomKillsSeen = new Map();
891
891
  export function markScopeOomKillsSeen(id, oomKills) {
892
892
  oomKillsSeen.set(id, oomKills);
893
893
  }
894
+ /**
895
+ * «a process in it was stopped» / «3 processes in it were stopped», so the verb
896
+ * agrees. Shared with the supervisor's feed notices, which count the same
897
+ * thing and must not word it differently.
898
+ */
899
+ export function stoppedProcesses(count, where = 'it') {
900
+ return count === 1
901
+ ? `a process in ${where} was stopped`
902
+ : `${count} processes in ${where} were stopped`;
903
+ }
894
904
  const deaths = new Map();
895
905
  /**
896
906
  * Snapshot the cgroup's verdict before systemd can take it away.
@@ -909,7 +919,7 @@ export function rememberDeath(id, unit, readStatus = readScopeMemoryStatus) {
909
919
  maxBytes: status.maxBytes,
910
920
  // `oom` moves only where the limit that was hit belongs. A kill with our
911
921
  // own counter still at zero came from an ancestor — the pool.
912
- ownWall: status.ownLimitOom > 0,
922
+ reached: status.ownLimitOom > 0 ? 'own-ceiling' : 'shared-pool',
913
923
  });
914
924
  }
915
925
  /**
@@ -928,9 +938,9 @@ function rememberDeathFromResult(id) {
928
938
  deaths.set(id, {
929
939
  oomKills: 1,
930
940
  maxBytes: sessionCage().memoryMaxBytes,
931
- // systemd stopped the unit for an OOM inside it; which limit was hit is not
932
- // in `Result`, and «your own wall» is the claim that must not be guessed.
933
- ownWall: false,
941
+ // systemd stopped the unit for an OOM inside it; which limit was reached is
942
+ // not in `Result`, and neither claim may be guessed.
943
+ reached: 'unknown',
934
944
  });
935
945
  }
936
946
  /**
@@ -947,16 +957,21 @@ export function explainMemoryDeath(id) {
947
957
  if (!death)
948
958
  return null;
949
959
  deaths.delete(id);
950
- const killed = death.oomKills === 1 ? 'a process' : `${death.oomKills} processes`;
951
- const wall = death.maxBytes === null ? '' : ` of ${Math.round(death.maxBytes / MIB)} MB`;
952
- if (death.ownWall) {
953
- return (`The session ran out of memory: it went over its own memory ceiling${wall} and the kernel killed ` +
954
- `${killed} inside it. ` +
955
- 'Resume it and avoid re-running the command that was in flight, or give the machine more memory or swap.');
960
+ const stopped = stoppedProcesses(death.oomKills);
961
+ const ceiling = death.maxBytes === null ? '' : ` of ${Math.round(death.maxBytes / MIB)} MB`;
962
+ if (death.reached === 'own-ceiling') {
963
+ return (`This session reached its memory ceiling${ceiling} and ${stopped} — ` +
964
+ 'send a message to carry on; if it keeps happening, the machine needs more memory.');
965
+ }
966
+ if (death.reached === 'shared-pool') {
967
+ return (`The machine ran out of memory for agent sessions and ${stoppedProcesses(death.oomKills, 'this one')} — ` +
968
+ 'send a message to carry on; if it keeps happening, run fewer sessions at once or give the machine more memory.');
956
969
  }
957
- return (`The machine ran out of memory for agent sessions — all sessions on it share one ceiling — and the kernel killed ` +
958
- `${killed} in this one. This session was not necessarily the greedy one. ` +
959
- 'Resume it; if it keeps happening, run fewer sessions at once or give the machine more memory or swap.');
970
+ // Which ceiling was reached is unknowable here, so the sentence names no
971
+ // culprit: it has to be true whether this session was the greedy one or a
972
+ // bystander.
973
+ return (`There was not enough memory for this session and ${stopped} — ` +
974
+ 'send a message to carry on; if it keeps happening, run fewer sessions at once or give the machine more memory.');
960
975
  }
961
976
  const realSystemctl = async (args) => {
962
977
  const { stdout, stderr } = await execFileAsync('systemctl', ['--user', ...args], {
@@ -26,7 +26,7 @@ import { agentByDbValue } from './agent-registry.js';
26
26
  import { invalidateAgentVersions, measureAgentVersions, } from './agent-versions.js';
27
27
  import { rememberWorkspacePath } from './environment.js';
28
28
  import { hostLoadChangedEnough, hostLoadHeartbeatDue, readHostLoad, HOST_LOAD_HEARTBEAT_MS, HOST_LOAD_SAMPLE_INTERVAL_MS, } from './host-load.js';
29
- import { markScopeOomKillsSeen, readScopeMemoryStatus, sessionScopeUnitOf, } from './session-cage.js';
29
+ import { markScopeOomKillsSeen, readScopeMemoryStatus, sessionScopeUnitOf, stoppedProcesses, } from './session-cage.js';
30
30
  import { composeMessageWithAttachments, saveAttachments, } from './attachments.js';
31
31
  import { applyRewind, createCheckpoint, dropCheckpoints, listCheckpoints, MAX_BUSY_SESSIONS, previewRewind, pruneCheckpoints, } from './checkpoints.js';
32
32
  import { DeliverMessageArgsSchema, QuestionAnswerArgsSchema } from './protocol.js';
@@ -715,18 +715,15 @@ export class Supervisor {
715
715
  // where it was came from the slice over ALL sessions, and telling this
716
716
  // session it went over its ceiling would be false (measured on this
717
717
  // host: the killed neighbour read `max 0, oom 0, oom_kill 1`).
718
- const ownWall = status.ownLimitOom > base.ownOom;
719
- const killed = status.oomKills - base.oomKills === 1
720
- ? 'a process'
721
- : `${status.oomKills - base.oomKills} processes`;
718
+ const ownCeiling = status.ownLimitOom > base.ownOom;
719
+ const stopped = stoppedProcesses(status.oomKills - base.oomKills);
722
720
  this.sendEvent(running, 'notice', {
723
721
  level: 'warn',
724
- text: ownWall
725
- ? `This session went over its memory ceiling${status.maxBytes === null ? '' : ` of ${mb(status.maxBytes)}`} and the kernel killed ${killed} inside it. ` +
726
- 'The session itself is still running; the command that was in flight most likely died with it — check its output before going on.'
727
- : `The machine ran out of memory for agent sessions — they share one ceiling — and the kernel killed ${killed} in this session. ` +
728
- `This session was using ${mb(status.currentBytes)}${status.maxBytes === null ? '' : ` of its own ${mb(status.maxBytes)}`}, so it was not necessarily the greedy one. ` +
729
- 'It is still running; the command that was in flight most likely died with it.',
722
+ text: ownCeiling
723
+ ? `This session reached its${status.maxBytes === null ? '' : ` ${mb(status.maxBytes)}`} memory ceiling and ${stopped} — ` +
724
+ 'check the output of the command that was running.'
725
+ : `The machine ran out of memory for agent sessions and ${stoppedProcesses(status.oomKills - base.oomKills, 'this one')} — ` +
726
+ 'run fewer sessions at once, or give the machine more memory.',
730
727
  });
731
728
  // So a death minutes later is not blamed on a kill the feed already carries.
732
729
  markScopeOomKillsSeen(running.descriptor.id, status.oomKills);
@@ -744,6 +741,10 @@ export class Supervisor {
744
741
  */
745
742
  const throttled = status.highEvents > base.highEvents ||
746
743
  (status.highBytes !== null && status.currentBytes > status.highBytes);
744
+ // `memory.high` reads `max` on a machine where the share never applied —
745
+ // then there is no number to name, and «its 0 MB share» is worse than no
746
+ // number at all.
747
+ const share = status.highBytes === null ? '' : ` ${mb(status.highBytes)}`;
747
748
  let { brakedSince, warnedLong, calmTicks } = base;
748
749
  if (throttled) {
749
750
  calmTicks = 0;
@@ -752,18 +753,15 @@ export class Supervisor {
752
753
  warnedLong = false;
753
754
  this.sendEvent(running, 'notice', {
754
755
  level: 'warn',
755
- text: `This session is over its memory share (${mb(status.highBytes ?? 0)}, using ${mb(status.currentBytes)}), so the kernel is slowing it down instead of killing it. ` +
756
- 'Heavy commands will finish slower until memory is freed.',
756
+ text: `This session has hit its${share} memory share — commands run slower until it uses less memory.`,
757
757
  });
758
758
  }
759
759
  else if (!warnedLong && now - brakedSince >= longMs) {
760
760
  warnedLong = true;
761
761
  this.sendEvent(running, 'notice', {
762
762
  level: 'warn',
763
- text: `Still over its memory share after ${Math.round(longMs / 60_000)} min (using ${mb(status.currentBytes)} of a ${mb(status.highBytes ?? 0)} share). ` +
764
- `If the current command is stuck rather than slow, stop it or pause the session.${status.maxBytes === null
765
- ? ''
766
- : ` The hard ceiling, where a process would be killed, is ${mb(status.maxBytes)}.`}`,
763
+ text: `This session has been over its${share} memory share for ${Math.round(longMs / 60_000)} min and is now using ${mb(status.currentBytes)} — ` +
764
+ 'if a command is stuck rather than slow, stop the turn or pause the session.',
767
765
  });
768
766
  }
769
767
  }
@@ -776,7 +774,7 @@ export class Supervisor {
776
774
  warnedLong = false;
777
775
  this.sendEvent(running, 'notice', {
778
776
  level: 'info',
779
- text: `Memory is back under the session's share (${mb(status.highBytes ?? 0)}); it runs at full speed again.`,
777
+ text: `This session is back under its${share} memory share — it runs at full speed again.`,
780
778
  });
781
779
  }
782
780
  }
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const RUNNER_VERSION = "0.55.0";
1
+ export declare const RUNNER_VERSION = "0.55.1";
2
2
  //# sourceMappingURL=version.d.ts.map
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // Kept in sync with package.json by the release script (manual for now).
2
- export const RUNNER_VERSION = '0.55.0';
2
+ export const RUNNER_VERSION = '0.55.1';
3
3
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bridge4dev/runner",
3
- "version": "0.55.0",
3
+ "version": "0.55.1",
4
4
  "description": "DevBridge dev runner — connects a dev server to DevBridge and runs agent sessions (Claude Code / Codex)",
5
5
  "homepage": "https://bridge4.dev",
6
6
  "license": "MIT",