@inerrata-corporation/errata 2.0.2-dev.682 → 2.0.2-dev.688

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.
Files changed (2) hide show
  1. package/errata.mjs +143 -33
  2. package/package.json +1 -1
package/errata.mjs CHANGED
@@ -52221,23 +52221,38 @@ init_review2();
52221
52221
  import { closeSync, existsSync as existsSync12, fstatSync, openSync, readdirSync as readdirSync5, readSync, statSync as statSync3 } from "node:fs";
52222
52222
  import { basename as basename3, dirname as dirname8, join as join15 } from "node:path";
52223
52223
  import { homedir as homedir3 } from "node:os";
52224
- function readFrom(path2, fromByte) {
52224
+ function readFrom(path2, fromByte, maxBytes) {
52225
52225
  let fd;
52226
52226
  try {
52227
52227
  fd = openSync(path2, "r");
52228
52228
  } catch {
52229
- return { text: "", nextOffset: fromByte };
52229
+ return { text: "", nextOffset: fromByte, more: false };
52230
52230
  }
52231
52231
  try {
52232
52232
  const size = fstatSync(fd).size;
52233
52233
  const start2 = fromByte > 0 && fromByte <= size ? fromByte : 0;
52234
- const len = size - start2;
52235
- if (len <= 0) return { text: "", nextOffset: size };
52236
- const buf = Buffer.allocUnsafe(len);
52237
- readSync(fd, buf, 0, len, start2);
52238
- return { text: buf.toString("utf8"), nextOffset: size };
52234
+ const remaining = size - start2;
52235
+ if (remaining <= 0) return { text: "", nextOffset: size, more: false };
52236
+ let cap = maxBytes && maxBytes > 0 ? maxBytes : remaining;
52237
+ for (; ; ) {
52238
+ const len = Math.min(cap, remaining);
52239
+ const buf = Buffer.allocUnsafe(len);
52240
+ readSync(fd, buf, 0, len, start2);
52241
+ if (len >= remaining) {
52242
+ return { text: buf.toString("utf8"), nextOffset: size, more: false };
52243
+ }
52244
+ const nl = buf.lastIndexOf(10);
52245
+ if (nl >= 0) {
52246
+ return {
52247
+ text: buf.subarray(0, nl + 1).toString("utf8"),
52248
+ nextOffset: start2 + nl + 1,
52249
+ more: true
52250
+ };
52251
+ }
52252
+ cap *= 2;
52253
+ }
52239
52254
  } catch {
52240
- return { text: "", nextOffset: fromByte };
52255
+ return { text: "", nextOffset: fromByte, more: false };
52241
52256
  } finally {
52242
52257
  closeSync(fd);
52243
52258
  }
@@ -52333,9 +52348,9 @@ function isUserTurnBoundary(obj) {
52333
52348
  function readAssistantTurns(transcriptPath, includeThinking = true, maxBytes = 2e6) {
52334
52349
  return parseAssistantTurns(readTail(transcriptPath, maxBytes), includeThinking);
52335
52350
  }
52336
- function readAssistantTurnsFrom(transcriptPath, fromByte, includeThinking = true) {
52337
- const { text, nextOffset } = readFrom(transcriptPath, fromByte);
52338
- return { turns: parseAssistantTurns(text, includeThinking), nextOffset };
52351
+ function readAssistantTurnsFrom(transcriptPath, fromByte, includeThinking = true, maxBytes) {
52352
+ const { text, nextOffset, more } = readFrom(transcriptPath, fromByte, maxBytes);
52353
+ return { turns: parseAssistantTurns(text, includeThinking), nextOffset, more };
52339
52354
  }
52340
52355
  function parseAssistantTurns(raw2, includeThinking) {
52341
52356
  if (!raw2) return [];
@@ -53558,7 +53573,12 @@ function passLedgerPath(configDir) {
53558
53573
  function appendPassLedger(configDir, kind, durationMs, counts) {
53559
53574
  const path2 = passLedgerPath(configDir);
53560
53575
  try {
53561
- const entry = { ts: Date.now(), kind, durationMs, counts };
53576
+ const entry = {
53577
+ ts: Date.now(),
53578
+ kind,
53579
+ durationMs,
53580
+ counts: { ...counts, rssMB: Math.round(process.memoryUsage().rss / 1048576) }
53581
+ };
53562
53582
  appendFileSync2(path2, JSON.stringify(entry) + "\n");
53563
53583
  const n = (appendsSinceTrim.get(path2) ?? 0) + 1;
53564
53584
  if (n < TRIM_EVERY) {
@@ -53744,18 +53764,38 @@ function recordEpisode(store, workspaceId2, t, delta, causal) {
53744
53764
  return id;
53745
53765
  }
53746
53766
 
53767
+ // src/footprint.ts
53768
+ function numEnv(key, fallback) {
53769
+ const raw2 = process.env[key];
53770
+ if (raw2 == null || raw2.trim() === "") return fallback;
53771
+ const n = Number(raw2);
53772
+ return Number.isFinite(n) && n > 0 ? n : fallback;
53773
+ }
53774
+ var DAEMON_MAX_HEAP_MB = numEnv("ERRATA_MAX_HEAP_MB", 2048);
53775
+ var WORKER_MAX_HEAP_MB = numEnv("ERRATA_WORKER_MAX_HEAP_MB", 1024);
53776
+ var HARVEST_SLICE_BYTES = numEnv("ERRATA_HARVEST_SLICE_BYTES", 4e6);
53777
+ function workerResourceLimits() {
53778
+ return { maxOldGenerationSizeMb: WORKER_MAX_HEAP_MB };
53779
+ }
53780
+ function isWorkerOom(err2) {
53781
+ const e = err2;
53782
+ if (!e) return false;
53783
+ if (e.code === "ERR_WORKER_OUT_OF_MEMORY") return true;
53784
+ return /out of memory|ERR_WORKER_OUT_OF_MEMORY|heap limit/i.test(e.message ?? "");
53785
+ }
53786
+
53747
53787
  // src/episode-retention.ts
53748
53788
  var EPISODE_RETAIN_DAYS_DEFAULT = 90;
53749
53789
  var EPISODE_RETIRE_BATCH_DEFAULT = 300;
53750
53790
  var DAY_MS = 864e5;
53751
- function numEnv(key, fallback) {
53791
+ function numEnv2(key, fallback) {
53752
53792
  const raw2 = process.env[key];
53753
53793
  if (raw2 == null || raw2.trim() === "") return fallback;
53754
53794
  const n = Number(raw2);
53755
53795
  return Number.isFinite(n) && n > 0 ? n : fallback;
53756
53796
  }
53757
53797
  function retireEpisodeProvenance(store, now, opts = {}) {
53758
- const retainDays = opts.retainDays ?? numEnv("ERRATA_EPISODE_RETAIN_DAYS", EPISODE_RETAIN_DAYS_DEFAULT);
53798
+ const retainDays = opts.retainDays ?? numEnv2("ERRATA_EPISODE_RETAIN_DAYS", EPISODE_RETAIN_DAYS_DEFAULT);
53759
53799
  const batch = opts.batch ?? EPISODE_RETIRE_BATCH_DEFAULT;
53760
53800
  const cutoff = now - retainDays * DAY_MS;
53761
53801
  const due = store.findNodesByLabel("Episode").filter((ep) => {
@@ -53831,7 +53871,15 @@ var PassWorker = class {
53831
53871
  ensureWorker() {
53832
53872
  if (this.worker) return;
53833
53873
  const workerUrl = new URL("./pass-worker.mjs", import.meta.url);
53834
- const worker = new Worker(workerUrl, { workerData: this.init, execArgv: process.execArgv });
53874
+ const worker = new Worker(workerUrl, {
53875
+ workerData: this.init,
53876
+ execArgv: process.execArgv,
53877
+ // HZ-footprint-cap: hard heap ceiling per isolate. A pass that would
53878
+ // exceed it dies HERE, in the isolate built to be expendable — the
53879
+ // engine's fallback must then SKIP the pass (isWorkerOom), never re-run
53880
+ // it inline where the same allocation would take down the hook server.
53881
+ resourceLimits: workerResourceLimits()
53882
+ });
53835
53883
  this.worker = worker;
53836
53884
  this.ready = new Promise((resolve6, reject) => {
53837
53885
  const onReady = (m) => {
@@ -55407,7 +55455,7 @@ function startLoopLagMonitor(thresholdMs = 1e3) {
55407
55455
  }
55408
55456
 
55409
55457
  // src/engine.ts
55410
- var DAEMON_VERSION = true ? "2.0.2-dev.682" : "2.0.0-alpha.0";
55458
+ var DAEMON_VERSION = true ? "2.0.2-dev.688" : "2.0.0-alpha.0";
55411
55459
  var IGNORED_PATH = /[\\/](?:\.git|node_modules|\.errata|\.claude|\.codex|\.cursor|\.turbo|\.next|dist|coverage|test-results|playwright-report|__pycache__)(?:[\\/]|$)/;
55412
55460
  var IGNORED_NOISE = /castalia\.db|eventlog\.sqlite|turn-cursors/;
55413
55461
  var GIT_OP_MUTE_MS = 4e3;
@@ -55953,6 +56001,36 @@ function createWorkspaceEngine(opts) {
55953
56001
  }
55954
56002
  return { report: r.report, localSkills: r.localSkills };
55955
56003
  } catch (err2) {
56004
+ if (isWorkerOom(err2)) {
56005
+ console.warn("[errata] nightly worker hit its heap ceiling \u2014 heavy pass skipped this fire");
56006
+ appendPassLedger(paths.configDir, "graph-rescore", 0, { oomSkipped: 1 });
56007
+ const sem = runSemanticMaintenance(store);
56008
+ return {
56009
+ report: {
56010
+ scoredNodes: 0,
56011
+ iterations: 0,
56012
+ converged: true,
56013
+ landmarks: 0,
56014
+ communities: 0,
56015
+ motifsPromoted: 0,
56016
+ techniques: 0,
56017
+ antipatterns: 0,
56018
+ skillsInduced: 0,
56019
+ skillsRefreshed: 0,
56020
+ pageRankWritten: 0,
56021
+ landmarkFlips: 0,
56022
+ mode: "incremental",
56023
+ regionSize: 0,
56024
+ designResolved: sem.designResolved,
56025
+ revisitsCleared: sem.revisitsCleared,
56026
+ fixCandidatesSettled: sem.fixCandidatesSettled,
56027
+ claimsEvaluated: sem.claimsEvaluated,
56028
+ claimsDerived: sem.claimsDerived,
56029
+ durationMs: sem.durationMs
56030
+ },
56031
+ localSkills: 0
56032
+ };
56033
+ }
55956
56034
  console.warn(
55957
56035
  "[errata] nightly worker unavailable, running inline:",
55958
56036
  err2 instanceof Error ? err2.message : err2
@@ -56712,20 +56790,38 @@ function createWorkspaceEngine(opts) {
56712
56790
  turns = readAssistantTurns(transcriptPath);
56713
56791
  nextOffset = transcriptSize(transcriptPath);
56714
56792
  } else {
56715
- ({ turns, nextOffset } = readAssistantTurnsFrom(transcriptPath, known));
56793
+ ({ turns, nextOffset } = readAssistantTurnsFrom(transcriptPath, known, true, HARVEST_SLICE_BYTES));
56716
56794
  }
56717
56795
  } catch {
56718
56796
  return;
56719
56797
  }
56720
- const fresh = turnsSince(turns, lastTurnUuid.get(sessionId));
56721
- turnOffset.set(sessionId, nextOffset);
56722
- if (fresh.length === 0) {
56798
+ for (let slice = 0; ; slice++) {
56799
+ const fresh = turnsSince(turns, lastTurnUuid.get(sessionId));
56800
+ turnOffset.set(sessionId, nextOffset);
56801
+ if (fresh.length > 0) {
56802
+ lastTurnUuid.set(sessionId, turns[turns.length - 1].uuid);
56803
+ await harvestTexts(sessionId, fresh);
56804
+ }
56723
56805
  saveTurnCursors(turnCursorPath, lastTurnUuid, turnOffset);
56724
- return;
56806
+ if (slice >= 256) {
56807
+ console.warn("[errata] harvest slice guard hit \u2014 backlog resumes next tick");
56808
+ break;
56809
+ }
56810
+ let next;
56811
+ try {
56812
+ next = readAssistantTurnsFrom(transcriptPath, nextOffset, true, HARVEST_SLICE_BYTES);
56813
+ } catch {
56814
+ break;
56815
+ }
56816
+ if (next.turns.length === 0 && !next.more) {
56817
+ turnOffset.set(sessionId, next.nextOffset);
56818
+ saveTurnCursors(turnCursorPath, lastTurnUuid, turnOffset);
56819
+ break;
56820
+ }
56821
+ turns = next.turns;
56822
+ nextOffset = next.nextOffset;
56823
+ await new Promise((resolve6) => setImmediate(resolve6));
56725
56824
  }
56726
- lastTurnUuid.set(sessionId, turns[turns.length - 1].uuid);
56727
- await harvestTexts(sessionId, fresh);
56728
- saveTurnCursors(turnCursorPath, lastTurnUuid, turnOffset);
56729
56825
  };
56730
56826
  const seenMessageIds = /* @__PURE__ */ new Set();
56731
56827
  const onMessage = (e) => {
@@ -58119,7 +58215,12 @@ var ConsolidateWorker = class {
58119
58215
  ensureWorker() {
58120
58216
  if (this.worker) return;
58121
58217
  const workerUrl = new URL("./consolidate-worker.mjs", import.meta.url);
58122
- const worker = new Worker2(workerUrl, { workerData: {}, execArgv: process.execArgv });
58218
+ const worker = new Worker2(workerUrl, {
58219
+ workerData: {},
58220
+ execArgv: process.execArgv,
58221
+ // HZ-footprint-cap — see pass-worker-client.ts for the contract.
58222
+ resourceLimits: workerResourceLimits()
58223
+ });
58123
58224
  this.worker = worker;
58124
58225
  this.ready = new Promise((resolve6, reject) => {
58125
58226
  const onReady = (m) => {
@@ -60248,7 +60349,7 @@ var DEFAULT_CONSOLIDATION_POLICY = {
60248
60349
  quiescenceMs: 6e4
60249
60350
  // sim preferred 60s over 90s (fresher, esp. heavy-code)
60250
60351
  };
60251
- function numEnv2(env2, key, fallback) {
60352
+ function numEnv3(env2, key, fallback) {
60252
60353
  const raw2 = env2[key];
60253
60354
  if (raw2 == null || raw2.trim() === "") return fallback;
60254
60355
  const n = Number(raw2);
@@ -60256,12 +60357,12 @@ function numEnv2(env2, key, fallback) {
60256
60357
  }
60257
60358
  function consolidationPolicyFromEnv(env2 = process.env) {
60258
60359
  return {
60259
- baseFloorMs: numEnv2(env2, "ERRATA_CONSOLIDATE_BASE_FLOOR_MS", DEFAULT_CONSOLIDATION_POLICY.baseFloorMs),
60260
- dutyFactor: numEnv2(env2, "ERRATA_CONSOLIDATE_DUTY_FACTOR", DEFAULT_CONSOLIDATION_POLICY.dutyFactor),
60261
- momentumThreshold: numEnv2(env2, "ERRATA_CONSOLIDATE_MOMENTUM", DEFAULT_CONSOLIDATION_POLICY.momentumThreshold),
60262
- momentumRatio: numEnv2(env2, "ERRATA_CONSOLIDATE_MOMENTUM_RATIO", DEFAULT_CONSOLIDATION_POLICY.momentumRatio),
60263
- momentumRatioCap: numEnv2(env2, "ERRATA_CONSOLIDATE_MOMENTUM_RATIO_CAP", DEFAULT_CONSOLIDATION_POLICY.momentumRatioCap),
60264
- quiescenceMs: numEnv2(env2, "ERRATA_CONSOLIDATE_QUIESCENCE_MS", DEFAULT_CONSOLIDATION_POLICY.quiescenceMs)
60360
+ baseFloorMs: numEnv3(env2, "ERRATA_CONSOLIDATE_BASE_FLOOR_MS", DEFAULT_CONSOLIDATION_POLICY.baseFloorMs),
60361
+ dutyFactor: numEnv3(env2, "ERRATA_CONSOLIDATE_DUTY_FACTOR", DEFAULT_CONSOLIDATION_POLICY.dutyFactor),
60362
+ momentumThreshold: numEnv3(env2, "ERRATA_CONSOLIDATE_MOMENTUM", DEFAULT_CONSOLIDATION_POLICY.momentumThreshold),
60363
+ momentumRatio: numEnv3(env2, "ERRATA_CONSOLIDATE_MOMENTUM_RATIO", DEFAULT_CONSOLIDATION_POLICY.momentumRatio),
60364
+ momentumRatioCap: numEnv3(env2, "ERRATA_CONSOLIDATE_MOMENTUM_RATIO_CAP", DEFAULT_CONSOLIDATION_POLICY.momentumRatioCap),
60365
+ quiescenceMs: numEnv3(env2, "ERRATA_CONSOLIDATE_QUIESCENCE_MS", DEFAULT_CONSOLIDATION_POLICY.quiescenceMs)
60265
60366
  };
60266
60367
  }
60267
60368
  function consolidationGapMs(lastPassMs, policy) {
@@ -62160,7 +62261,16 @@ function spawnDaemonDetached() {
62160
62261
  detached: true,
62161
62262
  stdio: ["ignore", out2, out2],
62162
62263
  windowsHide: true,
62163
- cwd: ROOT
62264
+ cwd: ROOT,
62265
+ // HZ-footprint-cap: hard V8 old-space ceiling on the daemon process. The
62266
+ // workers carry their own resourceLimits; this bounds the main isolate
62267
+ // (hook server + capture), whose bursts are slice-capped so the ceiling is
62268
+ // a backstop, not a working limit. NODE_OPTIONS so it applies however the
62269
+ // bundle re-executes.
62270
+ env: {
62271
+ ...process.env,
62272
+ NODE_OPTIONS: `${process.env["NODE_OPTIONS"] ?? ""} --max-old-space-size=${DAEMON_MAX_HEAP_MB}`.trim()
62273
+ }
62164
62274
  }).unref();
62165
62275
  if (out2 !== "ignore") closeSync2(out2);
62166
62276
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inerrata-corporation/errata",
3
- "version": "2.0.2-dev.682",
3
+ "version": "2.0.2-dev.688",
4
4
  "description": "errata - local-first observation engine for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {