@ouro.bot/cli 0.1.0-alpha.694 → 0.1.0-alpha.695

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.695",
6
+ "changes": [
7
+ "Prevent daemon-health context-loss Sentinel checks from treating Sentinel-authored flight-recorder cleanup as bundle dirt.",
8
+ "Clear daemon shutdown fallback timers after graceful SIGINT/SIGTERM and circuit-breaker stops."
9
+ ]
10
+ },
4
11
  {
5
12
  "version": "0.1.0-alpha.694",
6
13
  "changes": [
@@ -48,6 +48,7 @@ const runtime_1 = require("../nerves/runtime");
48
48
  const context_loss_gauntlet_1 = require("./context-loss-gauntlet");
49
49
  const provider_visibility_1 = require("./provider-visibility");
50
50
  const CONTEXT_LOSS_SENTINEL_RECEIPT_FILE_LIMIT = 512;
51
+ const CONTEXT_LOSS_SENTINEL_GIT_STATUS_TIMEOUT_MS = 5_000;
51
52
  const REQUIRED_LANES = ["outward", "inner"];
52
53
  function logicalLocator(...parts) {
53
54
  return path.posix.join(...parts);
@@ -524,7 +525,7 @@ function defaultGitStatus(agentRoot) {
524
525
  cwd: agentRoot,
525
526
  encoding: "utf-8",
526
527
  stdio: ["ignore", "pipe", "pipe"],
527
- timeout: 1_000,
528
+ timeout: CONTEXT_LOSS_SENTINEL_GIT_STATUS_TIMEOUT_MS,
528
529
  });
529
530
  return { ok: true, porcelain };
530
531
  }
@@ -574,7 +575,167 @@ function isSentinelGitStatusEntry(entry) {
574
575
  return normalizedPath === sentinelRoot || normalizedPath.startsWith(`${sentinelRoot}/`);
575
576
  });
576
577
  }
577
- function bundleSignal(status, options = {}) {
578
+ function isContextLossSentinelBlockerText(value) {
579
+ return value.startsWith("context-loss Sentinel blocked:");
580
+ }
581
+ function hasContextLossSentinelCheckpointEvents(agentRoot, resume) {
582
+ const events = readFlightRecorderEventsByIds(agentRoot, resume.lastSafeCheckpoint.sourceEventIds);
583
+ const foundEventIds = new Set(events.map((event) => event.id));
584
+ if (resume.lastSafeCheckpoint.sourceEventIds.some((eventId) => !foundEventIds.has(eventId)))
585
+ return false;
586
+ return resume.blockedBecause.length > 0
587
+ ? events.every(isContextLossSentinelBlockerEvent)
588
+ : events.every(isContextLossSentinelRecoveryEvent);
589
+ }
590
+ function isContextLossSentinelCheckpoint(agentRoot, resume) {
591
+ return resume.lastSafeCheckpoint.sourceEventIds.length > 0
592
+ && resume.lastSafeCheckpoint.sourceEventIds.every((eventId) => eventId.startsWith("fr-sentinel-"))
593
+ && resume.blockedBecause.every(isContextLossSentinelBlockerText)
594
+ && hasContextLossSentinelCheckpointEvents(agentRoot, resume);
595
+ }
596
+ function neutralizeSentinelCheckpoint(resume) {
597
+ return {
598
+ ...resume,
599
+ canContinue: true,
600
+ blockedBecause: [],
601
+ lastSafeCheckpoint: {
602
+ turnId: null,
603
+ sessionRef: null,
604
+ recordedAt: null,
605
+ sourceEventIds: [],
606
+ },
607
+ recorderHealth: { status: "ok", issues: [] },
608
+ };
609
+ }
610
+ function isContextLossSentinelProducedRef(value) {
611
+ if (!value || typeof value !== "object" || Array.isArray(value))
612
+ return false;
613
+ const record = value;
614
+ if (record.kind !== "arc" || typeof record.locator !== "string")
615
+ return false;
616
+ const sentinelRoot = relativeSentinelRoot();
617
+ return record.locator === sentinelRoot || record.locator.startsWith(`${sentinelRoot}/`);
618
+ }
619
+ function isContextLossSentinelFlightRecorderEvent(value) {
620
+ if (!value || typeof value !== "object" || Array.isArray(value))
621
+ return false;
622
+ const record = value;
623
+ const meta = record.meta;
624
+ const producedRefs = record.producedRefs;
625
+ return typeof record.id === "string"
626
+ && record.id.startsWith("fr-sentinel-")
627
+ && (record.kind === "blocker_detected" || record.kind === "agent_note")
628
+ && meta?.source === "context-loss-sentinel"
629
+ && Array.isArray(producedRefs)
630
+ && producedRefs.length > 0
631
+ && producedRefs.every(isContextLossSentinelProducedRef);
632
+ }
633
+ function isContextLossSentinelBlockerEvent(event) {
634
+ return isContextLossSentinelFlightRecorderEvent(event)
635
+ && event.kind === "blocker_detected"
636
+ && Array.isArray(event.blockedBecause)
637
+ && event.blockedBecause.length > 0
638
+ && event.blockedBecause.every(isContextLossSentinelBlockerText);
639
+ }
640
+ function isContextLossSentinelRecoveryEvent(event) {
641
+ return isContextLossSentinelFlightRecorderEvent(event)
642
+ && event.kind === "agent_note"
643
+ && Array.isArray(event.blockedBecause)
644
+ && event.blockedBecause.length === 0
645
+ && event.meta?.resolution === "blocked-signals-recovered";
646
+ }
647
+ function gitShowHeadFile(agentRoot, entryPath) {
648
+ try {
649
+ return (0, child_process_1.execFileSync)("git", ["show", `HEAD:${entryPath}`], {
650
+ cwd: agentRoot,
651
+ encoding: "utf-8",
652
+ stdio: ["ignore", "pipe", "ignore"],
653
+ timeout: CONTEXT_LOSS_SENTINEL_GIT_STATUS_TIMEOUT_MS,
654
+ });
655
+ }
656
+ catch {
657
+ return null;
658
+ }
659
+ }
660
+ function nonEmptyLines(text) {
661
+ return text.split(/\r?\n/).filter((line) => line.trim().length > 0);
662
+ }
663
+ function isFlightRecorderEventsPath(entryPath) {
664
+ return /^arc\/flight-recorder\/events\/[^/]+\.jsonl$/.test(entryPath);
665
+ }
666
+ function isFlightRecorderLatestPath(entryPath) {
667
+ return entryPath === "arc/flight-recorder/latest.json";
668
+ }
669
+ function isDeletedGitStatusEntry(entry) {
670
+ return entry.slice(0, 2).includes("D");
671
+ }
672
+ function isUntrackedGitStatusEntry(entry) {
673
+ return entry.startsWith("??");
674
+ }
675
+ function isWorktreeOnlyModifiedGitStatusEntry(entry) {
676
+ return entry.slice(0, 2) === " M";
677
+ }
678
+ function parseJsonLine(line) {
679
+ try {
680
+ return JSON.parse(line);
681
+ }
682
+ catch {
683
+ return null;
684
+ }
685
+ }
686
+ function isContextLossSentinelEventFileDirty(agentRoot, entry, entryPath) {
687
+ if (isDeletedGitStatusEntry(entry))
688
+ return false;
689
+ const filePath = path.join(agentRoot, entryPath);
690
+ if (isUntrackedGitStatusEntry(entry)) {
691
+ const candidateLines = fs.existsSync(filePath) ? nonEmptyLines(fs.readFileSync(filePath, "utf-8")) : [];
692
+ return candidateLines.length > 0
693
+ && candidateLines.every((line) => isContextLossSentinelFlightRecorderEvent(parseJsonLine(line)));
694
+ }
695
+ if (!isWorktreeOnlyModifiedGitStatusEntry(entry) || !fs.existsSync(filePath))
696
+ return false;
697
+ const headText = gitShowHeadFile(agentRoot, entryPath);
698
+ if (headText === null)
699
+ return false;
700
+ const headLines = nonEmptyLines(headText);
701
+ const currentLines = nonEmptyLines(fs.readFileSync(filePath, "utf-8"));
702
+ if (currentLines.length <= headLines.length)
703
+ return false;
704
+ if (!headLines.every((line, index) => currentLines[index] === line))
705
+ return false;
706
+ const appendedLines = currentLines.slice(headLines.length);
707
+ return appendedLines.every((line) => isContextLossSentinelFlightRecorderEvent(parseJsonLine(line)));
708
+ }
709
+ function isContextLossSentinelLatestDirty(agentRoot, entry, entryPath) {
710
+ if (isDeletedGitStatusEntry(entry) || isUntrackedGitStatusEntry(entry))
711
+ return false;
712
+ if (!isWorktreeOnlyModifiedGitStatusEntry(entry))
713
+ return false;
714
+ const current = readJson(path.join(agentRoot, entryPath));
715
+ if (!current.ok || !(0, flight_recorder_1.isFlightRecorderResume)(current.value) || !isContextLossSentinelCheckpoint(agentRoot, current.value)) {
716
+ return false;
717
+ }
718
+ const headText = gitShowHeadFile(agentRoot, entryPath);
719
+ if (headText === null)
720
+ return false;
721
+ const head = parseJsonLine(headText);
722
+ if (!(0, flight_recorder_1.isFlightRecorderResume)(head))
723
+ return false;
724
+ return JSON.stringify(neutralizeSentinelCheckpoint(current.value))
725
+ === JSON.stringify(neutralizeSentinelCheckpoint(head));
726
+ }
727
+ function isContextLossSentinelFlightRecorderGitStatusEntry(agentRoot, entry) {
728
+ return gitStatusPaths(entry).every((entryPath) => (isFlightRecorderEventsPath(entryPath)
729
+ ? isContextLossSentinelEventFileDirty(agentRoot, entry, entryPath)
730
+ : isFlightRecorderLatestPath(entryPath)
731
+ ? isContextLossSentinelLatestDirty(agentRoot, entry, entryPath)
732
+ : false));
733
+ }
734
+ function isContextLossSentinelOwnedGitStatusEntry(entry, options) {
735
+ return isSentinelGitStatusEntry(entry)
736
+ || isContextLossSentinelFlightRecorderGitStatusEntry(options.agentRoot, entry);
737
+ }
738
+ function bundleSignal(status, options) {
578
739
  const gitStatusCommand = "git status --porcelain=v1 -uall";
579
740
  if (!status.ok) {
580
741
  return {
@@ -589,7 +750,7 @@ function bundleSignal(status, options = {}) {
589
750
  };
590
751
  }
591
752
  const dirtyEntries = gitStatusEntries(status.porcelain)
592
- .filter((entry) => !(options.ignoreSentinelDirtyEntries && isSentinelGitStatusEntry(entry)));
753
+ .filter((entry) => !(options.ignoreSentinelDirtyEntries && isContextLossSentinelOwnedGitStatusEntry(entry, options)));
593
754
  if (dirtyEntries.length > 0) {
594
755
  return {
595
756
  id: "bundle:git",
@@ -723,6 +884,7 @@ function makeReceipt(agentName, agentRoot, options, generatedAt) {
723
884
  ...healthSignals(options.daemonHealthResults ?? []),
724
885
  bundleSignal((options.gitStatus ?? (() => defaultGitStatus(agentRoot)))(), {
725
886
  ignoreSentinelDirtyEntries: options.trigger === "daemon_health",
887
+ agentRoot,
726
888
  }),
727
889
  ];
728
890
  const verdict = sentinelVerdict(signals);
@@ -687,9 +687,15 @@ void preloadProviderCredentialPools().then(() => daemon.start()).then(async () =
687
687
  message: "daemon entrypoint failed",
688
688
  meta: { error: error.message },
689
689
  });
690
- setTimeout(() => process.exit(1), 5_000).unref();
691
- await daemon.stop();
692
- process.exit(1);
690
+ const forcedExit = setTimeout(() => process.exit(1), 5_000);
691
+ forcedExit.unref();
692
+ try {
693
+ await daemon.stop();
694
+ }
695
+ finally {
696
+ clearTimeout(forcedExit);
697
+ process.exit(1);
698
+ }
693
699
  });
694
700
  process.on("SIGINT", () => {
695
701
  // ALWAYS write a tombstone, even on signal-driven shutdown. The previous
@@ -702,15 +708,29 @@ process.on("SIGINT", () => {
702
708
  _tombstoneWritten = true;
703
709
  (0, daemon_tombstone_1.writeDaemonTombstone)("sigint", new Error("daemon received SIGINT"));
704
710
  stopEntryRuntime();
705
- setTimeout(() => process.exit(1), 5_000).unref();
706
- void daemon.stop().then(() => process.exit(0));
711
+ const forcedExit = setTimeout(() => process.exit(1), 5_000);
712
+ forcedExit.unref();
713
+ void daemon.stop().then(() => {
714
+ clearTimeout(forcedExit);
715
+ process.exit(0);
716
+ }, () => {
717
+ clearTimeout(forcedExit);
718
+ process.exit(1);
719
+ });
707
720
  });
708
721
  process.on("SIGTERM", () => {
709
722
  _tombstoneWritten = true;
710
723
  (0, daemon_tombstone_1.writeDaemonTombstone)("sigterm", new Error("daemon received SIGTERM"));
711
724
  stopEntryRuntime();
712
- setTimeout(() => process.exit(1), 5_000).unref();
713
- void daemon.stop().then(() => process.exit(0));
725
+ const forcedExit = setTimeout(() => process.exit(1), 5_000);
726
+ forcedExit.unref();
727
+ void daemon.stop().then(() => {
728
+ clearTimeout(forcedExit);
729
+ process.exit(0);
730
+ }, () => {
731
+ clearTimeout(forcedExit);
732
+ process.exit(1);
733
+ });
714
734
  });
715
735
  /* v8 ignore stop */
716
736
  // Suppress EPIPE on stdout/stderr — normal when detached daemon's parent exits
@@ -750,8 +770,12 @@ process.on("uncaughtException", (error) => {
750
770
  message: `daemon exiting: ${_uncaughtCount} uncaught exceptions in ${CIRCUIT_BREAKER_WINDOW_MS / 1000}s`,
751
771
  meta: { uncaughtCount: _uncaughtCount },
752
772
  });
753
- setTimeout(() => process.exit(1), 5_000).unref();
754
- void daemon.stop().then(() => process.exit(1));
773
+ const forcedExit = setTimeout(() => process.exit(1), 5_000);
774
+ forcedExit.unref();
775
+ void daemon.stop().finally(() => {
776
+ clearTimeout(forcedExit);
777
+ process.exit(1);
778
+ });
755
779
  }
756
780
  });
757
781
  process.on("unhandledRejection", (reason) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.694",
3
+ "version": "0.1.0-alpha.695",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",