@ouro.bot/cli 0.1.0-alpha.663 → 0.1.0-alpha.665
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 +12 -0
- package/dist/heart/context-loss-sentinel.js +177 -22
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
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.665",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Ignore Sentinel-owned daemon-health git dirt when computing the Sentinel bundle signal, preventing a self-sustaining health-receipt loop while keeping real bundle dirt visible."
|
|
8
|
+
]
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"version": "0.1.0-alpha.664",
|
|
12
|
+
"changes": [
|
|
13
|
+
"Coalesce unchanged periodic Arc Sentinel daemon-health receipts so healthy daemons do not keep synced bundles dirty."
|
|
14
|
+
]
|
|
15
|
+
},
|
|
4
16
|
{
|
|
5
17
|
"version": "0.1.0-alpha.663",
|
|
6
18
|
"changes": [
|
|
@@ -126,6 +126,103 @@ function compareReceiptOrder(left, right) {
|
|
|
126
126
|
function shouldReplaceReceipt(existing, candidate) {
|
|
127
127
|
return existing === null || compareReceiptOrder(candidate, existing) >= 0;
|
|
128
128
|
}
|
|
129
|
+
function orderFromReceipt(receipt) {
|
|
130
|
+
return { generatedAt: receipt.generatedAt, id: receipt.id };
|
|
131
|
+
}
|
|
132
|
+
function compareOrder(left, right) {
|
|
133
|
+
const leftTime = Date.parse(left.generatedAt);
|
|
134
|
+
const rightTime = Date.parse(right.generatedAt);
|
|
135
|
+
if (leftTime !== rightTime)
|
|
136
|
+
return leftTime - rightTime;
|
|
137
|
+
return left.id.localeCompare(right.id);
|
|
138
|
+
}
|
|
139
|
+
function maxOrder(left, right) {
|
|
140
|
+
if (!left)
|
|
141
|
+
return right;
|
|
142
|
+
if (!right)
|
|
143
|
+
return left;
|
|
144
|
+
return compareOrder(left, right) >= 0 ? left : right;
|
|
145
|
+
}
|
|
146
|
+
function isOrder(value) {
|
|
147
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
148
|
+
return false;
|
|
149
|
+
const record = value;
|
|
150
|
+
return typeof record.generatedAt === "string"
|
|
151
|
+
&& isValidTimestamp(record.generatedAt)
|
|
152
|
+
&& typeof record.id === "string";
|
|
153
|
+
}
|
|
154
|
+
function sentinelWatermarkPath(agentRoot) {
|
|
155
|
+
return path.join(agentRoot, "state", "arc", "context-loss-sentinel-watermark.json");
|
|
156
|
+
}
|
|
157
|
+
function readWatermark(agentRoot) {
|
|
158
|
+
const filePath = sentinelWatermarkPath(agentRoot);
|
|
159
|
+
try {
|
|
160
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
161
|
+
return {
|
|
162
|
+
schemaVersion: 1,
|
|
163
|
+
latest: isOrder(parsed.latest) ? parsed.latest : null,
|
|
164
|
+
latestReady: isOrder(parsed.latestReady) ? parsed.latestReady : null,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return { schemaVersion: 1, latest: null, latestReady: null };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function writeWatermark(agentRoot, watermark) {
|
|
172
|
+
atomicWriteJson(sentinelWatermarkPath(agentRoot), watermark);
|
|
173
|
+
}
|
|
174
|
+
function updateWatermarkOrder(watermark, key, candidate) {
|
|
175
|
+
watermark[key] = maxOrder(watermark[key], orderFromReceipt(candidate));
|
|
176
|
+
}
|
|
177
|
+
function stableReceiptState(receipt) {
|
|
178
|
+
return JSON.stringify({
|
|
179
|
+
verdict: receipt.verdict,
|
|
180
|
+
summary: receipt.summary,
|
|
181
|
+
latestReadyLocator: receipt.latestReadyLocator,
|
|
182
|
+
recoveryAnchor: receipt.recoveryAnchor,
|
|
183
|
+
gauntlet: receipt.gauntlet,
|
|
184
|
+
signals: receipt.signals,
|
|
185
|
+
resumeSnapshot: receipt.resumeSnapshot,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
function sameRecoveryState(left, right) {
|
|
189
|
+
return stableReceiptState(left) === stableReceiptState(right);
|
|
190
|
+
}
|
|
191
|
+
function setReceiptFileMtime(filePath, receipt) {
|
|
192
|
+
const timestamp = new Date(receipt.generatedAt);
|
|
193
|
+
fs.utimesSync(filePath, timestamp, timestamp);
|
|
194
|
+
}
|
|
195
|
+
function selectLatestReadyReceipt(existingReady, candidate, latestReadyOrder) {
|
|
196
|
+
if (candidate.verdict !== "ready")
|
|
197
|
+
return existingReady;
|
|
198
|
+
if (latestReadyOrder && compareOrder(orderFromReceipt(candidate), latestReadyOrder) < 0)
|
|
199
|
+
return existingReady;
|
|
200
|
+
return candidate;
|
|
201
|
+
}
|
|
202
|
+
function shouldReplaceLatestReceipt(existingLatest, candidate, latestOrder) {
|
|
203
|
+
if (latestOrder && compareOrder(orderFromReceipt(candidate), latestOrder) < 0)
|
|
204
|
+
return false;
|
|
205
|
+
if (!existingLatest)
|
|
206
|
+
return true;
|
|
207
|
+
return shouldReplaceReceipt(existingLatest, candidate);
|
|
208
|
+
}
|
|
209
|
+
function coalescedDaemonHealthReceipt(agentRoot, existingLatest, existingReady, candidate, watermark) {
|
|
210
|
+
if (candidate.trigger !== "daemon_health")
|
|
211
|
+
return null;
|
|
212
|
+
if (!existingLatest || !sameRecoveryState(existingLatest, candidate))
|
|
213
|
+
return null;
|
|
214
|
+
if (candidate.verdict === "ready" && (!existingReady || !sameRecoveryState(existingReady, candidate)))
|
|
215
|
+
return null;
|
|
216
|
+
const candidateOrder = orderFromReceipt(candidate);
|
|
217
|
+
if (watermark.latest && compareOrder(candidateOrder, watermark.latest) <= 0)
|
|
218
|
+
return existingLatest;
|
|
219
|
+
updateWatermarkOrder(watermark, "latest", candidate);
|
|
220
|
+
if (candidate.verdict === "ready" && existingReady) {
|
|
221
|
+
updateWatermarkOrder(watermark, "latestReady", candidate);
|
|
222
|
+
}
|
|
223
|
+
writeWatermark(agentRoot, watermark);
|
|
224
|
+
return existingLatest;
|
|
225
|
+
}
|
|
129
226
|
function readJson(filePath) {
|
|
130
227
|
try {
|
|
131
228
|
return { ok: true, value: JSON.parse(fs.readFileSync(filePath, "utf-8")) };
|
|
@@ -385,7 +482,7 @@ function healthSignals(results) {
|
|
|
385
482
|
}
|
|
386
483
|
function defaultGitStatus(agentRoot) {
|
|
387
484
|
try {
|
|
388
|
-
const porcelain = (0, child_process_1.execFileSync)("git", ["status", "--porcelain"], {
|
|
485
|
+
const porcelain = (0, child_process_1.execFileSync)("git", ["status", "--porcelain=v1", "-uall"], {
|
|
389
486
|
cwd: agentRoot,
|
|
390
487
|
encoding: "utf-8",
|
|
391
488
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -397,7 +494,50 @@ function defaultGitStatus(agentRoot) {
|
|
|
397
494
|
return { ok: false, error: String(error) };
|
|
398
495
|
}
|
|
399
496
|
}
|
|
400
|
-
function
|
|
497
|
+
function gitStatusEntries(porcelain) {
|
|
498
|
+
return porcelain.split(/\r?\n/).filter((line) => line.trim().length > 0);
|
|
499
|
+
}
|
|
500
|
+
function normalizeGitStatusPath(entryPath) {
|
|
501
|
+
return entryPath.trim().replace(/^"|"$/g, "");
|
|
502
|
+
}
|
|
503
|
+
function gitStatusPaths(entry) {
|
|
504
|
+
const rawPath = entry.slice(3).trim();
|
|
505
|
+
const paths = [];
|
|
506
|
+
let start = 0;
|
|
507
|
+
let inQuote = false;
|
|
508
|
+
let escaped = false;
|
|
509
|
+
for (let index = 0; index < rawPath.length; index += 1) {
|
|
510
|
+
const char = rawPath[index];
|
|
511
|
+
if (escaped) {
|
|
512
|
+
escaped = false;
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
if (inQuote && char === "\\") {
|
|
516
|
+
escaped = true;
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
if (char === "\"") {
|
|
520
|
+
inQuote = !inQuote;
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
if (!inQuote && rawPath.startsWith(" -> ", index)) {
|
|
524
|
+
paths.push(rawPath.slice(start, index));
|
|
525
|
+
start = index + 4;
|
|
526
|
+
index += 3;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
paths.push(rawPath.slice(start));
|
|
530
|
+
return paths.map(normalizeGitStatusPath);
|
|
531
|
+
}
|
|
532
|
+
function isSentinelGitStatusEntry(entry) {
|
|
533
|
+
const sentinelRoot = relativeSentinelRoot();
|
|
534
|
+
return gitStatusPaths(entry).every((entryPath) => {
|
|
535
|
+
const normalizedPath = entryPath.replace(/\/$/, "");
|
|
536
|
+
return normalizedPath === sentinelRoot || normalizedPath.startsWith(`${sentinelRoot}/`);
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
function bundleSignal(status, options = {}) {
|
|
540
|
+
const gitStatusCommand = "git status --porcelain=v1 -uall";
|
|
401
541
|
if (!status.ok) {
|
|
402
542
|
return {
|
|
403
543
|
id: "bundle:git",
|
|
@@ -406,11 +546,12 @@ function bundleSignal(status) {
|
|
|
406
546
|
severity: "warn",
|
|
407
547
|
verdictImpact: "watch",
|
|
408
548
|
summary: `bundle git status unavailable: ${status.error}`,
|
|
409
|
-
source: { kind: "git", locator:
|
|
410
|
-
repair: repair("agent-runnable", "bundle-cleanup", "Inspect bundle git state before assuming the local state is clean.",
|
|
549
|
+
source: { kind: "git", locator: gitStatusCommand },
|
|
550
|
+
repair: repair("agent-runnable", "bundle-cleanup", "Inspect bundle git state before assuming the local state is clean.", gitStatusCommand),
|
|
411
551
|
};
|
|
412
552
|
}
|
|
413
|
-
const dirtyEntries = status.porcelain
|
|
553
|
+
const dirtyEntries = gitStatusEntries(status.porcelain)
|
|
554
|
+
.filter((entry) => !(options.ignoreSentinelDirtyEntries && isSentinelGitStatusEntry(entry)));
|
|
414
555
|
if (dirtyEntries.length > 0) {
|
|
415
556
|
return {
|
|
416
557
|
id: "bundle:git",
|
|
@@ -419,8 +560,8 @@ function bundleSignal(status) {
|
|
|
419
560
|
severity: "warn",
|
|
420
561
|
verdictImpact: "watch",
|
|
421
562
|
summary: `bundle has ${dirtyEntries.length} uncommitted git status entr${dirtyEntries.length === 1 ? "y" : "ies"}`,
|
|
422
|
-
source: { kind: "git", locator:
|
|
423
|
-
repair: repair("agent-runnable", "bundle-cleanup", "Resolve or intentionally preserve local bundle changes before handoff.",
|
|
563
|
+
source: { kind: "git", locator: gitStatusCommand },
|
|
564
|
+
repair: repair("agent-runnable", "bundle-cleanup", "Resolve or intentionally preserve local bundle changes before handoff.", gitStatusCommand),
|
|
424
565
|
meta: { dirtyEntries },
|
|
425
566
|
};
|
|
426
567
|
}
|
|
@@ -431,7 +572,7 @@ function bundleSignal(status) {
|
|
|
431
572
|
severity: "info",
|
|
432
573
|
verdictImpact: "none",
|
|
433
574
|
summary: "bundle git status clean",
|
|
434
|
-
source: { kind: "git", locator:
|
|
575
|
+
source: { kind: "git", locator: gitStatusCommand },
|
|
435
576
|
};
|
|
436
577
|
}
|
|
437
578
|
function sentinelVerdict(signals) {
|
|
@@ -536,7 +677,9 @@ function makeReceipt(agentName, agentRoot, options, generatedAt) {
|
|
|
536
677
|
gauntletSignal(report),
|
|
537
678
|
...deriveContextLossSentinelProviderSignals(providerVisibility),
|
|
538
679
|
...healthSignals(options.daemonHealthResults ?? []),
|
|
539
|
-
bundleSignal((options.gitStatus ?? (() => defaultGitStatus(agentRoot)))()
|
|
680
|
+
bundleSignal((options.gitStatus ?? (() => defaultGitStatus(agentRoot)))(), {
|
|
681
|
+
ignoreSentinelDirtyEntries: options.trigger === "daemon_health",
|
|
682
|
+
}),
|
|
540
683
|
];
|
|
541
684
|
const verdict = sentinelVerdict(signals);
|
|
542
685
|
const latestReady = readLatestReady(agentRoot);
|
|
@@ -619,26 +762,38 @@ function recordBlockedReceiptEvent(agentRoot, receipt) {
|
|
|
619
762
|
}
|
|
620
763
|
async function persistReceipt(agentRoot, receipt, lockTimeoutMs) {
|
|
621
764
|
const paths = contextLossSentinelPaths(agentRoot);
|
|
622
|
-
|
|
765
|
+
return withFileLock(paths.lock, lockTimeoutMs, async () => {
|
|
623
766
|
ensureSentinelDirs(paths);
|
|
624
767
|
const existingLatest = readReceiptFile(paths.latest, "latest.json", []);
|
|
625
768
|
const existingReady = readLatestReady(agentRoot);
|
|
626
|
-
const
|
|
627
|
-
|
|
628
|
-
|
|
769
|
+
const watermark = readWatermark(agentRoot);
|
|
770
|
+
const latestOrder = maxOrder(maxOrder(maxOrder(existingLatest ? orderFromReceipt(existingLatest) : null, existingReady ? orderFromReceipt(existingReady) : null), watermark.latest), watermark.latestReady);
|
|
771
|
+
const latestReadyOrder = maxOrder(maxOrder(existingReady ? orderFromReceipt(existingReady) : null, watermark.latestReady), existingLatest?.verdict === "ready" ? orderFromReceipt(existingLatest) : null);
|
|
772
|
+
const nextReady = selectLatestReadyReceipt(existingReady, receipt, latestReadyOrder);
|
|
629
773
|
syncLatestReadyState(receipt, nextReady);
|
|
774
|
+
const coalesced = coalescedDaemonHealthReceipt(agentRoot, existingLatest, existingReady, receipt, watermark);
|
|
775
|
+
if (coalesced)
|
|
776
|
+
return coalesced;
|
|
630
777
|
atomicWriteJson(path.join(paths.receiptsDir, `${receipt.id}.json`), receipt);
|
|
631
778
|
appendHistory(paths, receipt);
|
|
632
|
-
if (
|
|
779
|
+
if (shouldReplaceLatestReceipt(existingLatest, receipt, latestOrder)) {
|
|
633
780
|
atomicWriteJson(paths.latest, receipt);
|
|
781
|
+
updateWatermarkOrder(watermark, "latest", receipt);
|
|
782
|
+
writeWatermark(agentRoot, watermark);
|
|
783
|
+
setReceiptFileMtime(paths.latest, receipt);
|
|
634
784
|
recordBlockedReceiptEvent(agentRoot, receipt);
|
|
635
785
|
}
|
|
636
|
-
else if (existingLatest &&
|
|
637
|
-
|
|
786
|
+
else if (existingLatest && nextReady === receipt) {
|
|
787
|
+
const updatedLatest = syncLatestReadyState(existingLatest, nextReady);
|
|
788
|
+
atomicWriteJson(paths.latest, updatedLatest);
|
|
638
789
|
}
|
|
639
790
|
if (receipt.verdict === "ready" && nextReady === receipt) {
|
|
640
791
|
atomicWriteJson(paths.latestReady, receipt);
|
|
792
|
+
updateWatermarkOrder(watermark, "latestReady", receipt);
|
|
793
|
+
writeWatermark(agentRoot, watermark);
|
|
794
|
+
setReceiptFileMtime(paths.latestReady, receipt);
|
|
641
795
|
}
|
|
796
|
+
return receipt;
|
|
642
797
|
});
|
|
643
798
|
}
|
|
644
799
|
async function refreshContextLossSentinel(agentName, agentRoot, options) {
|
|
@@ -647,7 +802,7 @@ async function refreshContextLossSentinel(agentName, agentRoot, options) {
|
|
|
647
802
|
if (options.delayBeforeWriteMs && options.delayBeforeWriteMs > 0) {
|
|
648
803
|
await sleep(options.delayBeforeWriteMs);
|
|
649
804
|
}
|
|
650
|
-
await persistReceipt(agentRoot, receipt, options.lockTimeoutMs ?? 5_000);
|
|
805
|
+
const persistedReceipt = await persistReceipt(agentRoot, receipt, options.lockTimeoutMs ?? 5_000);
|
|
651
806
|
(0, runtime_1.emitNervesEvent)({
|
|
652
807
|
component: "engine",
|
|
653
808
|
event: "engine.context_loss_sentinel_refreshed",
|
|
@@ -655,13 +810,13 @@ async function refreshContextLossSentinel(agentName, agentRoot, options) {
|
|
|
655
810
|
meta: {
|
|
656
811
|
agentName,
|
|
657
812
|
trigger: options.trigger,
|
|
658
|
-
verdict:
|
|
659
|
-
receiptId:
|
|
660
|
-
blockedSignals:
|
|
661
|
-
watchSignals:
|
|
813
|
+
verdict: persistedReceipt.verdict,
|
|
814
|
+
receiptId: persistedReceipt.id,
|
|
815
|
+
blockedSignals: persistedReceipt.signals.filter((entry) => entry.verdictImpact === "blocked").map((entry) => entry.id),
|
|
816
|
+
watchSignals: persistedReceipt.signals.filter((entry) => entry.verdictImpact === "watch").map((entry) => entry.id),
|
|
662
817
|
},
|
|
663
818
|
});
|
|
664
|
-
return
|
|
819
|
+
return persistedReceipt;
|
|
665
820
|
}
|
|
666
821
|
function readHistory(paths, limit, issues) {
|
|
667
822
|
if (!fs.existsSync(paths.historyDir))
|