@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:
|
|
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
|
|
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 &&
|
|
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)
|
|
691
|
-
|
|
692
|
-
|
|
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)
|
|
706
|
-
|
|
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)
|
|
713
|
-
|
|
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)
|
|
754
|
-
|
|
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) => {
|