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.
- package/bin/klypix-conformance.mjs +5 -1
- package/bin/klypix-worker.mjs +11 -5
- package/package.json +2 -2
- package/src/agent-presence.mjs +221 -2
- package/src/finding-routing.mjs +57 -4
- package/src/global-brain-hook.mjs +6 -2
- package/src/klypix-core.mjs +1 -1
- package/src/klypix-format.mjs +27 -0
- package/src/mcp-presence.mjs +506 -19
- package/src/project-graph.mjs +24 -5
- package/src/repo-state.mjs +204 -10
|
@@ -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\)
|
|
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 ─────────────────────
|
package/bin/klypix-worker.mjs
CHANGED
|
@@ -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
|
|
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(
|
|
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.
|
|
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"
|
package/src/agent-presence.mjs
CHANGED
|
@@ -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
|
-
|
|
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}`);
|
package/src/finding-routing.mjs
CHANGED
|
@@ -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}:
|
|
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}):
|
|
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
|
-
|
|
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
|
|
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
|
|
package/src/klypix-core.mjs
CHANGED
|
@@ -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
|
|
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
|
}
|
package/src/klypix-format.mjs
CHANGED
|
@@ -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)
|