@ouro.bot/cli 0.1.0-alpha.856 → 0.1.0-alpha.857

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.json CHANGED
@@ -1,6 +1,13 @@
1
1
  {
2
2
  "_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
3
3
  "versions": [
4
+ {
5
+ "version": "0.1.0-alpha.857",
6
+ "changes": [
7
+ "The daemon now caches external-event records by file stat so the one-second reconcile tick no longer re-reads and re-parses every unchanged record, cutting idle CPU.",
8
+ "The record cache never keeps a filesystem read error and re-reads any entry older than a minute, so a transient read failure or a missed change cannot hide a record."
9
+ ]
10
+ },
4
11
  {
5
12
  "version": "0.1.0-alpha.856",
6
13
  "changes": [
@@ -1,5 +1,5 @@
1
1
  {
2
- "runtimeVersion": "0.1.0-alpha.856",
2
+ "runtimeVersion": "0.1.0-alpha.857",
3
3
  "bundleSchemaVersion": 3,
4
4
  "lastUpdated": "2026-09-03T00:00:00.000Z"
5
5
  }
@@ -1,7 +1,7 @@
1
1
  <?xml version="1.0"?>
2
2
  <Container version="2">
3
3
  <Name>ouro-butler</Name>
4
- <Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.856</Repository>
4
+ <Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.857</Repository>
5
5
  <Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
6
6
  <Network>host</Network>
7
7
  <Shell>sh</Shell>
@@ -637,6 +637,7 @@ class OuroDaemon {
637
637
  externalEventRoot;
638
638
  privilegedEventSpoolRoot;
639
639
  privilegedEventScanner;
640
+ externalEventScanCache = new router_1.ExternalEventScanCache();
640
641
  rsvpHabitRunner;
641
642
  nativeHabitRunner;
642
643
  nativeHabitMatch;
@@ -1701,12 +1702,11 @@ class OuroDaemon {
1701
1702
  if (fs.existsSync(this.privilegedEventSpoolRoot)) {
1702
1703
  this.privilegedEventScanner({ spoolRoot: this.privilegedEventSpoolRoot, eventRoot: this.externalEventRootPath() });
1703
1704
  }
1704
- const statuses = (0, router_1.listExternalEventStatus)(this.externalEventRootPath());
1705
+ const scanned = this.externalEventScanCache.scan(this.externalEventRootPath());
1705
1706
  const readinessKeys = new Set();
1706
- for (const status of statuses) {
1707
- if (status.corrupt)
1707
+ for (const { record: candidate } of scanned) {
1708
+ if (!candidate)
1708
1709
  continue;
1709
- const candidate = (0, router_1.readExternalEventRecord)(status.recordPath);
1710
1710
  if (candidate.dispatchEnabled === false)
1711
1711
  continue;
1712
1712
  const retryDue = candidate.executionState === "retry_wait" && candidate.nextAttemptAt !== null && Date.parse(candidate.nextAttemptAt) <= Date.parse(now);
@@ -1739,10 +1739,10 @@ class OuroDaemon {
1739
1739
  const providerEvidenceByAgent = new Map();
1740
1740
  const runtimeEvidenceByAgent = new Map();
1741
1741
  const recoveredByClass = new Map();
1742
- for (const status of statuses) {
1743
- if (status.corrupt)
1742
+ for (const { record: scannedRecord } of scanned) {
1743
+ if (!scannedRecord)
1744
1744
  continue;
1745
- let record = (0, router_1.readExternalEventRecord)(status.recordPath);
1745
+ let record = scannedRecord;
1746
1746
  if (record.dispatchEnabled === false)
1747
1747
  continue;
1748
1748
  if (blockedReadinessKeys.has(`${record.agent}\0${record.source}`))
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.ExternalEventScanCache = void 0;
36
37
  exports.getExternalEventRoot = getExternalEventRoot;
37
38
  exports.externalEventRecordPath = externalEventRecordPath;
38
39
  exports.readExternalEventRecord = readExternalEventRecord;
@@ -622,6 +623,78 @@ function listExternalEventStatus(root) {
622
623
  }
623
624
  return rows.sort((left, right) => left.agent.localeCompare(right.agent) || left.source.localeCompare(right.source) || left.eventId.localeCompare(right.eventId));
624
625
  }
626
+ /**
627
+ * Stat-keyed record cache for the daemon's reconcile tick. Records are written by atomic rename, so any write
628
+ * changes inode, mtime or ctime and invalidates the entry. Cached records are only used to decide what is due;
629
+ * every mutation (claim, revive, reconcile) re-reads the file under its own lock and compare-and-swap check, so a
630
+ * stale cached record can at worst cause one rejected attempt that the next tick retries from fresh data.
631
+ * As a backstop against a change the stat key misses, an entry older than `maxAgeMs` is re-read anyway, and a
632
+ * filesystem read error (one carrying an fs `code`, such as EIO or EMFILE) is reported for this scan but never
633
+ * cached, so a transient failure cannot hide a healthy record until its file next changes.
634
+ */
635
+ class ExternalEventScanCache {
636
+ entries = new Map();
637
+ maxAgeMs;
638
+ now;
639
+ constructor(options = {}) {
640
+ this.maxAgeMs = options.maxAgeMs ?? 60_000;
641
+ this.now = options.now ?? Date.now;
642
+ }
643
+ scan(root) {
644
+ const seen = new Set();
645
+ const rows = [];
646
+ const nowMs = this.now();
647
+ if (fs.existsSync(root)) {
648
+ for (const agent of fs.readdirSync(root, { withFileTypes: true })) {
649
+ if (!agent.isDirectory() || agent.name.startsWith("."))
650
+ continue;
651
+ const agentPath = path.join(root, agent.name);
652
+ for (const source of fs.readdirSync(agentPath, { withFileTypes: true })) {
653
+ if (!source.isDirectory() || source.name.startsWith("."))
654
+ continue;
655
+ const sourcePath = path.join(agentPath, source.name);
656
+ for (const file of fs.readdirSync(sourcePath, { withFileTypes: true })) {
657
+ if (!file.isFile() || file.name.startsWith(".") || !file.name.endsWith(".json"))
658
+ continue;
659
+ const recordPath = path.join(sourcePath, file.name);
660
+ let stat;
661
+ try {
662
+ stat = fs.statSync(recordPath);
663
+ }
664
+ catch {
665
+ continue;
666
+ }
667
+ seen.add(recordPath);
668
+ const cached = this.entries.get(recordPath);
669
+ if (cached && cached.ino === stat.ino && cached.size === stat.size && cached.mtimeMs === stat.mtimeMs && cached.ctimeMs === stat.ctimeMs && nowMs - cached.cachedAtMs < this.maxAgeMs) {
670
+ rows.push(cached.entry);
671
+ continue;
672
+ }
673
+ let entry;
674
+ let cacheable = true;
675
+ try {
676
+ entry = { recordPath, record: readExternalEventRecord(recordPath), error: null };
677
+ }
678
+ catch (error) {
679
+ entry = { recordPath, record: null, error: error instanceof Error ? error.message : /* v8 ignore next -- filesystem and JSON parsers throw Error objects @preserve */ String(error) };
680
+ cacheable = !(error && typeof error === "object" && typeof error.code === "string");
681
+ }
682
+ if (cacheable)
683
+ this.entries.set(recordPath, { ino: stat.ino, size: stat.size, mtimeMs: stat.mtimeMs, ctimeMs: stat.ctimeMs, cachedAtMs: nowMs, entry });
684
+ else
685
+ this.entries.delete(recordPath);
686
+ rows.push(entry);
687
+ }
688
+ }
689
+ }
690
+ }
691
+ for (const key of this.entries.keys())
692
+ if (!seen.has(key))
693
+ this.entries.delete(key);
694
+ return rows;
695
+ }
696
+ }
697
+ exports.ExternalEventScanCache = ExternalEventScanCache;
625
698
  function readExisting(recordPath) {
626
699
  if (!fs.existsSync(recordPath))
627
700
  return { record: null, corruptDuplicateCount: 0 };
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.856",
3
+ "version": "0.1.0-alpha.857",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@ouro.bot/cli",
9
- "version": "0.1.0-alpha.856",
9
+ "version": "0.1.0-alpha.857",
10
10
  "dependencies": {
11
11
  "@anthropic-ai/sdk": "^0.78.0",
12
12
  "@azure/identity": "^4.13.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.856",
3
+ "version": "0.1.0-alpha.857",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },