klypix-mcp 1.73.2 → 1.75.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.
@@ -223,12 +223,16 @@ try {
223
223
  offeredAt: now - 900,
224
224
  acknowledgedAt: now - 500,
225
225
  consumedAt: now - 250,
226
+ // The STRONG claim: this peer sent a real receipt. Without this marker the
227
+ // record is an auto-lease or unknown, and the renderer must not say
228
+ // "explicitly" — the conformance fixture has to state which it means.
229
+ consumedVia: 'receipt',
226
230
  }],
227
231
  seen: ['finding-owner'],
228
232
  }],
229
233
  sessions: lane, selfId: 'finding-sender', now,
230
234
  });
231
- checks.findingReceiptRendered = /explicitly consumed by all 1 target peer\(s\) after model-context delivery \(not human-read\)/.test(renderReceiptSummary(receipt));
235
+ checks.findingReceiptRendered = /explicitly consumed by all 1 target peer\(s\) via receipt \(not human-read\)/.test(renderReceiptSummary(receipt));
232
236
  }
233
237
 
234
238
  // ── Cross-PC presence: simulated two-machine scenario ─────────────────────
@@ -562,7 +562,7 @@ server.registerTool('project_map_scan', {
562
562
 
563
563
  server.registerTool('project_map_drift', {
564
564
  title: 'Check brain cards against the repo\'s real files (drift report)',
565
- description: 'Read-only drift check: every brain card that references THIS project\'s files is verified against the working tree. Reports cards whose referenced files are gone or moved (with rename candidates by unique basename), and headlines when the checkout itself is behind its origin default branch — in that state a "missing" file may simply not be in this checkout. Slash-joined name enumerations and other-project paths are recognized and skipped, not reported as drift. Nothing is written; fix cards with a CORRECTION marker or by editing them in KLYPIX.',
565
+ description: 'Read-only drift check: every brain card that references THIS project\'s files is verified against the working tree. Reports cards whose referenced files are gone or moved (with rename candidates by unique basename), and headlines the checkout\'s own position against its origin default branch — reporting BOTH ahead and behind, so a DIVERGED checkout is named as diverged rather than merely behind. In either state a "missing" file may simply not be in this checkout. Slash-joined name enumerations and other-project paths are recognized and skipped, not reported as drift. Nothing is written; fix cards with a CORRECTION marker or by editing them in KLYPIX.',
566
566
  annotations: { destructiveHint: false, idempotentHint: true, openWorldHint: false },
567
567
  inputSchema: {
568
568
  project: z.string().optional().describe('Absolute project root. Defaults to this MCP connection\'s configured project/vault.'),
@@ -691,7 +691,7 @@ server.registerTool('brain_note', {
691
691
 
692
692
  server.registerTool('brain_message', {
693
693
  title: 'Message the other live agent sessions on this project (one-time note, not a brain card)',
694
- description: 'Leave a DELIBERATE, targeted note for the OTHER active agent sessions working on this project right now ("merged the hook refactor — rebase before you commit", "don\'t touch canvasStore, mid-refactor"). Any MCP client can send and receive through the shared machine-local presence lane. A supported lifecycle event or KLYPIX tool result offers the note into model-visible context; a later independent supported action acknowledges that offer. Pending/offered notes replay after reconnect, while expiry or capacity loss leaves a failed per-recipient receipt instead of silently disappearing. Acknowledged means a later action followed the offer — it is NOT proof a human read it. Delivery remains OS-user-local, machine-local, bounded by a 24h TTL, and unavailable to a peer that never takes a supported action. Ephemeral and NOT persisted to the brain — for a durable decision use brain_note instead.',
694
+ description: 'Leave a DELIBERATE, targeted note for the OTHER active agent sessions working on this project right now ("merged the hook refactor — rebase before you commit", "don\'t touch canvasStore, mid-refactor"). Any MCP client can send and receive through the shared machine-local presence lane. A supported lifecycle event or KLYPIX tool result offers the note into model-visible context; a later independent supported action acknowledges that offer. Pending/offered notes replay after reconnect, while expiry or capacity loss leaves a failed per-recipient receipt instead of silently disappearing. Acknowledged means a later action followed the offer — it is NOT proof a human read it. A note then retires either by an explicit brain_message_receipt ("acted on it") or by AUTO-CONSUMPTION on a further independent action, with no receipt; your receipt line names which, and auto-consumption evidences activity, not uptake. Delivery remains OS-user-local, machine-local, bounded by a 24h TTL, and unavailable to a peer that never takes a supported action. Ephemeral and NOT persisted to the brain — for a durable decision use brain_note instead.',
695
695
  inputSchema: {
696
696
  text: z.string().describe('The note to deliver (kept to 400 chars).'),
697
697
  to: z.string().optional().describe('Target hint — a peer session id-prefix or branch name; omit or "all" for every live session.'),
@@ -790,10 +790,15 @@ server.registerTool('brain_sync', {
790
790
  releaseIntent: z.object({
791
791
  version: z.string().max(64).describe('The version this session intends to release (e.g. "1.70.0").'),
792
792
  ref: z.string().max(200).describe('The git ref (branch or tag) the release will be cut from.'),
793
- acknowledge: z.array(z.string().max(40)).max(64).optional().describe('Commit shas this release DELIBERATELY leaves behind. Only needed after a refusal: if the ref would drop finished work, the lease is refused and the response names every sha. Re-declare with those shas here to proceed — and tell the user what they are first.'),
794
- }).optional().describe('Declare EXCLUSIVE intent to prepare a release of this project. The first declarer takes a ~2h lease (refreshed by checkpoints, freed by phase "complete", by expiry, or when the holder session ends); a second declarer gets a structured hard conflict naming the holder, version, and ref. While any lease is active every peer\'s sync gains a "release in preparation" footer line. A NEW declaration is also checked against what the release would LEAVE BEHIND: if the ref is missing commits that are on trunk or on a branch a live peer session is working on, the lease is REFUSED (nothing is changed) and the response lists them — report those commits to the user, then re-declare with acknowledge:[...] naming each sha if the release should go ahead without them.'),
793
+ acknowledge: z.array(z.string().max(40)).max(1024).optional().describe('Commit shas this release DELIBERATELY leaves behind. Only needed after a refusal: if the ref would drop finished work, the lease is refused and the response names every sha. Re-declare with those shas here to proceed — and tell the user what they are first.'),
794
+ }).optional().describe('Declare EXCLUSIVE intent to prepare a release of this project. The first declarer takes a ~2h lease (refreshed by checkpoints, freed by phase "complete", by expiry, or when the holder session ends); a second declarer gets a structured hard conflict naming the holder, version, and ref. While any lease is active every peer\'s sync gains a "release in preparation" footer line. A NEW declaration is also checked against what the release would LEAVE BEHIND: if the ref is missing commits that are on trunk or on a branch a live peer session is working on, OR commits any session STAKED a releaseClaim on (even one that has since ended), the lease is REFUSED (nothing is changed) and the response lists them — report those commits to the user, then re-declare with acknowledge:[...] naming each sha if the release should go ahead without them. Acknowledging away a claimed or live-owned commit queues its owner a notification automatically.'),
795
+ releaseClaim: z.object({
796
+ shas: z.array(z.string().max(40)).max(20).optional().describe('Commit shas that MUST ride the next release. Stake after committing work a user was promised — the claim OUTLIVES this session (14d), and every future releaseIntent must contain these commits or acknowledge them by name.'),
797
+ note: z.string().max(160).optional().describe('One line of why — shown verbatim in any refusal that names this claim ("founder was told the Arrow tool ships in the next build").'),
798
+ withdraw: z.union([z.array(z.string().max(40)).max(20), z.boolean()]).optional().describe('Shas to withdraw from this session\'s claim; [] or true withdraws the whole claim. Only the staking session (or its logical continuation) can withdraw.'),
799
+ }).optional().describe('Stake a durable claim that specific commits ride the NEXT release — the promise "you\'ll see it in the next build" made machine-readable. Unlike presence rows (which age out ~10min after a session ends), a claim persists until fulfilled (the release ref contains the shas — auto-retired with a courtesy note), withdrawn, or expired (14d). A release that would drop claimed shas is REFUSED until they are acknowledged BY NAME, and acknowledging them away notifies the owner. Use exactly one of shas (stake/extend) or withdraw.'),
795
800
  },
796
- }, async ({ project, intent, files, phase, include_context, results, releaseIntent }, extra) => {
801
+ }, async ({ project, intent, files, phase, include_context, results, releaseIntent, releaseClaim }, extra) => {
797
802
  const totalStartedAt = Date.now();
798
803
  const report = mcpPresence.sync({
799
804
  project,
@@ -802,6 +807,7 @@ server.registerTool('brain_sync', {
802
807
  phase,
803
808
  results,
804
809
  releaseIntent,
810
+ releaseClaim,
805
811
  deliverMessages: include_context !== false,
806
812
  actionId: extra?.klypixRequestIdentity?.actionId || '',
807
813
  preflight: extra?.klypixBrainSyncPreflight,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klypix-mcp",
3
- "version": "1.73.2",
3
+ "version": "1.75.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/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/install-rename-backoff.mjs && node test/project-binding-rebind.mjs && node test/context-gateway.mjs && node test/repo-state.mjs && node test/released-tag-guard.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/orphan-gardener.mjs && node test/brief-and-recall.mjs && node test/layout-cluster.mjs && node test/brain-ask.mjs && node test/retrieval-fusion.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/undeclared-active.mjs && node test/presence-liveness.mjs && node test/observed-scope.mjs && node test/release-lease.mjs && node test/release-ancestry.mjs && node test/release-handshake.mjs && node test/completion-guard.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/one-command-setup.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/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/install-rename-backoff.mjs && node test/project-binding-rebind.mjs && node test/context-gateway.mjs && node test/repo-state.mjs && node test/released-tag-guard.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/orphan-gardener.mjs && node test/brief-and-recall.mjs && node test/layout-cluster.mjs && node test/brain-ask.mjs && node test/retrieval-fusion.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/undeclared-active.mjs && node test/presence-liveness.mjs && node test/observed-scope.mjs && node test/release-lease.mjs && node test/release-ancestry.mjs && node test/release-claim-join.mjs && node test/release-claims.mjs && node test/release-handshake.mjs && node test/completion-guard.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/one-command-setup.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"
@@ -1235,6 +1235,9 @@ function normalizeReleaseLease(raw) {
1235
1235
  const refreshedAt = Number(raw.refreshedAt || raw.takenAt || 0);
1236
1236
  if (!holderId || !version || !ref || !takenAt || !refreshedAt) return null;
1237
1237
  const ttlMs = Math.max(60_000, Number(raw.ttlMs) || RELEASE_LEASE_TTL_MS);
1238
+ const acknowledgedShas = [...new Set((Array.isArray(raw.acknowledgedShas) ? raw.acknowledgedShas : [])
1239
+ .map((x) => String(x || '').trim().toLowerCase())
1240
+ .filter((x) => /^[0-9a-f]{4,40}$/.test(x)))].slice(0, 2048);
1238
1241
  return {
1239
1242
  holderId,
1240
1243
  holderClient: String(raw.holderClient || 'unknown').slice(0, 40),
@@ -1244,6 +1247,7 @@ function normalizeReleaseLease(raw) {
1244
1247
  refreshedAt,
1245
1248
  ttlMs,
1246
1249
  expiresAt: refreshedAt + ttlMs,
1250
+ ...(acknowledgedShas.length ? { acknowledgedShas } : {}),
1247
1251
  };
1248
1252
  }
1249
1253
 
@@ -1327,6 +1331,14 @@ export function declareReleaseLease({
1327
1331
  home,
1328
1332
  now = Date.now(),
1329
1333
  ttlMs = RELEASE_LEASE_TTL_MS,
1334
+ // Shas this declaration acknowledged away (ancestry + claims). PERSISTED on
1335
+ // the lease so a holder's refresh does not have to re-litigate them: without
1336
+ // this, the natural refresh pattern — keep attaching the same releaseIntent —
1337
+ // was refused on the very claim the holder had already acknowledged, the
1338
+ // else-chain skipped the refresh, and the lease starved mid-build: the exact
1339
+ // failure the lease-loss warning exists for, rebuilt by the claims gate
1340
+ // (2026-08-17 review blocker B1, found independently by all four lenses).
1341
+ acknowledgedShas = [],
1330
1342
  }) {
1331
1343
  const holderId = recipientKey(sessionId);
1332
1344
  const nextVersion = releaseLeaseVersionKey(version);
@@ -1338,6 +1350,14 @@ export function declareReleaseLease({
1338
1350
  return { write: false, outcome: { ok: false, status: 'conflict', holder: verdict.lease } };
1339
1351
  }
1340
1352
  const stillMine = mine && verdict?.status === 'active';
1353
+ // Union with what the held lease already acknowledged — a re-declare with a
1354
+ // NEW acknowledgement must extend the record, never erase the history that
1355
+ // keeps the holder's refresh from re-refusing.
1356
+ const priorAcks = stillMine && Array.isArray(verdict.lease.acknowledgedShas)
1357
+ ? verdict.lease.acknowledgedShas : [];
1358
+ const acks = [...new Set([...priorAcks, ...(Array.isArray(acknowledgedShas) ? acknowledgedShas : [])]
1359
+ .map((x) => String(x || '').trim().toLowerCase())
1360
+ .filter((x) => /^[0-9a-f]{4,40}$/.test(x)))].slice(0, 2048);
1341
1361
  const stored = {
1342
1362
  schemaVersion: 1,
1343
1363
  holderId,
@@ -1347,6 +1367,7 @@ export function declareReleaseLease({
1347
1367
  takenAt: stillMine ? verdict.lease.takenAt : now,
1348
1368
  refreshedAt: now,
1349
1369
  ttlMs: Math.max(60_000, Number(ttlMs) || RELEASE_LEASE_TTL_MS),
1370
+ ...(acks.length ? { acknowledgedShas: acks } : {}),
1350
1371
  };
1351
1372
  const reclaimed = !stillMine && verdict ? verdict.status : null;
1352
1373
  return {
@@ -1430,6 +1451,191 @@ export function freeReleaseLease({ brainPath, sessionId, home, now = Date.now()
1430
1451
  } });
1431
1452
  }
1432
1453
 
1454
+ // ── Release claims — "my commits ride the next build", surviving session exit ─
1455
+ //
1456
+ // The presence half of the release gate dies with its session: rows age out in
1457
+ // ~10 minutes, after which a dropped commit reverts to an anonymous sha, and a
1458
+ // commit on a branch NO live session is on never enters the ancestry comparison
1459
+ // at all. Field shape (2026-08-17, founder-surfaced): a session promises "you'll
1460
+ // see it in the next build", closes, and the next release passes every gate
1461
+ // while silently leaving that work behind — the exact v1.3.120 failure, one
1462
+ // session-lifetime later. A claim is that promise made DURABLE: shas + owner +
1463
+ // note, persisted in the lane, checked against EVERY releaseIntent ref until it
1464
+ // is fulfilled, withdrawn, or expires.
1465
+ //
1466
+ // Deliberately NOT a lease: claims never conflict with each other, any number
1467
+ // may coexist, and fulfilment is mechanical (the shas are contained in the
1468
+ // release ref). Same identity model as the lease (a claim survives id rotation
1469
+ // via the logical-session match), same atomic lane write, same fail-loud
1470
+ // bounding — a store the gate depends on must never silently drop an entry.
1471
+ export const RELEASE_CLAIM_TTL_MS = 14 * 24 * 60 * 60 * 1000;
1472
+ export const RELEASE_CLAIMS_MAX = 32;
1473
+ export const RELEASE_CLAIM_SHAS_MAX = 20;
1474
+
1475
+ const claimSha = (value) => {
1476
+ const s = String(value || '').trim().toLowerCase();
1477
+ return /^[0-9a-f]{4,40}$/.test(s) ? s : null;
1478
+ };
1479
+
1480
+ function normalizeReleaseClaim(raw, now = Date.now()) {
1481
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
1482
+ const ownerId = recipientKey(raw.ownerId);
1483
+ const shas = [...new Set((Array.isArray(raw.shas) ? raw.shas : []).map(claimSha).filter(Boolean))]
1484
+ .slice(0, RELEASE_CLAIM_SHAS_MAX);
1485
+ const stakedAt = Number(raw.stakedAt || 0);
1486
+ if (!ownerId || !shas.length || !stakedAt) return null;
1487
+ const expiresAt = Number(raw.expiresAt || 0) || (stakedAt + RELEASE_CLAIM_TTL_MS);
1488
+ if (now >= expiresAt) return null;
1489
+ return {
1490
+ ownerId,
1491
+ ownerClient: String(raw.ownerClient || 'unknown').slice(0, 40),
1492
+ ownerLogicalId: recipientKey(raw.ownerLogicalId) || null,
1493
+ branch: String(raw.branch || '').slice(0, 120) || null,
1494
+ note: String(raw.note || '').replace(/\s+/g, ' ').trim().slice(0, 160) || null,
1495
+ shas,
1496
+ stakedAt,
1497
+ expiresAt,
1498
+ };
1499
+ }
1500
+
1501
+ const liveReleaseClaims = (raw, now = Date.now()) => (Array.isArray(raw) ? raw : [])
1502
+ .map((c) => normalizeReleaseClaim(c, now))
1503
+ .filter(Boolean);
1504
+
1505
+ // A claim belongs to the caller when any identity in the caller's set matches
1506
+ // the identity the claim recorded — the same rotation-tolerant rule the lease
1507
+ // uses, because "the same conversation after /clear" must still own its claim.
1508
+ const sessionOwnsClaim = (claim, sessions, callerId) => {
1509
+ const key = recipientKey(callerId);
1510
+ if (!key || !claim) return false;
1511
+ if (claim.ownerId === key || claim.ownerLogicalId === key) return true;
1512
+ const me = (Array.isArray(sessions) ? sessions : [])
1513
+ .find((s) => recipientKey(s.id) === key) || null;
1514
+ if (!me) return false;
1515
+ const mine = new Set([recipientKey(me.id), recipientKey(me.logicalSessionId),
1516
+ ...normalizeAliases(me.aliases).map(recipientKey)].filter(Boolean));
1517
+ return mine.has(claim.ownerId) || (claim.ownerLogicalId && mine.has(claim.ownerLogicalId));
1518
+ };
1519
+
1520
+ function mutateReleaseClaims({ brainPath, home, now, mutate }) {
1521
+ if (!brainPath) return { ok: false, status: 'no-brain' };
1522
+ const laneFile = laneFileFor(brainPath, home);
1523
+ const lockFile = laneFile + '.lock';
1524
+ if (!acquireLock(lockFile)) return { ok: false, status: 'lane-locked' };
1525
+ try {
1526
+ const laneRead = readMutableLane(laneFile);
1527
+ if (!laneRead.ok) return { ok: false, status: laneRead.reason };
1528
+ const data = laneRead.data;
1529
+ const sessions = pruneSessions(data.sessions, now);
1530
+ const claims = liveReleaseClaims(data.releaseClaims, now);
1531
+ const result = mutate({ data, sessions, claims });
1532
+ if (result.write) {
1533
+ const next = { ...data };
1534
+ if (!result.claims || !result.claims.length) delete next.releaseClaims;
1535
+ else next.releaseClaims = result.claims;
1536
+ fs.mkdirSync(path.dirname(laneFile), { recursive: true });
1537
+ writeLaneFileAtomic(laneFile, JSON.stringify(next));
1538
+ }
1539
+ return result.outcome;
1540
+ } finally {
1541
+ releaseLock(lockFile);
1542
+ }
1543
+ }
1544
+
1545
+ // Stake (or extend) this session's claim. One claim per owner: re-staking
1546
+ // UNIONS the shas and refreshes note/expiry, so a session adding a second
1547
+ // commit does not spawn a second entry. The lane bound fails LOUD — silently
1548
+ // dropping a claim the gate depends on is the 1.74.0 livelock lesson applied
1549
+ // to a different store.
1550
+ export function stakeReleaseClaim({ brainPath, sessionId, client, logicalSessionId, branch, shas, note, home, now = Date.now() }) {
1551
+ const ownerId = recipientKey(sessionId);
1552
+ const cleanShas = [...new Set((Array.isArray(shas) ? shas : []).map(claimSha).filter(Boolean))];
1553
+ if (!ownerId) return { ok: false, status: 'no-session' };
1554
+ if (!cleanShas.length) return { ok: false, status: 'no-valid-shas' };
1555
+ if (cleanShas.length > RELEASE_CLAIM_SHAS_MAX) {
1556
+ return { ok: false, status: 'too-many-shas', limit: RELEASE_CLAIM_SHAS_MAX, given: cleanShas.length };
1557
+ }
1558
+ return mutateReleaseClaims({ brainPath, home, now, mutate: ({ sessions, claims }) => {
1559
+ const mineIndex = claims.findIndex((c) => sessionOwnsClaim(c, sessions, ownerId));
1560
+ if (mineIndex < 0 && claims.length >= RELEASE_CLAIMS_MAX) {
1561
+ return { write: false, outcome: { ok: false, status: 'claims-full', limit: RELEASE_CLAIMS_MAX } };
1562
+ }
1563
+ const existing = mineIndex >= 0 ? claims[mineIndex] : null;
1564
+ // The union must never silently shed a promised sha (review blocker B2 —
1565
+ // executed: stake 15 + extend 10 reported 'extended' while 5 promised shas
1566
+ // vanished; the v1.3.120 silent-drop class recurring inside its own fix).
1567
+ const unionSize = new Set([...(existing?.shas || []), ...cleanShas]).size;
1568
+ if (unionSize > RELEASE_CLAIM_SHAS_MAX) {
1569
+ return { write: false, outcome: {
1570
+ ok: false, status: 'claim-would-overflow',
1571
+ limit: RELEASE_CLAIM_SHAS_MAX, existing: (existing?.shas || []).length, adding: cleanShas.length,
1572
+ } };
1573
+ }
1574
+ const merged = {
1575
+ ownerId,
1576
+ ownerClient: String(client || existing?.ownerClient || 'unknown').slice(0, 40),
1577
+ ownerLogicalId: recipientKey(logicalSessionId) || existing?.ownerLogicalId || null,
1578
+ branch: String(branch || existing?.branch || '').slice(0, 120) || null,
1579
+ note: String(note || existing?.note || '').replace(/\s+/g, ' ').trim().slice(0, 160) || null,
1580
+ shas: [...new Set([...(existing?.shas || []), ...cleanShas])],
1581
+ stakedAt: existing?.stakedAt || now,
1582
+ expiresAt: now + RELEASE_CLAIM_TTL_MS,
1583
+ };
1584
+ const next = mineIndex >= 0
1585
+ ? claims.map((c, i) => (i === mineIndex ? merged : c))
1586
+ : [...claims, merged];
1587
+ return { write: true, claims: next, outcome: { ok: true, status: mineIndex >= 0 ? 'extended' : 'staked', claim: merged } };
1588
+ } });
1589
+ }
1590
+
1591
+ // Withdraw shas from this session's claim (empty/omitted shas = the whole
1592
+ // claim). Only the owner's identity set can withdraw — a releasing session
1593
+ // must acknowledge a foreign claim through the gate, never delete it.
1594
+ export function withdrawReleaseClaim({ brainPath, sessionId, shas, home, now = Date.now() }) {
1595
+ const ownerId = recipientKey(sessionId);
1596
+ if (!ownerId) return { ok: false, status: 'no-session' };
1597
+ const drop = new Set((Array.isArray(shas) ? shas : []).map(claimSha).filter(Boolean));
1598
+ return mutateReleaseClaims({ brainPath, home, now, mutate: ({ sessions, claims }) => {
1599
+ const mineIndex = claims.findIndex((c) => sessionOwnsClaim(c, sessions, ownerId));
1600
+ if (mineIndex < 0) return { write: false, outcome: { ok: true, status: 'no-claim' } };
1601
+ const mine = claims[mineIndex];
1602
+ const kept = drop.size
1603
+ ? mine.shas.filter((s) => ![...drop].some((d) => s.startsWith(d) || d.startsWith(s)))
1604
+ : [];
1605
+ const next = kept.length
1606
+ ? claims.map((c, i) => (i === mineIndex ? { ...mine, shas: kept } : c))
1607
+ : claims.filter((_, i) => i !== mineIndex);
1608
+ return { write: true, claims: next, outcome: { ok: true, status: kept.length ? 'trimmed' : 'withdrawn', remaining: kept.length } };
1609
+ } });
1610
+ }
1611
+
1612
+ // Retire specific (claim owner, shas) pairs after a release FULFILLS them —
1613
+ // called by the gateway on a granted lease whose ref contains the shas. Whole
1614
+ // claims disappear only when every sha they carry is fulfilled.
1615
+ export function retireFulfilledClaims({ brainPath, fulfilled, home, now = Date.now() }) {
1616
+ const byOwner = new Map((Array.isArray(fulfilled) ? fulfilled : [])
1617
+ .map((f) => [recipientKey(f?.ownerId), new Set((f?.shas || []).map(claimSha).filter(Boolean))]));
1618
+ if (!byOwner.size) return { ok: true, status: 'nothing-to-retire' };
1619
+ return mutateReleaseClaims({ brainPath, home, now, mutate: ({ claims }) => {
1620
+ let changed = false;
1621
+ const next = claims.map((c) => {
1622
+ const done = byOwner.get(c.ownerId);
1623
+ if (!done || !done.size) return c;
1624
+ const kept = c.shas.filter((s) => !done.has(s));
1625
+ if (kept.length !== c.shas.length) changed = true;
1626
+ return kept.length ? { ...c, shas: kept } : null;
1627
+ }).filter(Boolean);
1628
+ if (!changed) return { write: false, outcome: { ok: true, status: 'nothing-to-retire' } };
1629
+ return { write: true, claims: next, outcome: { ok: true, status: 'retired' } };
1630
+ } });
1631
+ }
1632
+
1633
+ // Read-only: live claims. Lock-free like every other read surface.
1634
+ export function readReleaseClaims({ brainPath, home, now = Date.now() } = {}) {
1635
+ if (!brainPath) return [];
1636
+ return liveReleaseClaims(readLane(laneFileFor(brainPath, home)).releaseClaims, now);
1637
+ }
1638
+
1433
1639
  // A long-lived MCP worker can observe SessionEnd(A) and then receive the first
1434
1640
  // request for a brand-new Codex thread B. That is rotation, not a rekey: A's
1435
1641
  // tombstone, scope, message audience/receipts, and authorship must remain bound
@@ -2200,6 +2406,15 @@ export function postPresenceMessage({
2200
2406
  dedupeKey = '',
2201
2407
  home,
2202
2408
  now = Date.now(),
2409
+ // Machine-addressed notifications only (claim owners, ancestry owners): the
2410
+ // exact recipient id is KNOWN, and the recipient being offline is the very
2411
+ // case the notification exists for — a staked claim outlives its session, so
2412
+ // its owner may be gone when the release that drops their work is declared.
2413
+ // The message waits directed in the lane (24h TTL, sender-visible dead-letter
2414
+ // on expiry) and greets the owner's next session. NEVER set this for
2415
+ // hand-typed targets: the exactly-one-live-row refusal below is what keeps an
2416
+ // ambiguous prefix from queuing a note nobody will ever receive.
2417
+ allowOfflineTarget = false,
2203
2418
  }) {
2204
2419
  const body = neutralizeMarkers(String(text || '').replace(/\s+/g, ' ').trim().slice(0, 400));
2205
2420
  if (!brainPath || !from || !body) return { posted: false, message: null, reason: 'invalid-message' };
@@ -2238,7 +2453,11 @@ export function postPresenceMessage({
2238
2453
  // unsafe: it may be an ambiguous UUID prefix or duplicated branch. Refuse
2239
2454
  // instead of queuing a note whose visible `to` never had a recipient.
2240
2455
  if (!broadcast && candidateIds.length !== 1) {
2241
- return { posted: false, message: null, reason: 'target-not-unique' };
2456
+ if (!(allowOfflineTarget && candidateIds.length === 0)) {
2457
+ return { posted: false, message: null, reason: 'target-not-unique' };
2458
+ }
2459
+ // Offline machine-known recipient: address the exact id we were handed.
2460
+ candidateIds.push(String(target).slice(0, 160));
2242
2461
  }
2243
2462
  const message = {
2244
2463
  id: sha16(`${from}|${to}|${body}|${now}|${crypto.randomBytes(4).toString('hex')}`),
@@ -2471,7 +2690,7 @@ export function formatReceivedMessages(messages, now = Date.now(), decay = {}, s
2471
2690
  lines.push(`- from ${senderLabel} (${ageMin}m ago): ${neutralizeMarkers(String(message.text || '').replace(/\s+/g, ' ').trim().slice(0, 400))}`);
2472
2691
  const receipts = group.map((item) => messageDeliveryReceipt(item, sessionId)).filter(Boolean);
2473
2692
  if (receipts.length) {
2474
- lines.push(` Receipt(s): ${receipts.map((receipt) => `${receipt.messageId}:${receipt.offerToken}`).join(', ')}. After incorporating ${receipts.length === 1 ? 'it' : 'them'}, call brain_message_receipt with each exact message_id and offer_token.`);
2693
+ lines.push(` Receipt(s): ${receipts.map((receipt) => `${receipt.messageId}:${receipt.offerToken}`).join(', ')}. After incorporating ${receipts.length === 1 ? 'it' : 'them'}, call brain_message_receipt with each exact message_id and offer_token — that is the ONLY way the sender learns you acted on ${receipts.length === 1 ? 'it' : 'them'}. If you skip it, your next independent action auto-consumes ${receipts.length === 1 ? 'this note' : 'these notes'} and the sender is told only that ${receipts.length === 1 ? 'it was' : 'they were'} auto-consumed.`);
2475
2694
  }
2476
2695
  const info = messageDecayInfo({ ...message, ts: oldestTs }, now, decay);
2477
2696
  if (info) lines.push(` ${info.stampText}`);
@@ -511,6 +511,14 @@ const receiptRecordMap = (message) => {
511
511
  state: ['pending', 'offered', 'acknowledged', 'consumed', 'failed'].includes(raw?.state)
512
512
  ? raw.state : 'offered',
513
513
  reason: raw?.reason ? String(raw.reason) : null,
514
+ // HOW consumption happened, not just THAT it did. Dropping this during
515
+ // normalization is the root cause of the receipt surfaces calling every
516
+ // consumption "explicit": the distinction was recorded on disk by
517
+ // agent-presence and then discarded one layer before the renderer, so no
518
+ // renderer could tell the strong claim from the weak one. Only the two
519
+ // values the writer actually sets are honoured; anything else is unknown.
520
+ consumedVia: raw?.consumedVia === 'receipt' || raw?.consumedVia === 'auto-lease'
521
+ ? raw.consumedVia : null,
514
522
  });
515
523
  }
516
524
  // Historical `seen` was stamped before output transport/model injection. It
@@ -551,6 +559,17 @@ export function summarizeReceipts({ messages, sessions, selfId, now = Date.now()
551
559
  const failed = stateIds('failed');
552
560
  const pending = candidateIds.filter((id) => !records.has(id) || records.get(id)?.state === 'pending');
553
561
  const unresolved = [...pending, ...offered, ...acknowledged].map((id) => String(id).slice(0, 8));
562
+ // WHY consumption is split: 'consumed' is reached two very different ways.
563
+ // 'receipt' is the strong claim — the recipient named the message id and offer
564
+ // token, i.e. "I acted on it". 'auto-lease' is the weak one — a later independent
565
+ // action merely followed the offer, which is evidence of activity, not of uptake.
566
+ // An ABSENT consumedVia is UNKNOWN, never auto-lease: the field is serialized only
567
+ // when set (agent-presence.mjs:507) and deleted on state reset (:588), so records
568
+ // written by an older bundled engine carry no marker and must not be miscounted.
569
+ const via = (id) => records.get(id)?.consumedVia || null;
570
+ const consumedByReceipt = consumed.filter((id) => via(id) === 'receipt');
571
+ const consumedByLease = consumed.filter((id) => via(id) === 'auto-lease');
572
+ const consumedUnknownVia = consumed.filter((id) => via(id) === null);
554
573
  return {
555
574
  id: m.id,
556
575
  to: m.to || 'all',
@@ -560,6 +579,9 @@ export function summarizeReceipts({ messages, sessions, selfId, now = Date.now()
560
579
  text: String(m.text || '').slice(0, 120),
561
580
  acknowledged: acknowledged.length,
562
581
  consumed: consumed.length,
582
+ consumedByReceipt: consumedByReceipt.length,
583
+ consumedByLease: consumedByLease.length,
584
+ consumedUnknownVia: consumedUnknownVia.length,
563
585
  // Compatibility alias for programmatic consumers. It now means a later
564
586
  // model-context acknowledgement milestone (including subsequently
565
587
  // consumed deliveries), never `seen` and never human-read.
@@ -571,6 +593,8 @@ export function summarizeReceipts({ messages, sessions, selfId, now = Date.now()
571
593
  offeredIds: offered.map((id) => String(id).slice(0, 8)),
572
594
  acknowledgedIds: acknowledged.map((id) => String(id).slice(0, 8)),
573
595
  consumedIds: consumed.map((id) => String(id).slice(0, 8)),
596
+ consumedByReceiptIds: consumedByReceipt.map((id) => String(id).slice(0, 8)),
597
+ consumedByLeaseIds: consumedByLease.map((id) => String(id).slice(0, 8)),
574
598
  failedIds: failed.map((id) => String(id).slice(0, 8)),
575
599
  deadLetterReason: m.deadLetter?.reason ? String(m.deadLetter.reason) : null,
576
600
  };
@@ -578,6 +602,35 @@ export function summarizeReceipts({ messages, sessions, selfId, now = Date.now()
578
602
  return { sent: receipts.length, receipts };
579
603
  }
580
604
 
605
+ // ONE phrase, shared by every receipt surface. Two renderers drifting apart is how
606
+ // "explicitly consumed" survived after auto-lease made it untrue on most paths, so
607
+ // the wording lives in exactly one place and both callers below must use it.
608
+ // `receipt.consumed === receipt.candidates` is the caller's precondition.
609
+ export function fullConsumptionPhrase(receipt) {
610
+ const weak = (receipt.consumedByLease || 0) + (receipt.consumedUnknownVia || 0);
611
+ if (!weak) {
612
+ return `explicitly consumed by all ${receipt.candidates} target peer(s) via receipt (not human-read)`;
613
+ }
614
+ const parts = [];
615
+ if (receipt.consumedByReceipt) parts.push(`${receipt.consumedByReceipt} by explicit receipt`);
616
+ if (receipt.consumedByLease) parts.push(`${receipt.consumedByLease} auto-consumed after a later independent action`);
617
+ if (receipt.consumedUnknownVia) parts.push(`${receipt.consumedUnknownVia} consumed by an unrecorded route`);
618
+ return `consumed by all ${receipt.candidates} target peer(s) — ${parts.join(', ')}; auto-consumption is not proof the note was acted on, and never proof a human read it`;
619
+ }
620
+
621
+ // Shown only when some peers consumed by the weak route, so a partial line still
622
+ // distinguishes uptake from mere activity.
623
+ function partialConsumptionSplit(receipt) {
624
+ if (!receipt.consumed) return '';
625
+ const weak = (receipt.consumedByLease || 0) + (receipt.consumedUnknownVia || 0);
626
+ if (!weak) return '';
627
+ // 'auto-consumed' is itself a mechanism claim; an unknown-route record (older
628
+ // engine wrote it) must not be folded into it (2026-08-17 review catch).
629
+ return receipt.consumedUnknownVia
630
+ ? ` (${receipt.consumedByReceipt || 0} by receipt, ${weak} auto-consumed or unrecorded)`
631
+ : ` (${receipt.consumedByReceipt || 0} by receipt, ${weak} auto-consumed)`;
632
+ }
633
+
581
634
  // One honest line per receipt. `pending` is listed by id-prefix up to 4 — a name
582
635
  // you can chase beats a number you can't.
583
636
  export function renderReceipt(receipt) {
@@ -590,14 +643,14 @@ export function renderReceipt(receipt) {
590
643
  return `- ${who}, ${age}: queued with no live local target snapshot; no local delivery is claimed. “${receipt.text}”`;
591
644
  }
592
645
  if (receipt.consumed === receipt.candidates && !receipt.failed) {
593
- return `- ${who}, ${age}: explicitly consumed by all ${receipt.candidates} target peer(s) after model-context delivery (not human-read). “${receipt.text}”`;
646
+ return `- ${who}, ${age}: ${fullConsumptionPhrase(receipt)}. “${receipt.text}”`;
594
647
  }
595
648
  const pending = receipt.pendingIds.slice(0, 4).join(', ');
596
649
  const more = receipt.pendingIds.length > 4 ? ` +${receipt.pendingIds.length - 4} more` : '';
597
650
  const offered = receipt.offered ? ` · offered ${receipt.offered}, awaiting later-action ack` : '';
598
651
  const acknowledged = receipt.acknowledged ? ` · acknowledged ${receipt.acknowledged}, awaiting explicit consumption` : '';
599
652
  const failed = receipt.failed ? ` · failed ${receipt.failed}${failure ? ` (${failure})` : ''}` : '';
600
- return `- ${who}, ${age}: consumed ${receipt.consumed} of ${receipt.candidates}${offered}${acknowledged}${failed}${pending ? ` · unresolved ${pending}${more}` : ''}. “${receipt.text}”`;
653
+ return `- ${who}, ${age}: consumed ${receipt.consumed} of ${receipt.candidates}${partialConsumptionSplit(receipt)}${offered}${acknowledged}${failed}${pending ? ` · unresolved ${pending}${more}` : ''}. “${receipt.text}”`;
601
654
  }
602
655
 
603
656
  // Compact, text-free receipt for surfaces that must stay cheap (SessionStart
@@ -615,14 +668,14 @@ export function renderReceiptSummary(summary) {
615
668
  : `📬 Your last note${target} (${age}): queued with no live local target snapshot; no local delivery claimed.`;
616
669
  }
617
670
  if (receipt.consumed === receipt.candidates && !receipt.failed) {
618
- return `📬 Your last note${target} (${age}): explicitly consumed by all ${receipt.candidates} target peer(s) after model-context delivery (not human-read).`;
671
+ return `📬 Your last note${target} (${age}): ${fullConsumptionPhrase(receipt)}.`;
619
672
  }
620
673
  const pending = receipt.pendingIds.slice(0, 4).join(', ');
621
674
  const more = receipt.pendingIds.length > 4 ? ` +${receipt.pendingIds.length - 4} more` : '';
622
675
  const offered = receipt.offered ? ` · offered ${receipt.offered}, awaiting ack` : '';
623
676
  const acknowledged = receipt.acknowledged ? ` · acknowledged ${receipt.acknowledged}, awaiting explicit consumption` : '';
624
677
  const failed = receipt.failed ? ` · failed ${receipt.failed}${failure ? ` (${failure})` : ''}` : '';
625
- return `📬 Your last note${target} (${age}): consumed ${receipt.consumed} of ${receipt.candidates}${offered}${acknowledged}${failed}${pending ? ` · unresolved ${pending}${more}` : ''}.`;
678
+ return `📬 Your last note${target} (${age}): consumed ${receipt.consumed} of ${receipt.candidates}${partialConsumptionSplit(receipt)}${offered}${acknowledged}${failed}${pending ? ` · unresolved ${pending}${more}` : ''}.`;
626
679
  }
627
680
 
628
681
  export function renderReceipts(summary, { limit = 3 } = {}) {
@@ -958,7 +958,11 @@ function peerFooter(sid) {
958
958
  const legend = marked.some(f => f.endsWith('*')) ? ' (* = observed from live edits, scope not declared)' : '';
959
959
  warn = ` · ⚠️ both edited: ${marked.join(', ')} — expect a conflict, KEEP BOTH${legend}`;
960
960
  }
961
- else if (p.branch && myBranch && p.branch === myBranch) warn = ' · ⚠️ same branch — pull/rebase before you commit';
961
+ // State the fact, never the git operation. This runs in a latency-sensitive
962
+ // hook and must not spawn git, so it cannot know whether the branch is merely
963
+ // behind or has DIVERGED — and on a diverged branch "pull/rebase" is advice
964
+ // the real state contradicts. Naming the collision is what this footer is for.
965
+ else if (p.branch && myBranch && p.branch === myBranch) warn = ' · ⚠️ same branch — your commits will interleave; coordinate before you commit';
962
966
  // Unique over the full live list (not just shown peers) — the MSG router
963
967
  // resolves a prefix against every row, so uniqueness must match that set.
964
968
  lines.push(`- session ${shortestUniquePeerPrefix(list, p.id)} · ${bits.join(' · ')}${warn}`);
@@ -966,7 +970,7 @@ function peerFooter(sid) {
966
970
  // v1.32.0 law: a truncated list must never render as a complete one. This
967
971
  // overflow line is unconditional — never subject to any budget.
968
972
  if (peers.length > 4) lines.push(`- …and ${peers.length - 4} more live session(s) not shown — \`npx klypix-mcp doctor\` or brain_sync lists all.`);
969
- lines.push('Coordinate BEFORE touching shared files: reply with `🧠 MSG [<their id-prefix or branch>]: <text>` (or call `brain_message`). KLYPIX queues it for supported lifecycle/MCP actions and replays it until the receiving model explicitly records consumption with `brain_message_receipt`. Check the brain for durable decisions/ships.');
973
+ lines.push('Coordinate BEFORE touching shared files: reply with `🧠 MSG [<their id-prefix or branch>]: <text>` (or call `brain_message`). KLYPIX queues it for supported lifecycle/MCP actions and replays it until the offer is acknowledged; the receiver then either records uptake with `brain_message_receipt` or a further independent action auto-consumes it without one. Check the brain for durable decisions/ships.');
970
974
  return lines.join('\n');
971
975
  }
972
976
 
@@ -1189,7 +1189,7 @@ export async function opBrainMessage({ vault, canvas, text: msgText, to, via, fr
1189
1189
  const message = result.message;
1190
1190
  const candidates = Array.isArray(message.candidateIds) ? message.candidateIds.length : 0;
1191
1191
  return {
1192
- blocks: [text(`📨 queued in this project's coordination lane (to: ${message.to}; id: ${message.id}) — ${candidates} live target session(s) were snapshotted. Delivery is pending until a supported lifecycle/MCP action offers it into model-visible context; it replays through acknowledgement until the receiver explicitly records consumption with brain_message_receipt. This is a coordination receipt, not proof a human read it and not a brain card — use brain_note for durable project decisions.`)],
1192
+ blocks: [text(`📨 queued in this project's coordination lane (to: ${message.to}; id: ${message.id}) — ${candidates} live target session(s) were snapshotted. Delivery is pending until a supported lifecycle/MCP action offers it into model-visible context, and it replays until the offer is acknowledged. AFTER acknowledgement it retires one of two ways: the receiver calls brain_message_receipt with the exact message id and offer token ("acted on it"), or a further independent action AUTO-CONSUMES it without any receipt. Your receipt line names which. Auto-consumption is not proof the note was acted on, and no path here is proof a human read it. Not a brain card — use brain_note for durable project decisions.`)],
1193
1193
  message,
1194
1194
  };
1195
1195
  }
@@ -2002,6 +2002,33 @@ export function rankForQuestion(struct, question, { semantic = null, k = 10, as_
2002
2002
  // the embedding side, not from reweighting here. The kept behaviors are
2003
2003
  // pinned numerically in test/retrieval-fusion.mjs — a change that trips
2004
2004
  // that suite must re-run the harness before shipping.
2005
+ //
2006
+ // THE EMBEDDING SIDE WAS THEN SWEPT TOO (2026-08-17, n=113 frozen v2,
2007
+ // real 2,553-card brain, production contract via embedTexts — prefix,
2008
+ // CLS, normalize; ordering = this function's semantic formula):
2009
+ // bge-small q8 (ships) MRR .436 · @5 61 · paraphrase 46 baseline
2010
+ // bge-small fp32 MRR .456 · @5 60 · paraphrase 46 — so the
2011
+ // q8 quantization is NOT the ceiling; the 384-dim model is.
2012
+ // bge-base q8 (768d) MRR .475 · @5 61 · paraphrase 51 [39,63] vs
2013
+ // [34,58] — every needle right, none CI-clearing, at 2.4× embed
2014
+ // cost (622s vs 255s full-brain) and ~3× download. NOT flipped.
2015
+ // exact-duplicate collapse at rank time: FLAT (61→61, para 46→46),
2016
+ // confirming the MMR-deferral result above by a second route.
2017
+ // 1,500-char truncation: measurably NOT a failure mode — gold cards
2018
+ // >1,500 chars score BETTER (@5 71% vs 57%), and 0 of 34 paraphrase
2019
+ // misses attribute to it. "We truncate, therefore we miss" is
2020
+ // falsified; do not chunk on that rationale.
2021
+ // (gte-small collapsed to MRR .17 under this contract — it wants mean
2022
+ // pooling and no prefix, so that number is contract mismatch, not a
2023
+ // fair test of gte.)
2024
+ // Failure signature stands: cosine COMPRESSION — on paraphrase misses
2025
+ // the gold sits a median 0.040 cosine below the 5th hit, 21/34 at rank
2026
+ // >50, 6/34 outside the 200 pool. Same-class drop-in models do not fix
2027
+ // that on this corpus. The remaining live path is DOC-SIDE enrichment
2028
+ // (capture-time question/intent text embedded alongside the card) or a
2029
+ // genuinely larger embedder — both are product decisions with shipping
2030
+ // costs, not tuning. Runner: scratchpad ab-runner.mjs pattern — embed
2031
+ // through embedTexts with a swapped pipe, never a hand-copied contract.
2005
2032
  const semRank = new Map(scored
2006
2033
  .filter(hit => Number.isFinite(hit.sem))
2007
2034
  .sort((a, b) => b.sem - a.sem)