klypix-mcp 1.72.0 → 1.73.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -6
- package/bin/klypix-install.mjs +2 -2
- package/bin/klypix-worker.mjs +0 -86
- package/package.json +2 -3
- package/src/mcp-presence.mjs +59 -0
- package/src/remote-client.mjs +0 -1108
package/README.md
CHANGED
|
@@ -79,7 +79,7 @@ npx klypix-mcp conformance
|
|
|
79
79
|
|
|
80
80
|
It runs in a temporary fixture and touches nothing else. It checks tool discovery, task memory,
|
|
81
81
|
truthful peer reporting, overlap surfacing, proactive logging, and in-band delivery of a peer note.
|
|
82
|
-
It verifies 15 required coordination behaviours — not the
|
|
82
|
+
It verifies 15 required coordination behaviours — not the 22 tools, and not the retrieval engine.
|
|
83
83
|
|
|
84
84
|
---
|
|
85
85
|
|
|
@@ -102,7 +102,7 @@ result** before it exits:
|
|
|
102
102
|
editors Claude Code · Cursor · Codex · Gemini CLI · Antigravity · VS Code
|
|
103
103
|
wired 9 file(s) · 9 updated (skipped 5 for tools you don't have)
|
|
104
104
|
git lossless .klypix merge driver registered
|
|
105
|
-
verified ✓
|
|
105
|
+
verified ✓ 22 tools reachable via .mcp.json (892ms)
|
|
106
106
|
```
|
|
107
107
|
|
|
108
108
|
That last line is the point. MCP config fails **silently** — a wrong entry means the server never
|
|
@@ -542,10 +542,6 @@ The MCP verbs below are what agents call. These are what **you** call:
|
|
|
542
542
|
| `project_map_context` | Read-only, bounded code-graph evidence beside correction-aware brain context, with exact-path review proposals; external artifacts (e.g. Graphify) are supported but never installed or run locally |
|
|
543
543
|
| `project_map_scan` | KLYPIX's own zero-install scanner: gitignore-aware file inventory + file-level import edges (relative, tsconfig-alias, and monorepo-workspace imports resolved) written to `klypix-map/graph.json` — which then serves `project_map_context` automatically |
|
|
544
544
|
| `project_map_drift` | Read-only drift report: brain cards whose referenced files are gone or moved (with rename candidates), plus a headline when the checkout itself is behind its origin default branch |
|
|
545
|
-
| `remote_status` | Inspect the local KLYPIX tray relay and its verified Remote capabilities |
|
|
546
|
-
| `remote_sessions` | List coding-agent sessions with exact provider, host-binding and capability receipts |
|
|
547
|
-
| `remote_actions` | List pending questions, approvals, failures, conflicts and reviews reported by supported providers |
|
|
548
|
-
| `remote_command` | Control one exact verified coding-agent session using a fresh capability receipt; unsupported operations fail closed |
|
|
549
545
|
| `canvas_view` | Returns the board as a structured render spec plus a text summary, and declares an MCP Apps (SEP-1865) UI resource |
|
|
550
546
|
| `read_canvas` | A canvas as markdown (cards, connection graph, `[[links]]`, `#tags`) |
|
|
551
547
|
| `search_canvases` | Search across canvases by name and content |
|
package/bin/klypix-install.mjs
CHANGED
|
@@ -246,7 +246,7 @@ const flatten = (code) => code
|
|
|
246
246
|
.replace(/\.\.\/src\/klypix-(core|format)\.mjs/g, './klypix-$1.mjs')
|
|
247
247
|
// brain-doctor + agent-rules (the server's lazy `import('../src/brain-doctor.mjs')`
|
|
248
248
|
// for the brain_doctor tool) → flat sibling refs in the runtime layout.
|
|
249
|
-
.replace(/\.\.\/src\/(bench|brain-doctor|agent-presence|agent-rules|finding-routing|mcp-presence|mcp-supervisor|mcp-auto-update|presence-relay|semantic-memory|runtime-inspector|project-graph|git-capture-install
|
|
249
|
+
.replace(/\.\.\/src\/(bench|brain-doctor|agent-presence|agent-rules|finding-routing|mcp-presence|mcp-supervisor|mcp-auto-update|presence-relay|semantic-memory|runtime-inspector|project-graph|git-capture-install)\.mjs/g, './$1.mjs')
|
|
250
250
|
.replace(/klypix-worker\.mjs/g, 'klypix-mcp-worker.mjs')
|
|
251
251
|
.replace(/const PKG_VERSION = \(\(\) => \{[\s\S]*?\}\)\(\);/, `const PKG_VERSION = '${VERSION}'; // baked at install (flat layout has no package.json)`);
|
|
252
252
|
|
|
@@ -380,7 +380,7 @@ try {
|
|
|
380
380
|
// canvas-view-app.html is the canvas_view MCP App UI — staged raw (an HTML
|
|
381
381
|
// file must never get a JS-comment banner) beside the flat server, which
|
|
382
382
|
// resolves it via its ./canvas-view-app.html candidate path.
|
|
383
|
-
for (const f of ['global-brain-hook.mjs', 'brain-semantic.mjs', 'semantic-memory.mjs', 'brain-note.mjs', 'brain-git-hook.mjs', 'git-capture-install.mjs', 'brain-history.mjs', 'brain-graveyard.mjs', 'klypix-format.mjs', 'klypix-core.mjs', 'brain-write-lock.mjs', 'agent-rules.mjs', 'brain-doctor.mjs', 'agent-presence.mjs', 'mcp-presence.mjs', 'repo-state.mjs', 'result-reconcile.mjs', 'finding-routing.mjs', 'presence-relay.mjs', 'mcp-supervisor.mjs', 'mcp-auto-update.mjs', 'runtime-inspector.mjs', 'project-graph.mjs', '
|
|
383
|
+
for (const f of ['global-brain-hook.mjs', 'brain-semantic.mjs', 'semantic-memory.mjs', 'brain-note.mjs', 'brain-git-hook.mjs', 'git-capture-install.mjs', 'brain-history.mjs', 'brain-graveyard.mjs', 'klypix-format.mjs', 'klypix-core.mjs', 'brain-write-lock.mjs', 'agent-rules.mjs', 'brain-doctor.mjs', 'agent-presence.mjs', 'mcp-presence.mjs', 'repo-state.mjs', 'result-reconcile.mjs', 'finding-routing.mjs', 'presence-relay.mjs', 'mcp-supervisor.mjs', 'mcp-auto-update.mjs', 'runtime-inspector.mjs', 'project-graph.mjs', 'bench.mjs', 'codex-brain-hook.mjs', 'codex-hooks.mjs', 'canvas-view-app.html']) {
|
|
384
384
|
const s = path.join(SRC, f); if (exists(s)) staged.push({ dst: f, content: fs.readFileSync(s, 'utf8') });
|
|
385
385
|
}
|
|
386
386
|
for (const [src, dst] of [
|
package/bin/klypix-worker.mjs
CHANGED
|
@@ -39,7 +39,6 @@ import {
|
|
|
39
39
|
registerProjectBrain,
|
|
40
40
|
spawnAutoUpdateHelper,
|
|
41
41
|
} from '../src/mcp-auto-update.mjs';
|
|
42
|
-
import { remoteActions, remoteCommand, remoteSessions, remoteStatus } from '../src/remote-client.mjs';
|
|
43
42
|
// Namespace import (already in-process via the klypix-core chain, so zero added
|
|
44
43
|
// load cost) so a bundle whose klypix-format predates classifyDecay degrades
|
|
45
44
|
// gracefully — a named import of a missing export would kill the whole server.
|
|
@@ -408,91 +407,6 @@ const toContent = (r) => {
|
|
|
408
407
|
return result;
|
|
409
408
|
};
|
|
410
409
|
|
|
411
|
-
// Return raw MCP results here. The universal registerTool wrapper above owns
|
|
412
|
-
// the single presence/message transition for every non-brain_sync call. A
|
|
413
|
-
// helper-level decoration would offer and then acknowledge one note inside the
|
|
414
|
-
// same Remote call, violating delivery-v3's later-action invariant.
|
|
415
|
-
const remoteToolResult = (label, value) => ({
|
|
416
|
-
content: [{ type: 'text', text: `${label}\n\n${JSON.stringify(value, null, 2)}` }],
|
|
417
|
-
structuredContent: { result: value },
|
|
418
|
-
});
|
|
419
|
-
const remoteToolError = (error) => {
|
|
420
|
-
const message = typeof error?.message === 'string' && /^KLYPIX_[A-Z0-9_]+$/.test(error.message)
|
|
421
|
-
? error.message : 'KLYPIX_REMOTE_REQUEST_REJECTED';
|
|
422
|
-
return {
|
|
423
|
-
content: [{ type: 'text', text: `${message}. The KLYPIX desktop window may be closed, but its tray relay must be running and Remote control must be enabled.` }],
|
|
424
|
-
isError: true,
|
|
425
|
-
};
|
|
426
|
-
};
|
|
427
|
-
const remoteProjectRoot = () => (mcpPresence.brainPath ? path.dirname(mcpPresence.brainPath) : null);
|
|
428
|
-
|
|
429
|
-
server.registerTool('remote_status', {
|
|
430
|
-
title: 'Inspect the local KLYPIX Remote relay',
|
|
431
|
-
description: 'Reports whether the KLYPIX tray relay, discovery, provider hooks, verified controls, phone pairing, and encrypted network relay are active. The desktop WINDOW may be closed; KLYPIX must still be running in the tray and the PC must be awake.',
|
|
432
|
-
annotations: { destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
433
|
-
inputSchema: {},
|
|
434
|
-
}, async () => {
|
|
435
|
-
try { return remoteToolResult('KLYPIX Remote status', await remoteStatus({ projectRoot: remoteProjectRoot() })); }
|
|
436
|
-
catch (error) { return remoteToolError(error); }
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
server.registerTool('remote_sessions', {
|
|
440
|
-
title: 'List coding-agent sessions by KLYPIX project',
|
|
441
|
-
description: 'Lists local coding-agent sessions with their project/path binding, current state, active host binding, and exact capability receipts. Treat each receipt as the truth for that specific provider + version + live session; never infer a missing action. The KLYPIX tray relay must be running.',
|
|
442
|
-
annotations: { destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
443
|
-
inputSchema: {},
|
|
444
|
-
}, async () => {
|
|
445
|
-
try { return remoteToolResult('KLYPIX Remote sessions', await remoteSessions({ projectRoot: remoteProjectRoot() })); }
|
|
446
|
-
catch (error) { return remoteToolError(error); }
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
server.registerTool('remote_actions', {
|
|
450
|
-
title: 'List pending coding-agent decisions',
|
|
451
|
-
description: 'Lists pending questions, permission requests, confirmations, failures, conflicts, and reviews reported by supported provider hooks. Approval/rejection still requires the exact session capability receipt and request digest. The KLYPIX tray relay must be running.',
|
|
452
|
-
annotations: { destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
453
|
-
inputSchema: {},
|
|
454
|
-
}, async () => {
|
|
455
|
-
try { return remoteToolResult('KLYPIX Remote actions', await remoteActions({ projectRoot: remoteProjectRoot() })); }
|
|
456
|
-
catch (error) { return remoteToolError(error); }
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
server.registerTool('remote_command', {
|
|
460
|
-
title: 'Control one verified coding-agent session',
|
|
461
|
-
description: 'Sends one command to the exact machine/provider/session and host binding named by a fresh remote_sessions capability receipt. Supports opening the exact provider session, messages, images/files, interrupt, approve/reject, close/archive/resume only when that exact receipt says the operation is available. File paths are local to this PC. The KLYPIX tray relay must be running and verified controls enabled.',
|
|
462
|
-
annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
463
|
-
inputSchema: {
|
|
464
|
-
machine_id: z.string().min(1).max(256),
|
|
465
|
-
provider: z.enum(['antigravity-agent', 'claude-code', 'codex', 'cursor-agent', 'gemini-cli', 'grok-build', 'kimi-code', 'opencode', 'pi', 'qoder', 'mistral-vibe', 'unknown']),
|
|
466
|
-
external_session_id: z.string().min(1).max(512),
|
|
467
|
-
operation: z.enum(['send-text', 'send-message', 'attach-image', 'attach-file', 'interrupt', 'approve', 'reject', 'close', 'archive', 'resume', 'open-provider']),
|
|
468
|
-
capability_receipt_id: z.string().min(1).max(256).describe('Fresh receipt id for this exact operation from remote_sessions.'),
|
|
469
|
-
binding_id: z.string().min(1).max(256).describe('Fresh host binding id named by the receipt.'),
|
|
470
|
-
request_digest: z.string().regex(/^[0-9a-f]{64}$/).optional().describe('Required for approve/reject; copy it from the pending remote action.'),
|
|
471
|
-
text: z.string().min(1).max(20000).optional(),
|
|
472
|
-
replace_draft: z.boolean().optional(),
|
|
473
|
-
attachments: z.array(z.object({
|
|
474
|
-
path: z.string().min(1).max(2048).describe('Absolute local file path on this PC.'),
|
|
475
|
-
kind: z.enum(['image', 'video', 'audio', 'document', 'file']),
|
|
476
|
-
mime_type: z.string().min(1).max(128).optional(),
|
|
477
|
-
})).max(25).optional(),
|
|
478
|
-
},
|
|
479
|
-
}, async ({ machine_id, provider, external_session_id, operation, capability_receipt_id, binding_id, request_digest, text, replace_draft, attachments }, extra) => {
|
|
480
|
-
try {
|
|
481
|
-
const receipt = await remoteCommand({
|
|
482
|
-
machineId: machine_id, provider, externalSessionId: external_session_id, operation,
|
|
483
|
-
capabilityReceiptId: capability_receipt_id, bindingId: binding_id,
|
|
484
|
-
requestDigest: request_digest, text, replaceDraft: replace_draft,
|
|
485
|
-
attachments: attachments?.map((attachment) => ({
|
|
486
|
-
path: attachment.path, kind: attachment.kind, mimeType: attachment.mime_type,
|
|
487
|
-
})),
|
|
488
|
-
}, {
|
|
489
|
-
projectRoot: remoteProjectRoot(),
|
|
490
|
-
actionIdentity: extra.klypixRequestIdentity?.actionId,
|
|
491
|
-
});
|
|
492
|
-
return remoteToolResult('KLYPIX Remote command receipt', receipt);
|
|
493
|
-
} catch (error) { return remoteToolError(error); }
|
|
494
|
-
});
|
|
495
|
-
|
|
496
410
|
server.registerTool('list_canvases', {
|
|
497
411
|
title: 'List KLYPIX canvases',
|
|
498
412
|
description: 'List all .klypix / .any canvas files in the vault, with card and connection counts.',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "klypix-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.73.2",
|
|
4
4
|
"description": "Shared project brain and MCP coordination server for multi-agent coding.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -79,12 +79,11 @@
|
|
|
79
79
|
"node": ">=18"
|
|
80
80
|
},
|
|
81
81
|
"scripts": {
|
|
82
|
-
"test:remote": "node test/remote-client.mjs",
|
|
83
82
|
"test:project-graph": "node test/project-graph.mjs",
|
|
84
83
|
"bench": "node bin/klypix-mcp.mjs bench",
|
|
85
84
|
"test:bench": "node test/bench.mjs",
|
|
86
85
|
"pretest": "node test/publish-workflow.mjs",
|
|
87
|
-
"test": "node test/publish-verdict.mjs && node test/
|
|
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",
|
|
88
87
|
"test:memory": "node test/memory-runtime.mjs",
|
|
89
88
|
"test:memory:soak": "node --expose-gc test/memory-soak.mjs",
|
|
90
89
|
"runtime": "node bin/klypix-runtime.mjs"
|
package/src/mcp-presence.mjs
CHANGED
|
@@ -598,6 +598,12 @@ const publicSession = (session, now) => {
|
|
|
598
598
|
// Only real prompt/tool work stamps activityAt; heartbeats deliberately
|
|
599
599
|
// preserve it, so a fresh stamp means work, not merely a live socket.
|
|
600
600
|
const UNDECLARED_ACTIVE_MS = 10 * 60 * 1000;
|
|
601
|
+
|
|
602
|
+
// How much of the ~2h release lease must remain before the HOLDER is warned.
|
|
603
|
+
// 25 minutes is longer than a full desktop candidate build + verify, so a
|
|
604
|
+
// holder who sees the warning still has time to finish or refresh rather than
|
|
605
|
+
// losing the lease mid-release.
|
|
606
|
+
const RELEASE_LEASE_WARN_MS = 25 * 60 * 1000;
|
|
601
607
|
const isUndeclaredActive = (session, now) =>
|
|
602
608
|
!isTaskSession(session)
|
|
603
609
|
&& Number(session?.activityAt || 0) > 0
|
|
@@ -1829,6 +1835,11 @@ export function createMcpPresence({
|
|
|
1829
1835
|
: undefined;
|
|
1830
1836
|
if (started && resolved === vault
|
|
1831
1837
|
&& (selectedBrainPath === undefined || selectedBrainPath === brainPath)) {
|
|
1838
|
+
// Same reason as the sync() adoption above: a reconnect to the vault we
|
|
1839
|
+
// are already on must still re-adopt the freshly validated binding, or a
|
|
1840
|
+
// brain rewritten since the last bind leaves this connection guarded by a
|
|
1841
|
+
// dead inode.
|
|
1842
|
+
if (validatedSelection?.projectBinding) currentProjectBinding = validatedSelection.projectBinding;
|
|
1832
1843
|
return touch({ event: 'McpReconnect', includePresence: true });
|
|
1833
1844
|
}
|
|
1834
1845
|
if (started) stop({
|
|
@@ -2299,6 +2310,19 @@ export function createMcpPresence({
|
|
|
2299
2310
|
clientInfoPrepared: preparedClientInfo,
|
|
2300
2311
|
branchPrepared: preparedBranch,
|
|
2301
2312
|
};
|
|
2313
|
+
// Adopt the preflight's binding on BOTH arms, not just the start() arm.
|
|
2314
|
+
// A same-project re-sync takes touch(), which never reassigns — so the
|
|
2315
|
+
// guard kept comparing a binding whose brain inode had already been retired
|
|
2316
|
+
// by an atomic write (every managed brain write is writeFileSync+rename, so
|
|
2317
|
+
// ANY write by anyone, including this session's own Stop hook, mints a new
|
|
2318
|
+
// inode). The result was a permanent deadlock: brain_sync succeeded and
|
|
2319
|
+
// reported the right project while every other verb was rejected, and the
|
|
2320
|
+
// only refresh path was itself gated behind the failing check. This binding
|
|
2321
|
+
// was already validated three times earlier in THIS call (preflightSync →
|
|
2322
|
+
// consumeSyncPreflight → revalidateConsumedSyncPreflight), so adopting it is
|
|
2323
|
+
// strictly more current than retaining the stale one and grants no new
|
|
2324
|
+
// authority — the canonical-root and escape checks already ran on it.
|
|
2325
|
+
if (projectBinding) currentProjectBinding = projectBinding;
|
|
2302
2326
|
let report = started && requestedVault === vault && resultBrainPath === brainPath
|
|
2303
2327
|
? touch(details)
|
|
2304
2328
|
: start(requestedVault, details, {
|
|
@@ -2454,6 +2478,10 @@ export function createMcpPresence({
|
|
|
2454
2478
|
{
|
|
2455
2479
|
const leaseStamp = now();
|
|
2456
2480
|
let outcome = null;
|
|
2481
|
+
// Set when the holder's own sync arrived with the lease already close to
|
|
2482
|
+
// lapsing; reported after the refresh so the holder learns the habit that
|
|
2483
|
+
// protects them, not merely that nothing broke this time.
|
|
2484
|
+
let nearExpiryBeforeRefreshMs = null;
|
|
2457
2485
|
if (releaseIntentChecked.provided && completing) {
|
|
2458
2486
|
// The DECLARATION is ignored on completion, but a REAL completion must
|
|
2459
2487
|
// still free a lease this session holds — a holder who kept attaching
|
|
@@ -2528,6 +2556,17 @@ export function createMcpPresence({
|
|
|
2528
2556
|
const freed = freeReleaseLease({ brainPath, sessionId, home, now: leaseStamp });
|
|
2529
2557
|
if (freed.status === 'released') outcome = freed;
|
|
2530
2558
|
} else if (!completing) {
|
|
2559
|
+
// Snapshot BEFORE the refresh. A holder's sync refreshes the lease, so
|
|
2560
|
+
// by the time we report, expiry has already moved out and "expiring
|
|
2561
|
+
// soon" is never true — yet the risk was real a millisecond earlier.
|
|
2562
|
+
// The field failure was a holder who ran a 40-minute build with no
|
|
2563
|
+
// sync in it and lost the lease silently; the useful moment to say so
|
|
2564
|
+
// is the sync that arrives while the clock is still low.
|
|
2565
|
+
const preRefresh = readReleaseLease({ brainPath, home, now: leaseStamp });
|
|
2566
|
+
if (preRefresh && preRefresh.holderId === sessionId && preRefresh.expiresAt) {
|
|
2567
|
+
const leftBefore = Math.max(0, preRefresh.expiresAt - leaseStamp);
|
|
2568
|
+
if (leftBefore <= RELEASE_LEASE_WARN_MS) nearExpiryBeforeRefreshMs = leftBefore;
|
|
2569
|
+
}
|
|
2531
2570
|
const refreshed = refreshReleaseLease({ brainPath, sessionId, home, now: leaseStamp });
|
|
2532
2571
|
// 'lease-lost' = THIS session held the lease but it lapsed (TTL passed
|
|
2533
2572
|
// with no checkpoint). Surfacing it beats silent pruning: a holder who
|
|
@@ -2626,6 +2665,26 @@ export function createMcpPresence({
|
|
|
2626
2665
|
releaseText = `KLYPIX release lease freed: completion released the exclusive release lease (v${outcome.lease?.version} from ${outcome.lease?.ref}).`;
|
|
2627
2666
|
} else if (active) {
|
|
2628
2667
|
releaseLease = { status: 'held', holder: holderBlock };
|
|
2668
|
+
// The holder must never learn about expiry by noticing the footer
|
|
2669
|
+
// vanished — which is exactly what happened in the field on
|
|
2670
|
+
// 2026-08-16, mid-release-build. The 'lease-lost' message this design
|
|
2671
|
+
// relies on is UNREACHABLE in any project that has peers: a non-holder's
|
|
2672
|
+
// sync meets the expired lease first, prunes it (status 'stale-pruned',
|
|
2673
|
+
// write:true, lease:null) and is told nothing, so the holder's next sync
|
|
2674
|
+
// takes the no-lease fast path and never reaches the lease-lost branch.
|
|
2675
|
+
// Warning while the lease still EXISTS is therefore the last moment the
|
|
2676
|
+
// holder can still act on it, and it needs no cross-session state.
|
|
2677
|
+
}
|
|
2678
|
+
// Holder-facing expiry telemetry, attached to whichever branch produced
|
|
2679
|
+
// the block. Always reporting expiresInMs is half the fix on its own: the
|
|
2680
|
+
// holder can see the clock instead of inferring it from a footer.
|
|
2681
|
+
if (releaseLease && holderBlock && holderBlock.sessionId === sessionId && holderBlock.expiresAt) {
|
|
2682
|
+
releaseLease.expiresInMs = Math.max(0, holderBlock.expiresAt - leaseStamp);
|
|
2683
|
+
if (nearExpiryBeforeRefreshMs !== null) {
|
|
2684
|
+
releaseLease.expiringSoon = true;
|
|
2685
|
+
releaseLease.nearExpiryBeforeRefreshMs = nearExpiryBeforeRefreshMs;
|
|
2686
|
+
releaseText = `KLYPIX release lease expires in ~${Math.max(1, Math.round(nearExpiryBeforeRefreshMs / 60000))} min — refreshed by this sync. Only a brain_sync from THIS session refreshes it, so a long build with no sync in it will let the lease lapse, and a peer then prunes it without telling you.`;
|
|
2687
|
+
}
|
|
2629
2688
|
}
|
|
2630
2689
|
// ── Would this release leave finished work behind? (1.72.0) ──────────
|
|
2631
2690
|
// Coordination in this engine had always been about files two sessions
|