klypix-mcp 1.67.2 → 1.68.0
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/README.md +11 -6
- package/package.json +2 -2
- package/src/agent-presence.mjs +74 -11
- package/src/global-brain-hook.mjs +158 -33
- package/src/klypix-core.mjs +25 -9
- package/src/klypix-format.mjs +18 -2
- package/src/mcp-presence.mjs +8 -3
package/README.md
CHANGED
|
@@ -693,17 +693,22 @@ Read this section before you build on any of it.
|
|
|
693
693
|
Every number here is measured on our own project brain. Nothing below is published, benchmarked or
|
|
694
694
|
independently validated.
|
|
695
695
|
|
|
696
|
-
- **Dogfood scale.** KLYPIX itself is built with its own brain: **
|
|
696
|
+
- **Dogfood scale.** KLYPIX itself is built with its own brain: **2,479 cards and 2,018
|
|
697
697
|
connections**, written by multiple concurrent agent sessions, receipts in the file. Current as of
|
|
698
|
-
2026-08-
|
|
698
|
+
2026-08-13.
|
|
699
699
|
- **Recall.** 73% of past decisions recovered with one search round, 55% brief-only, 0% cold.
|
|
700
700
|
Caveat that travels with it: n=20, our own brain, self-authored questions, LLM-judged.
|
|
701
|
-
- **Ranker.**
|
|
702
|
-
|
|
703
|
-
|
|
701
|
+
- **Ranker.** With the production embedder (the eval harness was fixed 2026-08-10 — it had been
|
|
702
|
+
measuring a vector space the product does not use): recall@5 **30%**, recall@10 35%, recall@20
|
|
703
|
+
45%, MRR 0.22 of the true source card on n=20 frozen human-paraphrase questions. Lexical-only
|
|
704
|
+
scores 0% on the same set. The previously published "15% → 40% with the reranker" is **retired**:
|
|
705
|
+
re-measured validly, the reranker *reduced* recall@5 to 25% and now ships off by default. At n=20
|
|
706
|
+
every one of these percentages carries a ±20-point 95% confidence interval — treat them as
|
|
707
|
+
directional until the larger frozen set lands. The regressions are recorded next to the wins:
|
|
708
|
+
contextual prefixes on short cards, and the reranker itself.
|
|
704
709
|
- **What we do not publish.** No download count: this package's own 24-hour auto-updater generates
|
|
705
710
|
most of it, so it is not a user count. No adoption, team or customer figures. No brief-token
|
|
706
|
-
figure — the last one was measured at ~600 cards and is stale at
|
|
711
|
+
figure — the last one was measured at ~600 cards and is stale at 2,479.
|
|
707
712
|
- **The eval harness is not in this repo.** It lives in the private KLYPIX desktop repository. The
|
|
708
713
|
numbers above are ours to defend, not yours to reproduce from here — treat them accordingly.
|
|
709
714
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "klypix-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.68.0",
|
|
4
4
|
"description": "Shared project brain and MCP coordination server for multi-agent coding.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"bench": "node bin/klypix-mcp.mjs bench",
|
|
84
84
|
"test:bench": "node test/bench.mjs",
|
|
85
85
|
"pretest": "node test/publish-workflow.mjs",
|
|
86
|
-
"test": "node test/publish-verdict.mjs && node test/remote-client.mjs && node test/project-graph.mjs && node test/project-map-cli.mjs && node test/mcp-auto-update.mjs && node test/mcp-supervisor.mjs && node test/runtime-inspector.mjs && node test/codex-hooks.mjs && node test/request-identity.mjs && node test/session-identity-core.mjs && node test/agent-presence.mjs && node test/message-delivery-v3.mjs && node test/claude-message-delivery-v3.mjs && node test/result-reconcile.mjs && node test/evidence-publication-gate.mjs && node test/release-evidence-cli.mjs && node test/intent-guard.mjs && node test/git-capture-install.mjs && node test/brain-history.mjs && node test/brain-graveyard.mjs && node test/archived-visibility.mjs && node test/finding-routing.mjs && node test/finding-routing-hook.mjs && node test/presence-relay.mjs && node test/install-version.mjs && node test/context-gateway.mjs && node test/conformance.mjs && node test/brain-doctor.mjs && node test/version-currency.mjs && node test/ship-capture.mjs && node test/lane-message.mjs && node test/brain-quality.mjs && node test/brain-connect-orphans.mjs && node test/brief-and-recall.mjs && node test/layout-cluster.mjs && node test/brain-ask.mjs && node test/field-report-2026-07-04.mjs && node test/autoprop.mjs && node test/overlay-recency-2026-07-12.mjs && node test/brain-challenge.mjs && node test/brain-lens.mjs && node test/brain-kind.mjs && node test/rule-drafts.mjs && node test/claim-engine.mjs && node test/skill-staleness.mjs && node test/canvas-view.mjs && node test/status-completeness.mjs && node test/semantic-security.mjs && node test/semantic-gate.mjs && node test/memory-runtime.mjs && node test/semantic-cache.mjs && node test/decay-status.mjs && node test/decay-hook.mjs && node test/evidence-anchors.mjs && node test/presence-visibility.mjs && node test/merge-brains.mjs && node test/concurrent-writes.mjs && node test/lock-interop.mjs && node test/a2a-smoke.mjs && node test/cli-args.mjs && node test/format-guard.mjs && node test/git-tools.mjs && node test/uninstall.mjs",
|
|
86
|
+
"test": "node test/publish-verdict.mjs && node test/remote-client.mjs && node test/project-graph.mjs && node test/project-map-cli.mjs && node test/mcp-auto-update.mjs && node test/mcp-supervisor.mjs && node test/runtime-inspector.mjs && node test/codex-hooks.mjs && node test/request-identity.mjs && node test/session-identity-core.mjs && node test/agent-presence.mjs && node test/message-delivery-v3.mjs && node test/claude-message-delivery-v3.mjs && node test/result-reconcile.mjs && node test/evidence-publication-gate.mjs && node test/release-evidence-cli.mjs && node test/intent-guard.mjs && node test/git-capture-install.mjs && node test/brain-history.mjs && node test/brain-graveyard.mjs && node test/archived-visibility.mjs && node test/finding-routing.mjs && node test/finding-routing-hook.mjs && node test/presence-relay.mjs && node test/install-version.mjs && node test/context-gateway.mjs && node test/conformance.mjs && node test/brain-doctor.mjs && node test/version-currency.mjs && node test/ship-capture.mjs && node test/lane-message.mjs && node test/brain-quality.mjs && node test/brain-connect-orphans.mjs && node test/brief-and-recall.mjs && node test/layout-cluster.mjs && node test/brain-ask.mjs && node test/field-report-2026-07-04.mjs && node test/autoprop.mjs && node test/overlay-recency-2026-07-12.mjs && node test/brain-challenge.mjs && node test/brain-lens.mjs && node test/brain-kind.mjs && node test/rule-drafts.mjs && node test/claim-engine.mjs && node test/skill-staleness.mjs && node test/canvas-view.mjs && node test/status-completeness.mjs && node test/semantic-security.mjs && node test/semantic-gate.mjs && node test/memory-runtime.mjs && node test/semantic-cache.mjs && node test/decay-status.mjs && node test/decay-hook.mjs && node test/evidence-anchors.mjs && node test/presence-visibility.mjs && node test/merge-brains.mjs && node test/concurrent-writes.mjs && node test/lock-interop.mjs && node test/capture-write-failure.mjs && node test/a2a-smoke.mjs && node test/cli-args.mjs && node test/format-guard.mjs && node test/git-tools.mjs && node test/uninstall.mjs",
|
|
87
87
|
"test:memory": "node test/memory-runtime.mjs",
|
|
88
88
|
"test:memory:soak": "node --expose-gc test/memory-soak.mjs",
|
|
89
89
|
"runtime": "node bin/klypix-runtime.mjs"
|
package/src/agent-presence.mjs
CHANGED
|
@@ -454,6 +454,7 @@ function normalizeDeliveryRecord(record) {
|
|
|
454
454
|
...(record?.acknowledgedActionId ? { acknowledgedActionId: String(record.acknowledgedActionId).slice(0, 160) } : {}),
|
|
455
455
|
...(record?.consumedActionId ? { consumedActionId: String(record.consumedActionId).slice(0, 160) } : {}),
|
|
456
456
|
...(record?.offerToken ? { offerToken: String(record.offerToken).slice(0, 160) } : {}),
|
|
457
|
+
...(record?.consumedVia ? { consumedVia: String(record.consumedVia).slice(0, 40) } : {}),
|
|
457
458
|
...(record?.reason ? { reason: String(record.reason).slice(0, 120) } : {}),
|
|
458
459
|
...(record?.legacySeen ? { legacySeen: true } : {}),
|
|
459
460
|
};
|
|
@@ -485,9 +486,16 @@ export function normalizeMessageDelivery(message, now = Date.now()) {
|
|
|
485
486
|
next.deliveryVersion = MESSAGE_DELIVERY_VERSION;
|
|
486
487
|
next.deliveries = [...byRecipient.values()];
|
|
487
488
|
if (!Array.isArray(next.seen)) next.seen = [];
|
|
488
|
-
//
|
|
489
|
-
//
|
|
490
|
-
|
|
489
|
+
// History is never rewritten (2026-08-13). The previous migration deleted
|
|
490
|
+
// retiredAt from v2-retired messages to "conservatively replay" them — which
|
|
491
|
+
// resurrected up to a week of already-delivered notes, and immediately
|
|
492
|
+
// re-terminalized every one older than 24h as FAILED. That falsified
|
|
493
|
+
// delivery history in both directions on first post-upgrade touch. A v2
|
|
494
|
+
// retirement proved model-context injection; it stays retired, recorded as
|
|
495
|
+
// exactly that and no more.
|
|
496
|
+
if (sourceVersion < MESSAGE_DELIVERY_VERSION && next.retiredAt && !next.deadLetter && !next.retirement) {
|
|
497
|
+
next.retirement = { reason: 'v2-retired — model-context injection proven, explicit consumption unknown', at: Number(next.retiredAt) || now };
|
|
498
|
+
}
|
|
491
499
|
return next;
|
|
492
500
|
}
|
|
493
501
|
|
|
@@ -527,6 +535,7 @@ function setDeliveryState(message, sessionId, state, now, reason = null, actionI
|
|
|
527
535
|
delete record.consumedAt;
|
|
528
536
|
delete record.acknowledgedActionId;
|
|
529
537
|
delete record.consumedActionId;
|
|
538
|
+
delete record.consumedVia;
|
|
530
539
|
delete record.failedAt;
|
|
531
540
|
delete record.reason;
|
|
532
541
|
} else if (state === 'acknowledged') {
|
|
@@ -557,14 +566,29 @@ function terminalizeMessage(message, now, reason) {
|
|
|
557
566
|
...(Array.isArray(next.candidateIds) ? next.candidateIds : []),
|
|
558
567
|
...next.deliveries.map((entry) => entry.recipientId),
|
|
559
568
|
].map(recipientKey).filter(Boolean));
|
|
560
|
-
|
|
569
|
+
// Split by how far delivery actually got (2026-08-13). 'acknowledged' means
|
|
570
|
+
// the note was rendered into model context on two independent actions —
|
|
571
|
+
// expiring after that is NOT a delivery failure, and the old blanket
|
|
572
|
+
// 'failed' told the sender "no target consumed it" about a note the model
|
|
573
|
+
// saw repeatedly. Only recipients the note never reached (pending) or
|
|
574
|
+
// reached exactly once without confirmation (offered) fail, each with a
|
|
575
|
+
// reason that says which. A message whose every recipient at least
|
|
576
|
+
// acknowledged retires as delivered-unconfirmed instead of dead-lettering.
|
|
577
|
+
let failures = 0, reachedUnconsumed = 0;
|
|
561
578
|
for (const recipientId of known) {
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
579
|
+
const state = messageDeliveryState(next, recipientId);
|
|
580
|
+
if (state === 'consumed') continue;
|
|
581
|
+
if (state === 'acknowledged') { reachedUnconsumed++; continue; }
|
|
582
|
+
failures++;
|
|
583
|
+
setDeliveryState(next, recipientId, 'failed', now,
|
|
584
|
+
state === 'pending' ? `${reason} (never delivered)` : `${reason} (offered once, unconfirmed)`);
|
|
585
|
+
}
|
|
586
|
+
if (failures || known.size === 0) {
|
|
587
|
+
next.deadLetter = { state: 'failed', reason, at: now };
|
|
588
|
+
} else {
|
|
589
|
+
next.retiredAt = now;
|
|
590
|
+
if (reachedUnconsumed) next.retirement = { reason: `${reason} — after acknowledgement (delivered, consumption unconfirmed)`, at: now };
|
|
565
591
|
}
|
|
566
|
-
if (unconsumed) next.deadLetter = { state: 'failed', reason, at: now };
|
|
567
|
-
else next.retiredAt = now;
|
|
568
592
|
return next;
|
|
569
593
|
}
|
|
570
594
|
|
|
@@ -1537,9 +1561,13 @@ export function receiveMessages({
|
|
|
1537
1561
|
const record = messageDeliveryRecord(message, sessionId);
|
|
1538
1562
|
return !isTerminalMessage(message)
|
|
1539
1563
|
&& message.from !== sessionId
|
|
1540
|
-
|
|
1564
|
+
// 'acknowledged' re-renders ONLY when there is no action-identity
|
|
1565
|
+
// evidence of a third action (no actionId on this call, or a legacy
|
|
1566
|
+
// record acknowledged without one). With evidence, the lease pass
|
|
1567
|
+
// below retires it as consumed instead of injecting it a third time.
|
|
1568
|
+
&& (state === 'pending' || state === 'offered'
|
|
1569
|
+
|| (state === 'acknowledged' && (!actionId || !record?.acknowledgedActionId)))
|
|
1541
1570
|
&& !(state === 'offered' && actionId && record?.offeredActionId === String(actionId))
|
|
1542
|
-
&& !(state === 'acknowledged' && actionId && record?.acknowledgedActionId === String(actionId))
|
|
1543
1571
|
&& messageTargetsSession(message, me, sessionId, sessions);
|
|
1544
1572
|
}).sort((left, right) => {
|
|
1545
1573
|
// Never let an acknowledged-but-not-yet-consumed replay starve a fresh
|
|
@@ -1577,6 +1605,30 @@ export function receiveMessages({
|
|
|
1577
1605
|
setDeliveryState(message, sessionId, 'acknowledged', now, null, actionId);
|
|
1578
1606
|
}
|
|
1579
1607
|
}
|
|
1608
|
+
// Lease auto-consume (2026-08-13). An acknowledged note was rendered into
|
|
1609
|
+
// model context on TWO independent actions; when a THIRD independent
|
|
1610
|
+
// action arrives, the model has demonstrably moved on with the note in
|
|
1611
|
+
// hand. Retire it as consumed (consumedVia 'auto-lease') instead of
|
|
1612
|
+
// replaying it every action for 24h and then dead-lettering a note the
|
|
1613
|
+
// model saw repeatedly as "failed" — the old design guaranteed a steady
|
|
1614
|
+
// background rate of false-failure receipts from every recipient that
|
|
1615
|
+
// cannot (hook-only lanes) or does not copy offer tokens back. The
|
|
1616
|
+
// explicit brain_message_receipt remains the only path that can record
|
|
1617
|
+
// the stronger claim (consumedVia 'receipt' — "I acted on it").
|
|
1618
|
+
// Requires a real, DIFFERENT actionId: with no action identity there is
|
|
1619
|
+
// no evidence of a third action, so behavior stays replay-until-receipt.
|
|
1620
|
+
if (actionId) {
|
|
1621
|
+
for (const message of messages) {
|
|
1622
|
+
if (isTerminalMessage(message) || message.from === sessionId) continue;
|
|
1623
|
+
const record = messageDeliveryRecord(message, sessionId);
|
|
1624
|
+
if (!record || record.state !== 'acknowledged') continue;
|
|
1625
|
+
if (!record.acknowledgedActionId || record.acknowledgedActionId === String(actionId)) continue;
|
|
1626
|
+
if (!messageTargetsSession(message, me, sessionId, sessions)) continue;
|
|
1627
|
+
setDeliveryState(message, sessionId, 'consumed', now, null, actionId);
|
|
1628
|
+
record.consumedVia = 'auto-lease';
|
|
1629
|
+
retireFullyConsumed(message, now);
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1580
1632
|
// Persist migration/expiry/dead-letter changes even when the inbox is empty.
|
|
1581
1633
|
fs.mkdirSync(path.dirname(laneFile), { recursive: true });
|
|
1582
1634
|
writeLaneFileAtomic(laneFile, JSON.stringify({ ...data, sessions, messages }));
|
|
@@ -1684,6 +1736,15 @@ export function consumeMessageReceipt({
|
|
|
1684
1736
|
return receiptResult(false, false, 'rejected', 'offer-token-mismatch', wantedMessageId, recipientId);
|
|
1685
1737
|
}
|
|
1686
1738
|
if (state === 'consumed') {
|
|
1739
|
+
// An auto-leased consumption (the third-action lease in receiveMessages)
|
|
1740
|
+
// proves the model moved on with the note in context; an explicit
|
|
1741
|
+
// receipt is the STRONGER claim — "I acted on it". Record the upgrade.
|
|
1742
|
+
if (record.consumedVia !== 'receipt') {
|
|
1743
|
+
record.consumedVia = 'receipt';
|
|
1744
|
+
fs.mkdirSync(path.dirname(laneFile), { recursive: true });
|
|
1745
|
+
writeLaneFileAtomic(laneFile, JSON.stringify({ ...data, sessions, messages }));
|
|
1746
|
+
return receiptResult(true, true, 'consumed', null, wantedMessageId, recipientId);
|
|
1747
|
+
}
|
|
1687
1748
|
return receiptResult(true, false, 'consumed', null, wantedMessageId, recipientId);
|
|
1688
1749
|
}
|
|
1689
1750
|
if (state === 'offered') {
|
|
@@ -1698,6 +1759,8 @@ export function consumeMessageReceipt({
|
|
|
1698
1759
|
return receiptResult(false, false, 'rejected', 'delivery-not-acknowledged', wantedMessageId, recipientId);
|
|
1699
1760
|
}
|
|
1700
1761
|
setDeliveryState(message, recipientId, 'consumed', now, null, actionId);
|
|
1762
|
+
const consumedRecord = messageDeliveryRecord(message, recipientId);
|
|
1763
|
+
if (consumedRecord) consumedRecord.consumedVia = 'receipt';
|
|
1701
1764
|
retireFullyConsumed(message, now);
|
|
1702
1765
|
fs.mkdirSync(path.dirname(laneFile), { recursive: true });
|
|
1703
1766
|
writeLaneFileAtomic(laneFile, JSON.stringify({ ...data, sessions, messages }));
|
|
@@ -400,20 +400,80 @@ function sweepStaleTmp(dir) {
|
|
|
400
400
|
// replace only what it drained, so a batch a peer queued meanwhile survives.
|
|
401
401
|
const PENDING_CAPTURES_FILE = path.join(os.homedir(), '.claude', 'project-brain', 'pending', `${sha(normBrainPath(laneCanon(BRAIN)))}.captures.json`);
|
|
402
402
|
const PENDING_CAPTURES_LOCK = PENDING_CAPTURES_FILE + '.lock';
|
|
403
|
+
// A non-ENOENT read failure must ABORT any read-modify-write instead of
|
|
404
|
+
// masquerading as an empty queue — an RMW over a transient EPERM/corrupt read
|
|
405
|
+
// would rewrite the file as "just my change" and destroy every queued batch
|
|
406
|
+
// (2026-08-14 adversarial review). ENOENT alone means genuinely empty.
|
|
407
|
+
function readMainPendingChecked() {
|
|
408
|
+
try { const d = JSON.parse(fs.readFileSync(PENDING_CAPTURES_FILE, 'utf8')); return { ok: true, batches: Array.isArray(d) ? d : [] }; }
|
|
409
|
+
catch (e) { return e?.code === 'ENOENT' ? { ok: true, batches: [] } : { ok: false, batches: [] }; }
|
|
410
|
+
}
|
|
411
|
+
function readMainPendingFile() { return readMainPendingChecked().batches; }
|
|
412
|
+
// Orphan sidecars: the lossless fallback when the pending LOCK itself is
|
|
413
|
+
// contended (two sessions refuse in the same instant — the trigger, a held
|
|
414
|
+
// brain lock, is CORRELATED across sessions, so "rare" was wrong). Each orphan
|
|
415
|
+
// is one batch in its own uniquely-named file, written tmp→rename so a drain's
|
|
416
|
+
// readdir never sees a torn one. Drained orphans are deleted by PATH only
|
|
417
|
+
// after the brain write is durable — same discipline as clear-by-id.
|
|
418
|
+
function listOrphanCaptureFiles() {
|
|
419
|
+
try {
|
|
420
|
+
const dir = path.dirname(PENDING_CAPTURES_FILE);
|
|
421
|
+
const prefix = path.basename(PENDING_CAPTURES_FILE) + '.orphan-';
|
|
422
|
+
return fs.readdirSync(dir).filter((n) => n.startsWith(prefix) && !/\.tmp-[a-z0-9-]+$/.test(n)).map((n) => path.join(dir, n));
|
|
423
|
+
} catch { return []; }
|
|
424
|
+
}
|
|
403
425
|
function readPendingCaptures() {
|
|
404
|
-
|
|
426
|
+
const batches = readMainPendingFile();
|
|
427
|
+
for (const f of listOrphanCaptureFiles()) {
|
|
428
|
+
try { const b = JSON.parse(fs.readFileSync(f, 'utf8')); if (b && typeof b === 'object') batches.push({ ...b, __orphanPath: f }); }
|
|
429
|
+
catch { /* unreadable orphan stays in place; the next drain retries it */ }
|
|
430
|
+
}
|
|
431
|
+
return batches;
|
|
405
432
|
}
|
|
406
433
|
function updatePendingCaptures(mutate) {
|
|
407
434
|
const got = acquireLock(PENDING_CAPTURES_LOCK, { tries: 20, waitMs: 25 });
|
|
408
435
|
try {
|
|
409
|
-
const
|
|
436
|
+
const read = readMainPendingChecked();
|
|
437
|
+
if (!read.ok) return; // unreadable ≠ empty: abort the RMW, preserve the bytes; the next drain retries
|
|
438
|
+
const next = mutate(read.batches);
|
|
410
439
|
fs.mkdirSync(path.dirname(PENDING_CAPTURES_FILE), { recursive: true });
|
|
411
|
-
const tmp = PENDING_CAPTURES_FILE
|
|
440
|
+
const tmp = `${PENDING_CAPTURES_FILE}.tmp-${process.pid}-${Math.random().toString(36).slice(2, 8)}`;
|
|
412
441
|
fs.writeFileSync(tmp, JSON.stringify(next));
|
|
413
442
|
fs.renameSync(tmp, PENDING_CAPTURES_FILE);
|
|
414
|
-
} catch { /*
|
|
443
|
+
} catch { /* clears are retried by the next drain; adds go through queuePendingCapture */ }
|
|
415
444
|
finally { if (got) releaseLock(PENDING_CAPTURES_LOCK); }
|
|
416
445
|
}
|
|
446
|
+
// Lossless add. RMW under the pending lock is first choice; on lock contention
|
|
447
|
+
// or a failed write it falls back to an append-only orphan file. Returns false
|
|
448
|
+
// only when BOTH paths failed — the caller must then leave every baseline
|
|
449
|
+
// unadvanced so the markers are re-gathered next Stop instead of lost.
|
|
450
|
+
function queuePendingCapture(batch) {
|
|
451
|
+
const got = acquireLock(PENDING_CAPTURES_LOCK, { tries: 20, waitMs: 25 });
|
|
452
|
+
if (got) {
|
|
453
|
+
try {
|
|
454
|
+
const read = readMainPendingChecked();
|
|
455
|
+
if (!read.ok) throw new Error('pending queue unreadable — RMW would destroy it; using the orphan path');
|
|
456
|
+
const next = [...read.batches, batch];
|
|
457
|
+
fs.mkdirSync(path.dirname(PENDING_CAPTURES_FILE), { recursive: true });
|
|
458
|
+
const tmp = `${PENDING_CAPTURES_FILE}.tmp-${process.pid}-${Math.random().toString(36).slice(2, 8)}`;
|
|
459
|
+
fs.writeFileSync(tmp, JSON.stringify(next));
|
|
460
|
+
fs.renameSync(tmp, PENDING_CAPTURES_FILE);
|
|
461
|
+
return true;
|
|
462
|
+
} catch { /* fall through to the orphan path */ }
|
|
463
|
+
finally { releaseLock(PENDING_CAPTURES_LOCK); }
|
|
464
|
+
}
|
|
465
|
+
try {
|
|
466
|
+
fs.mkdirSync(path.dirname(PENDING_CAPTURES_FILE), { recursive: true });
|
|
467
|
+
const orphan = `${PENDING_CAPTURES_FILE}.orphan-${process.pid}-${Math.random().toString(36).slice(2, 8)}`;
|
|
468
|
+
const tmp = `${orphan}.tmp-${process.pid}`;
|
|
469
|
+
fs.writeFileSync(tmp, JSON.stringify(batch));
|
|
470
|
+
fs.renameSync(tmp, orphan);
|
|
471
|
+
return true;
|
|
472
|
+
} catch { return false; }
|
|
473
|
+
}
|
|
474
|
+
function clearDrainedOrphans(paths) {
|
|
475
|
+
for (const f of (paths || [])) { try { fs.unlinkSync(f); } catch { /* re-drained next time; landing twice is superseded away */ } }
|
|
476
|
+
}
|
|
417
477
|
const SESSION_FRESH_MS = 10 * 60 * 1000; // a lane unseen for 10min is treated as ended
|
|
418
478
|
const MCP_SESSION_FRESH_MS = 3 * 60 * 1000; // an mcp-channel heartbeat is dead after 3min (matches agent-presence)
|
|
419
479
|
// The host CLI's pid (Claude Code exports CLAUDE_PID to every child, including
|
|
@@ -806,6 +866,7 @@ function normalizeMsg(m, now = Date.now()) {
|
|
|
806
866
|
...(raw.acknowledgedActionId ? { acknowledgedActionId: String(raw.acknowledgedActionId).slice(0, 160) } : {}),
|
|
807
867
|
...(raw.consumedActionId ? { consumedActionId: String(raw.consumedActionId).slice(0, 160) } : {}),
|
|
808
868
|
...(raw.offerToken ? { offerToken: String(raw.offerToken).slice(0, 160) } : {}),
|
|
869
|
+
...(raw.consumedVia ? { consumedVia: String(raw.consumedVia).slice(0, 40) } : {}),
|
|
809
870
|
...(raw.reason ? { reason: String(raw.reason).slice(0, 120) } : {}),
|
|
810
871
|
...(raw.legacySeen ? { legacySeen: true } : {}),
|
|
811
872
|
});
|
|
@@ -821,10 +882,15 @@ function normalizeMsg(m, now = Date.now()) {
|
|
|
821
882
|
next.deliveryVersion = MSG_DELIVERY_VERSION;
|
|
822
883
|
next.deliveries = [...records.values()];
|
|
823
884
|
if (!Array.isArray(next.seen)) next.seen = [];
|
|
824
|
-
//
|
|
825
|
-
//
|
|
826
|
-
//
|
|
827
|
-
|
|
885
|
+
// History is never rewritten (2026-08-13, PARITY with agent-presence.mjs
|
|
886
|
+
// normalizeMessageDelivery). The old "conservatively reopen" migration
|
|
887
|
+
// deleted retiredAt from v2-retired messages, resurrecting a week of
|
|
888
|
+
// delivered notes and re-terminalizing the >24h ones as FAILED. A v2
|
|
889
|
+
// retirement proved model-context injection; it stays retired, recorded
|
|
890
|
+
// as exactly that and no more.
|
|
891
|
+
if (sourceVersion < MSG_DELIVERY_VERSION && next.retiredAt && !next.deadLetter && !next.retirement) {
|
|
892
|
+
next.retirement = { reason: 'v2-retired — model-context injection proven, explicit consumption unknown', at: Number(next.retiredAt) || now };
|
|
893
|
+
}
|
|
828
894
|
return next;
|
|
829
895
|
}
|
|
830
896
|
function msgDeliveryState(m, sid) {
|
|
@@ -852,6 +918,7 @@ function setMsgDelivery(m, sid, state, now, reason = null, actionId = '') {
|
|
|
852
918
|
if (actionId) r.offeredActionId = String(actionId).slice(0, 160);
|
|
853
919
|
delete r.acknowledgedAt; delete r.consumedAt;
|
|
854
920
|
delete r.acknowledgedActionId; delete r.consumedActionId;
|
|
921
|
+
delete r.consumedVia;
|
|
855
922
|
delete r.failedAt; delete r.reason;
|
|
856
923
|
} else if (state === 'acknowledged') {
|
|
857
924
|
if (!r.offerToken) r.offerToken = makeMsgOfferToken();
|
|
@@ -874,13 +941,25 @@ function setMsgDelivery(m, sid, state, now, reason = null, actionId = '') {
|
|
|
874
941
|
function terminalizeMsg(m, now, reason) {
|
|
875
942
|
const next = normalizeMsg(m, now);
|
|
876
943
|
const known = new Set([...(Array.isArray(next.candidateIds) ? next.candidateIds : []), ...next.deliveries.map(r => r.recipientId)].map(msgRecipientKey).filter(Boolean));
|
|
877
|
-
|
|
944
|
+
// Honest terminal split (2026-08-13, PARITY with agent-presence.mjs
|
|
945
|
+
// terminalizeMessage): 'acknowledged' reached model context on two
|
|
946
|
+
// independent actions — expiry after that is not a delivery failure.
|
|
947
|
+
// Only never-delivered (pending) and offered-once-unconfirmed records
|
|
948
|
+
// fail; an all-acknowledged message retires as delivered-unconfirmed.
|
|
949
|
+
let failures = 0, reachedUnconsumed = 0;
|
|
878
950
|
for (const id of known) {
|
|
879
|
-
|
|
880
|
-
|
|
951
|
+
const state = msgDeliveryState(next, id);
|
|
952
|
+
if (state === 'consumed') continue;
|
|
953
|
+
if (state === 'acknowledged') { reachedUnconsumed++; continue; }
|
|
954
|
+
failures++;
|
|
955
|
+
setMsgDelivery(next, id, 'failed', now, state === 'pending' ? `${reason} (never delivered)` : `${reason} (offered once, unconfirmed)`);
|
|
956
|
+
}
|
|
957
|
+
if (failures || known.size === 0) {
|
|
958
|
+
next.deadLetter = { state: 'failed', reason, at: now };
|
|
959
|
+
} else {
|
|
960
|
+
next.retiredAt = now;
|
|
961
|
+
if (reachedUnconsumed) next.retirement = { reason: `${reason} — after acknowledgement (delivered, consumption unconfirmed)`, at: now };
|
|
881
962
|
}
|
|
882
|
-
if (failed) next.deadLetter = { state: 'failed', reason, at: now };
|
|
883
|
-
else next.retiredAt = now;
|
|
884
963
|
return next;
|
|
885
964
|
}
|
|
886
965
|
const msgTerminal = (m) => Boolean(m?.deadLetter || m?.retiredAt);
|
|
@@ -1273,10 +1352,14 @@ function messageFooter(sid, tp, lib, actionId = messageActionId(readHookInput(),
|
|
|
1273
1352
|
const due = messages.filter(m => {
|
|
1274
1353
|
const state = msgDeliveryState(m, sid);
|
|
1275
1354
|
const record = msgDeliveryRecord(m, sid);
|
|
1355
|
+
// 'acknowledged' re-renders ONLY without action-identity evidence
|
|
1356
|
+
// of a third action; with evidence the lease pass below retires it
|
|
1357
|
+
// as consumed instead of injecting it a third time (PARITY with
|
|
1358
|
+
// agent-presence.mjs receiveMessages).
|
|
1276
1359
|
return m && !msgTerminal(m) && m.from !== sid
|
|
1277
|
-
&& ['pending', 'offered'
|
|
1360
|
+
&& (['pending', 'offered'].includes(state)
|
|
1361
|
+
|| (state === 'acknowledged' && (!actionId || !record?.acknowledgedActionId)))
|
|
1278
1362
|
&& !(state === 'offered' && actionId && record?.offeredActionId === String(actionId))
|
|
1279
|
-
&& !(state === 'acknowledged' && actionId && record?.acknowledgedActionId === String(actionId))
|
|
1280
1363
|
&& msgTargetsMe(m, me, sid, sessions);
|
|
1281
1364
|
}).sort((left, right) => {
|
|
1282
1365
|
// Fresh pending work wins the six-message context budget, followed
|
|
@@ -1320,13 +1403,29 @@ function messageFooter(sid, tp, lib, actionId = messageActionId(readHookInput(),
|
|
|
1320
1403
|
const prior = msgDeliveryState(m, sid);
|
|
1321
1404
|
if (prior === 'pending') setMsgDelivery(m, sid, 'offered', now, null, actionId);
|
|
1322
1405
|
else if (prior === 'offered' || prior === 'acknowledged') {
|
|
1323
|
-
// A later independent action acknowledges the offer.
|
|
1324
|
-
// actions keep replaying it and refresh only the action dedupe;
|
|
1325
|
-
// acknowledgement never retires a note in v3.
|
|
1406
|
+
// A later independent action acknowledges the offer.
|
|
1326
1407
|
setMsgDelivery(m, sid, 'acknowledged', now, null, actionId);
|
|
1327
1408
|
}
|
|
1328
1409
|
retireFullyConsumedMsg(m, now);
|
|
1329
1410
|
}
|
|
1411
|
+
// Lease auto-consume (2026-08-13, PARITY with agent-presence.mjs
|
|
1412
|
+
// receiveMessages): a note acknowledged on an EARLIER action is retired
|
|
1413
|
+
// as consumed (consumedVia 'auto-lease') when this later independent
|
|
1414
|
+
// action arrives, instead of replaying for 24h and dead-lettering as
|
|
1415
|
+
// "failed" a note the model saw repeatedly. The explicit
|
|
1416
|
+
// brain_message_receipt still records the stronger 'receipt' claim.
|
|
1417
|
+
if (actionId) {
|
|
1418
|
+
for (const m of messages) {
|
|
1419
|
+
if (msgTerminal(m) || m.from === sid) continue;
|
|
1420
|
+
const record = msgDeliveryRecord(m, sid);
|
|
1421
|
+
if (!record || record.state !== 'acknowledged') continue;
|
|
1422
|
+
if (!record.acknowledgedActionId || record.acknowledgedActionId === String(actionId)) continue;
|
|
1423
|
+
if (!msgTargetsMe(m, me, sid, sessions)) continue;
|
|
1424
|
+
setMsgDelivery(m, sid, 'consumed', now, null, actionId);
|
|
1425
|
+
record.consumedVia = 'auto-lease';
|
|
1426
|
+
retireFullyConsumedMsg(m, now);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1330
1429
|
writeLaneAtomic(JSON.stringify({ ...data, sessions, messages }));
|
|
1331
1430
|
} catch (error) {
|
|
1332
1431
|
const reason = String(error?.code || error?.message || 'lane-write-failed').slice(0, 100);
|
|
@@ -2531,28 +2630,35 @@ async function capture(lib) {
|
|
|
2531
2630
|
// cards into [Area] containers, and wires [[wikilink]] connections.
|
|
2532
2631
|
const doCapture = async (locked) => {
|
|
2533
2632
|
const merged = readState(); for (const k of seen) merged.add(k);
|
|
2633
|
+
// Own-batch snapshot BEFORE the drain merges queued peers' batches in:
|
|
2634
|
+
// if the brain write fails we re-queue only OUR contribution (drained
|
|
2635
|
+
// batches stay queued — ids/paths clear only after a durable write),
|
|
2636
|
+
// so nothing lands twice and nothing is lost.
|
|
2637
|
+
const ownCards = cards.slice(), ownResolutions = resolutions.slice(), ownUpdates = updates.slice();
|
|
2534
2638
|
if (!locked) {
|
|
2535
|
-
// REFUSED: never write from a stale base. The batch is queued
|
|
2536
|
-
// (
|
|
2537
|
-
//
|
|
2538
|
-
if
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2639
|
+
// REFUSED: never write from a stale base. The batch is queued
|
|
2640
|
+
// (lock-held RMW, falling back to an orphan sidecar) and drained by
|
|
2641
|
+
// the next capture — deferred-but-safe beats immediate-but-clobbering.
|
|
2642
|
+
// Baselines advance ONLY if the queue write really succeeded;
|
|
2643
|
+
// otherwise everything stays re-gatherable from the transcript.
|
|
2644
|
+
const queued = (cards.length || resolutions.length || updates.length)
|
|
2645
|
+
? queuePendingCapture({ id: `${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2)}`, ts: nowIso(), cards, resolutions, updates })
|
|
2646
|
+
: true;
|
|
2647
|
+
if (queued) {
|
|
2648
|
+
writeState(merged);
|
|
2649
|
+
writeLastCommit(newLastCommit); // safe: the batch itself is durably queued (verified above, not assumed)
|
|
2650
|
+
if (drainedShips && typeof lib.clearPendingShips === 'function') lib.clearPendingShips(CWD);
|
|
2651
|
+
advanceShipBaseline(lib);
|
|
2543
2652
|
}
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
if (drainedShips && typeof lib.clearPendingShips === 'function') lib.clearPendingShips(CWD);
|
|
2547
|
-
advanceShipBaseline(lib);
|
|
2548
|
-
appendJsonl(HEALTH, { ts: nowIso(), project: path.basename(CWD), mode: 'capture', ok: false, err: 'lock-timeout — batch QUEUED for the next capture; brain untouched' }, 500);
|
|
2549
|
-
process.stderr.write('[brain] capture deferred: the brain lock is held (desktop save or peer capture) — batch queued durably, nothing lost\n');
|
|
2653
|
+
appendJsonl(HEALTH, { ts: nowIso(), project: path.basename(CWD), mode: 'capture', ok: false, err: queued ? 'lock-timeout — batch QUEUED for the next capture; brain untouched' : 'lock-timeout AND queue write failed — nothing advanced; markers remain re-gatherable' }, 500);
|
|
2654
|
+
process.stderr.write(queued ? '[brain] capture deferred: the brain lock is held (desktop save or peer capture) — batch queued durably, nothing lost\n' : '[brain] capture deferred AND queueing failed — markers stay in the transcript for the next Stop\n');
|
|
2550
2655
|
return null;
|
|
2551
2656
|
}
|
|
2552
2657
|
// Drain the queue UNDER the lock: read, land, clear-by-id — a peer's
|
|
2553
2658
|
// batch queued after this read survives, and no batch lands twice.
|
|
2554
2659
|
const pendingBatches = readPendingCaptures();
|
|
2555
2660
|
const drainedPendingIds = new Set(pendingBatches.map(b => b && b.id).filter(Boolean));
|
|
2661
|
+
const drainedOrphanPaths = pendingBatches.map(b => b && b.__orphanPath).filter(Boolean);
|
|
2556
2662
|
for (const b of pendingBatches) {
|
|
2557
2663
|
if (!b || typeof b !== 'object') continue;
|
|
2558
2664
|
for (const c of (Array.isArray(b.cards) ? b.cards : [])) cards.push(c);
|
|
@@ -2594,6 +2700,7 @@ async function capture(lib) {
|
|
|
2594
2700
|
writeState(merged);
|
|
2595
2701
|
writeLastCommit(newLastCommit);
|
|
2596
2702
|
if (drainedPendingIds.size) updatePendingCaptures((current) => current.filter(b => b && !drainedPendingIds.has(b.id)));
|
|
2703
|
+
clearDrainedOrphans(drainedOrphanPaths);
|
|
2597
2704
|
if (drainedShips && typeof lib.clearPendingShips === 'function') lib.clearPendingShips(CWD);
|
|
2598
2705
|
advanceShipBaseline(lib);
|
|
2599
2706
|
return null;
|
|
@@ -2605,10 +2712,28 @@ async function capture(lib) {
|
|
|
2605
2712
|
});
|
|
2606
2713
|
// Re-pack the whole grid so a container that grew never overlaps its neighbor.
|
|
2607
2714
|
let out = res.buffer; try { out = (await lib.tidyBrain(res.buffer)).buffer; } catch { /* keep append result if tidy fails */ }
|
|
2608
|
-
|
|
2715
|
+
try {
|
|
2716
|
+
await lib.atomicWrite(BRAIN, out);
|
|
2717
|
+
} catch (e) {
|
|
2718
|
+
// The write failed even after atomicWrite's bounded EPERM backoff
|
|
2719
|
+
// (something held brain.klypix past ~2.7s — a desktop save, AV, an
|
|
2720
|
+
// indexer). Same discipline as a lock refusal: queue OUR OWN batch
|
|
2721
|
+
// durably (drained batches stay queued — their ids/paths were never
|
|
2722
|
+
// cleared), advance baselines only if that queue write succeeded,
|
|
2723
|
+
// and exit 0 by contract. Field case: the 2026-08-12 EPERM landed
|
|
2724
|
+
// on a session's FINAL Stop and its markers had nowhere to go.
|
|
2725
|
+
const queued = (ownCards.length || ownResolutions.length || ownUpdates.length)
|
|
2726
|
+
? queuePendingCapture({ id: `${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2)}`, ts: nowIso(), cards: ownCards, resolutions: ownResolutions, updates: ownUpdates })
|
|
2727
|
+
: true;
|
|
2728
|
+
if (queued) { writeState(merged); writeLastCommit(newLastCommit); }
|
|
2729
|
+
appendJsonl(HEALTH, { ts: nowIso(), project: path.basename(CWD), mode: 'capture', ok: false, err: `write failed (${e?.code || String(e?.message || e).slice(0, 80)}) — own batch ${queued ? 'QUEUED durably' : 'NOT queued; markers remain re-gatherable'}; drained batches remain queued` }, 500);
|
|
2730
|
+
process.stderr.write(`[brain] capture write failed (${e?.code || 'error'}) — ${queued ? 'batch queued durably, nothing lost' : 'queueing ALSO failed; markers stay in the transcript for the next Stop'}\n`);
|
|
2731
|
+
return null;
|
|
2732
|
+
}
|
|
2609
2733
|
writeState(merged);
|
|
2610
2734
|
writeLastCommit(newLastCommit); // advance the commit baseline only after a successful write
|
|
2611
2735
|
if (drainedPendingIds.size) updatePendingCaptures((current) => current.filter(b => b && !drainedPendingIds.has(b.id)));
|
|
2736
|
+
clearDrainedOrphans(drainedOrphanPaths);
|
|
2612
2737
|
// Same discipline for the two ship channels: the queue is consumed and
|
|
2613
2738
|
// the observation baseline advances ONLY now that the cards are durable.
|
|
2614
2739
|
if (drainedShips && typeof lib.clearPendingShips === 'function') lib.clearPendingShips(CWD);
|
package/src/klypix-core.mjs
CHANGED
|
@@ -171,12 +171,15 @@ const sha1 = (s) => crypto.createHash('sha1').update(s).digest('hex');
|
|
|
171
171
|
// "↩ superseded" variant AND the gardener's "⤵ consolidated" stamp), so
|
|
172
172
|
// cross-brain as_of was silently stricter than brain_ask as_of (2026-07-23).
|
|
173
173
|
|
|
174
|
-
// ── On-device cross-encoder reranker (brain_ask
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
174
|
+
// ── On-device cross-encoder reranker (brain_ask, opt-in experiment) ──────────
|
|
175
|
+
// RETIRED as a default after the 2026-08-10 harness fix. The "recall@5 15%→40%"
|
|
176
|
+
// that once justified it here was measured in a vector space the product does
|
|
177
|
+
// not use (mean-pooled, unprefixed queries — the broken pre-fix harness).
|
|
178
|
+
// Re-measured with the PRODUCTION embedder on the frozen human-paraphrase set:
|
|
179
|
+
// the single BGE pass scores recall@5 30% / MRR 0.22; adding the reranker DROPS
|
|
180
|
+
// that to 25% / 0.167 and costs ~3.5s/query. It still scores (question, card)
|
|
181
|
+
// pairs jointly with full token interaction; keep it reachable for experiments
|
|
182
|
+
// via KLYPIX_RERANK=1 (opt-IN — off by default; see the gate below).
|
|
180
183
|
|
|
181
184
|
// ── small block helpers ──────────────────────────────────────────────────────
|
|
182
185
|
const text = (t) => ({ kind: 'text', text: t });
|
|
@@ -523,11 +526,23 @@ export async function opBrainAsk({ vault, canvas, question, as_of, k = 10, log =
|
|
|
523
526
|
const vecs = await vectorsForBrain(pipe, t.file, struct.cards);
|
|
524
527
|
if (qv && vecs && vecs.size) { semantic = new Map(); for (const [id, v] of vecs) semantic.set(id, dot(qv, v)); cardVecs = vecs; mode = 'semantic+lexical (on-device)'; }
|
|
525
528
|
}
|
|
526
|
-
} catch { semantic = null; cardVecs = null;
|
|
529
|
+
} catch { semantic = null; cardVecs = null; }
|
|
530
|
+
// Parity with search_all_brains (8445e9c): never say "warming — retry" to a
|
|
531
|
+
// host with no runtime installed — for them that advice is wrong forever and
|
|
532
|
+
// their results are the measured recall@5 = 0% lexical path. The full notice
|
|
533
|
+
// is a SEPARATE advisory line: gluing three sentences and a filesystem path
|
|
534
|
+
// into the "N matched, X ranking" header garbled every lexical-only answer
|
|
535
|
+
// (2026-08-14 review), and later "+ rerank"/"+ status-mode" suffixes would
|
|
536
|
+
// have landed mid-sentence.
|
|
537
|
+
let fallbackNotice = null;
|
|
538
|
+
if (!semantic) {
|
|
539
|
+
fallbackNotice = semanticFallbackNotice(false);
|
|
540
|
+
if (fallbackNotice) mode = fallbackNotice.startsWith('LEXICAL ONLY') ? 'lexical-only (no semantic runtime)' : 'lexical (semantic warming)';
|
|
541
|
+
}
|
|
527
542
|
const kk = Math.max(1, Math.min(20, k || 10));
|
|
528
543
|
const timeTravel = asOfTs != null;
|
|
529
544
|
// Cross-encoder rerank is OPT-IN. On the frozen human/paraphrase set it made
|
|
530
|
-
// BGE top-5 recall worse (
|
|
545
|
+
// BGE top-5 recall worse (30%→25%) and added ~3.5s/query. Keep the reversible
|
|
531
546
|
// escape hatch for experiments, but the best measured experience is the single
|
|
532
547
|
// BGE pass. Suppressed under as_of either way.
|
|
533
548
|
const wantRerank = !timeTravel && process.env.KLYPIX_RERANK === '1';
|
|
@@ -558,7 +573,8 @@ export async function opBrainAsk({ vault, canvas, question, as_of, k = 10, log =
|
|
|
558
573
|
if (result.statusStrong && !timeTravel) {
|
|
559
574
|
try { statusMd = statusContextToMarkdown(struct); mode += ' + status-mode'; } catch { statusMd = ''; }
|
|
560
575
|
}
|
|
561
|
-
|
|
576
|
+
const noticeMd = fallbackNotice ? `> ${fallbackNotice}\n\n` : '';
|
|
577
|
+
return { blocks: [text(stamp + noticeMd + statusMd + questionContextToMarkdown(q, result, { mode, as_of: timeTravel ? as_of : null }))] };
|
|
562
578
|
}
|
|
563
579
|
|
|
564
580
|
// ── brain_challenge — the adversarial brain ───────────────────────────────────
|
package/src/klypix-format.mjs
CHANGED
|
@@ -218,9 +218,25 @@ export async function atomicWrite(filePath, buf, opts = {}) {
|
|
|
218
218
|
}
|
|
219
219
|
const tmp = filePath + '.tmp-' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
220
220
|
fs.writeFileSync(tmp, buf);
|
|
221
|
-
|
|
222
|
-
|
|
221
|
+
// Windows rename-over-open-destination throws EPERM/EBUSY/EACCES while
|
|
222
|
+
// another process (a desktop save/read, AV, an indexer) briefly holds the
|
|
223
|
+
// target — three real capture failures in the field (.hook-health.jsonl,
|
|
224
|
+
// 2026-08-11/12). A bounded backoff (~2.7s total) outlasts a transient
|
|
225
|
+
// hold; a persistent holder still throws so callers can queue the batch —
|
|
226
|
+
// delayed is acceptable, lost is not.
|
|
227
|
+
for (let attempt = 0; ; attempt++) {
|
|
228
|
+
try { fs.renameSync(tmp, filePath); break; } // Node uses MoveFileEx(REPLACE_EXISTING) on Windows → overwrites atomically
|
|
229
|
+
catch (e) {
|
|
230
|
+
if (attempt >= RENAME_BACKOFF_MS.length || !RENAME_RETRYABLE_CODES.has(e?.code)) {
|
|
231
|
+
try { fs.rmSync(tmp); } catch { /* */ }
|
|
232
|
+
throw e;
|
|
233
|
+
}
|
|
234
|
+
await new Promise((resolve) => setTimeout(resolve, RENAME_BACKOFF_MS[attempt]));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
223
237
|
}
|
|
238
|
+
const RENAME_RETRYABLE_CODES = new Set(['EPERM', 'EBUSY', 'EACCES']);
|
|
239
|
+
const RENAME_BACKOFF_MS = [40, 120, 300, 700, 1500];
|
|
224
240
|
|
|
225
241
|
// ── verify: suffix (decay-aware status, 2026-07-28 post-mortem) ──────────────
|
|
226
242
|
// A card may carry the EXACT live-probe command for its fast-decay claim as a
|
package/src/mcp-presence.mjs
CHANGED
|
@@ -1589,6 +1589,11 @@ export function createMcpPresence({
|
|
|
1589
1589
|
hostIdentityPrepared: details.hostIdentityPrepared === true,
|
|
1590
1590
|
clientInfoPrepared: details.clientInfoPrepared || null,
|
|
1591
1591
|
branchPrepared: details.branchPrepared,
|
|
1592
|
+
// Forward the action identity (2026-08-14 review): without it, every
|
|
1593
|
+
// server instance's FIRST sync advanced delivery receipts with
|
|
1594
|
+
// actionId '' — records got no offered/acknowledgedActionId, breaking
|
|
1595
|
+
// same-action dedupe and the lease's third-action evidence.
|
|
1596
|
+
actionId: details.actionId || '',
|
|
1592
1597
|
});
|
|
1593
1598
|
// Consume the write verdict (1.52.0 plumbed it; nothing read it): a
|
|
1594
1599
|
// contended lane skips the write, and ~3 skipped heartbeats in a row used
|
|
@@ -2157,9 +2162,9 @@ export function createMcpPresence({
|
|
|
2157
2162
|
delivery: {
|
|
2158
2163
|
proactive: 'mcp-logging-best-effort-preview',
|
|
2159
2164
|
modelContext: shouldDeliverMessages ? 'supported-klypix-action' : 'deferred',
|
|
2160
|
-
stateMachine: 'pending -> offered -> acknowledged -> consumed | failed',
|
|
2161
|
-
acknowledgement: 'a later independent supported action followed an offer; explicit token-bound brain_message_receipt records
|
|
2162
|
-
retention: 'machine-local; 24h TTL and bounded lane capacity
|
|
2165
|
+
stateMachine: 'pending -> offered -> acknowledged -> consumed (auto-lease on the next independent action, or explicit receipt) | failed',
|
|
2166
|
+
acknowledgement: 'a later independent supported action followed an offer; a further independent action auto-consumes the lease, and explicit token-bound brain_message_receipt records the stronger acted-on-it claim',
|
|
2167
|
+
retention: 'machine-local; 24h TTL and bounded lane capacity; expiry fails only never-delivered or offered-once records — an acknowledged note retires as delivered-unconfirmed, never as failed',
|
|
2163
2168
|
writeOk: shouldDeliverMessages ? report.deliveryWriteOk : null,
|
|
2164
2169
|
...(report.deliveryWriteSkippedReason ? { writeFailure: report.deliveryWriteSkippedReason } : {}),
|
|
2165
2170
|
},
|