klypix-mcp 1.100.0 → 1.101.1
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 +123 -33
- package/bin/klypix-worker.mjs +18 -3
- package/package.json +8 -3
- package/src/agent-presence.mjs +31 -5
- package/src/codex-brain-hook.mjs +27 -0
- package/src/global-brain-hook.mjs +2 -1
package/README.md
CHANGED
|
@@ -1,20 +1,34 @@
|
|
|
1
|
-
#
|
|
1
|
+
# klypix-mcp — shared project memory for Claude Code, Codex and Cursor
|
|
2
|
+
|
|
3
|
+
**Every project gets a brain.**
|
|
2
4
|
|
|
3
5
|
Shared project memory for **Claude Code, Codex, Cursor** and other MCP coding tools. One
|
|
4
6
|
`brain.klypix` file, committed with your code, carries the project's decisions, corrections and open
|
|
5
7
|
questions across sessions and between tools. Corrections supersede stale decisions; sessions declare
|
|
6
8
|
the files they expect to touch and get warned about same-machine overlap. Versioned in Git. Served
|
|
7
|
-
over MCP by a process on your machine
|
|
8
|
-
what the agent reads
|
|
9
|
+
over MCP by a process on your machine. In the standalone setup, brain data stays in your files;
|
|
10
|
+
your agent's provider can still receive what the agent reads. Integration depth differs by host — see
|
|
9
11
|
[Supported hosts](#supported-hosts-and-their-integration-level).
|
|
10
12
|
|
|
13
|
+
The **Apache-2.0 engine works without the KLYPIX desktop app or a KLYPIX account**. The optional
|
|
14
|
+
[Windows workspace](https://klypix.com/developers) lets people inspect the same project brain
|
|
15
|
+
beside their files. App-based sharing has its own cloud path; see [Security and permissions](#security-and-permissions).
|
|
16
|
+
|
|
17
|
+
[Install](#quick-start) · [Try your first decision](#your-first-decision-and-correction) ·
|
|
18
|
+
[Supported hosts](#supported-hosts-and-their-integration-level) ·
|
|
19
|
+
[Privacy](#security-and-permissions) · [Limits](#current-limitations) · [Uninstall](#uninstall)
|
|
20
|
+
|
|
21
|
+
For a team walkthrough, read the [shared project memory setup guide](https://klypix.com/guides/shared-project-memory).
|
|
22
|
+
Choosing an approach? Our [memory tools comparison](https://klypix.com/guides/shared-project-memory-tools)
|
|
23
|
+
links each tool's official documentation and discloses that Klypix wrote it.
|
|
24
|
+
|
|
11
25
|

|
|
12
26
|
|
|
13
27
|
<sub>Real output, not a mockup: both panes run a real MCP client against this server
|
|
14
28
|
([docs/demo/](docs/demo/) — the GIF is rendered by CI from a scripted tape against this server, not
|
|
15
29
|
hand-recorded, and re-rendered when the server's responses change).</sub>
|
|
16
30
|
|
|
17
|
-
|
|
31
|
+
With **Node.js 20 or newer**, run this inside your project:
|
|
18
32
|
|
|
19
33
|
```bash
|
|
20
34
|
npx klypix-mcp install
|
|
@@ -59,15 +73,14 @@ carry forward into supported coding-agent sessions.
|
|
|
59
73
|
## The problem
|
|
60
74
|
|
|
61
75
|
You are running more than one coding agent on one codebase — a Claude Code session here, Codex in
|
|
62
|
-
another terminal, Cursor open on the side.
|
|
63
|
-
the others:
|
|
76
|
+
another terminal, Cursor open on the side. Context saved in one tool may be missing from the next:
|
|
64
77
|
|
|
65
|
-
-
|
|
66
|
-
-
|
|
78
|
+
- A new session misses a prior decision, and you explain the same architecture again.
|
|
79
|
+
- A decision captured only in a Claude conversation is unavailable to your Codex session.
|
|
67
80
|
- One agent implements an approach the team already rejected, because the reason it was rejected
|
|
68
81
|
lived in a chat that ended.
|
|
69
82
|
- Two sessions start changing the same files and nobody finds out until review.
|
|
70
|
-
- Git stores the code history
|
|
83
|
+
- Git stores the code history; the reasons behind a change still need to be recorded.
|
|
71
84
|
|
|
72
85
|
Your agents may run independently. Their project understanding should not.
|
|
73
86
|
|
|
@@ -75,10 +88,14 @@ Your agents may run independently. Their project understanding should not.
|
|
|
75
88
|
|
|
76
89
|
## 60 seconds: two agents, one project
|
|
77
90
|
|
|
91
|
+
Illustrative calls and responses: replace `/absolute/path/to/project` with the same project root
|
|
92
|
+
in both sessions (for example, `C:/work/api` on Windows).
|
|
93
|
+
|
|
78
94
|
Session A — Claude Code, in your repo:
|
|
79
95
|
|
|
80
96
|
```jsonc
|
|
81
|
-
brain_sync {
|
|
97
|
+
brain_sync { project: "/absolute/path/to/project", phase: "start",
|
|
98
|
+
intent: "rewrite the auth token refresh", files: ["src/auth/token.ts"] }
|
|
82
99
|
// → task-relevant memory capsule (bounded, ~2.8KB)
|
|
83
100
|
// → peers: none
|
|
84
101
|
```
|
|
@@ -86,7 +103,8 @@ brain_sync { intent: "rewrite the auth token refresh", files: ["src/auth/token.t
|
|
|
86
103
|
Session B — Codex, same repo, half a minute later:
|
|
87
104
|
|
|
88
105
|
```jsonc
|
|
89
|
-
brain_sync {
|
|
106
|
+
brain_sync { project: "/absolute/path/to/project", phase: "start",
|
|
107
|
+
intent: "add rate limiting to the auth routes",
|
|
90
108
|
files: ["src/auth/token.ts", "src/auth/routes.ts"] }
|
|
91
109
|
// → task capsule
|
|
92
110
|
// → peers: 1 active session (claude-code) — "rewrite the auth token refresh"
|
|
@@ -100,7 +118,7 @@ expected to touch.
|
|
|
100
118
|
Then the brain pushes back before the decision, not after:
|
|
101
119
|
|
|
102
120
|
```jsonc
|
|
103
|
-
brain_challenge { "move token storage to localStorage" }
|
|
121
|
+
brain_challenge { claim: "move token storage to localStorage" }
|
|
104
122
|
// → "reversed on June 12 — here's the correction card, captured by a different agent."
|
|
105
123
|
```
|
|
106
124
|
|
|
@@ -213,6 +231,59 @@ npx klypix-mcp link --check # audits without writing; exits non-zero on drift
|
|
|
213
231
|
[KLYPIX app](https://klypix.com) does it in one click (*Save canvas as project brain*), or
|
|
214
232
|
`create_canvas` makes one from any agent.
|
|
215
233
|
|
|
234
|
+
### Your first decision and correction
|
|
235
|
+
|
|
236
|
+
After installation, reopen your coding tool or reconnect its MCP server, and allow the KLYPIX
|
|
237
|
+
tools if your host asks. Run `npx klypix-mcp doctor` in the project to inspect the installation;
|
|
238
|
+
a clean config report does not prove that your host has loaded the tools.
|
|
239
|
+
|
|
240
|
+
Try the following in a **disposable trial project**, or replace the example decision with a real
|
|
241
|
+
one before saving it. These are MCP tool calls for your agent, not shell commands. Replace
|
|
242
|
+
`/absolute/path/to/project` with your project root throughout. The `files: []` scope means this
|
|
243
|
+
trial does not plan source-code edits.
|
|
244
|
+
|
|
245
|
+
1. **Record a decision and its reason.** Ask your agent to call:
|
|
246
|
+
|
|
247
|
+
```jsonc
|
|
248
|
+
brain_sync { project: "/absolute/path/to/project", phase: "start",
|
|
249
|
+
intent: "Record the trial storage decision", files: [] }
|
|
250
|
+
brain_note { area: "Storage",
|
|
251
|
+
text: "Use SQLite for the trial project database because this prototype runs on one machine." }
|
|
252
|
+
brain_sync { project: "/absolute/path/to/project", phase: "complete" }
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
2. **Open a fresh session in the same project**, optionally in a different supported coding tool.
|
|
256
|
+
Ask it to retrieve the decision rather than infer the answer from your code:
|
|
257
|
+
|
|
258
|
+
```jsonc
|
|
259
|
+
brain_sync { project: "/absolute/path/to/project", phase: "start",
|
|
260
|
+
intent: "Review the trial storage decision", files: [] }
|
|
261
|
+
brain_ask { question: "Which database does the trial project use, and why?" }
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Check that the returned card contains SQLite and the one-machine rationale. If the tool is
|
|
265
|
+
missing, reconnect the host and check the [host integration table](#supported-hosts-and-their-integration-level).
|
|
266
|
+
If the card is missing, confirm both sessions used the same project and that `brain_note`
|
|
267
|
+
reported a successful capture. A fresh brain only knows what has been recorded.
|
|
268
|
+
|
|
269
|
+
3. **Change the trial decision explicitly**, then ask again:
|
|
270
|
+
|
|
271
|
+
```jsonc
|
|
272
|
+
brain_note { area: "Storage",
|
|
273
|
+
text: "CORRECTION: Use PostgreSQL instead of SQLite for the trial project database because the trial now needs concurrent writes from several machines." }
|
|
274
|
+
brain_ask { question: "Which database does the trial project use now, and why?" }
|
|
275
|
+
brain_sync { project: "/absolute/path/to/project", phase: "complete" }
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Inspect the returned source cards: PostgreSQL should be the current decision, with SQLite
|
|
279
|
+
retained as superseded history. Confirm that the correction names the earlier decision; a
|
|
280
|
+
correction cue alone is not a guarantee of a match. This small trial checks your capture and
|
|
281
|
+
recall path. It does not establish retrieval quality for a larger brain or prove host automation.
|
|
282
|
+
|
|
283
|
+
For a team on different machines, follow the [Git sharing steps](https://klypix.com/guides/shared-project-memory#team)
|
|
284
|
+
and install in each clone. Standalone live presence and file-overlap warnings stay local to one
|
|
285
|
+
machine and OS user. Keep approved architecture records and stable conventions alongside the brain.
|
|
286
|
+
|
|
216
287
|
---
|
|
217
288
|
|
|
218
289
|
## How the brain works
|
|
@@ -597,16 +668,29 @@ and the person opens Codex (or a fresh Claude Code) and says *continue*. Until 1
|
|
|
597
668
|
handoff was whatever the person pasted. Now there are two layers, and the person does not have
|
|
598
669
|
to ask for either.
|
|
599
670
|
|
|
600
|
-
**Automatic
|
|
601
|
-
remembers sessions for two weeks
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
the
|
|
671
|
+
**Automatic — whichever agent stopped.** Each session's last state is recorded on the lane that
|
|
672
|
+
already remembers sessions for two weeks, by whatever that host offers:
|
|
673
|
+
|
|
674
|
+
| Host | Recorded automatically |
|
|
675
|
+
|---|---|
|
|
676
|
+
| Claude Code (hooks) | last prompt, the tail of the last reply, files edited, its *own* commits |
|
|
677
|
+
| Codex (hooks) | last prompt, the tail of the last reply, files edited, its *own* commits |
|
|
678
|
+
| Any MCP host — Cursor, VS Code, Windsurf, Gemini CLI, … | the task and files it declared with `brain_sync` |
|
|
679
|
+
|
|
680
|
+
*Own* commits are the shas that session's `git commit` printed, never a branch range, because
|
|
681
|
+
twenty sessions can share one checkout. A session is listed when it stopped in the last 24 hours
|
|
682
|
+
without completing its task — quiet for ten minutes, or closed — and left a record. A session that
|
|
683
|
+
recorded nothing is never listed, so finished chats do not pile up.
|
|
684
|
+
|
|
685
|
+
The next session on the project, in any agent, sees **⏸️ Work other sessions may have left
|
|
686
|
+
unfinished** at its first `brain_sync` when its task is to continue something ("continue what Codex
|
|
687
|
+
was doing on the invoice" puts that session first) or touches a file one of them touched. It sees the
|
|
688
|
+
same list at session start. Each record carries its status, files, commits, last words and resume
|
|
689
|
+
command. It says, in the text and in the JSON (`state`, `working:false`, `holdsFiles:false`), that
|
|
690
|
+
the session is not running, holds no claim on any file, and that a "do not touch" note it left no
|
|
691
|
+
longer applies: the list is history, not a lock. The agent takes a record over with
|
|
692
|
+
`brain_handoff {action:"take", session:"<id>"}` so peers see it is taken, then marks it `done`. This
|
|
693
|
+
layer is machine-local, like the rest of the lane.
|
|
610
694
|
|
|
611
695
|
**Deliberate.** When a session knows it is stopping — the person asks for a handoff, the context or
|
|
612
696
|
usage limit is near, the day ends, it is blocked, it is switching tool — it calls
|
|
@@ -1174,8 +1258,11 @@ Read this section before you build on any of it.
|
|
|
1174
1258
|
that list together with the shared one.
|
|
1175
1259
|
- **`npx klypix-mcp link` does not manage `CLAUDE.md`.** It manages `AGENTS.md` and seven other
|
|
1176
1260
|
rules files. Only the desktop app writes `CLAUDE.md`.
|
|
1177
|
-
- **
|
|
1178
|
-
|
|
1261
|
+
- **Semantic retrieval depends on the install path.** An interactive terminal install enables
|
|
1262
|
+
the on-device search runtime by default. CI and noninteractive installs need `--semantic`;
|
|
1263
|
+
`--no-semantic` (or `KLYPIX_SEMANTIC=0`) opts out. The Claude plugin stays keyword-only. Without
|
|
1264
|
+
the model, retrieval uses keywords; a model download or a brain still being prepared is not a
|
|
1265
|
+
guarantee that a particular decision will be found.
|
|
1179
1266
|
- **The capture lock is fail-open** past ~3.6 seconds of contention (see *Git and concurrency*).
|
|
1180
1267
|
- **`test/` is not in the published tarball.** Run the suite from a clone. The publish workflow
|
|
1181
1268
|
*does* gate on it — a `gate` job runs `npm ci`, asserts the test chain is intact, runs `npm test`,
|
|
@@ -1208,7 +1295,8 @@ independently validated.
|
|
|
1208
1295
|
2026-10-11 on the same brain grown to 3,829 cards (102 of the frozen questions still answerable),
|
|
1209
1296
|
with words written by Gemini 2.5 Flash as a stand-in for the calling agent: recall@5 54% → 59%
|
|
1210
1297
|
(8 found, 3 lost), recall@8 61% → 68%, recall@20 68% → 76%, MRR 0.375 → 0.424. With no local
|
|
1211
|
-
model (
|
|
1298
|
+
model (for example, an install with `--no-semantic` or the Claude plugin) the words count as extra
|
|
1299
|
+
keywords at half weight:
|
|
1212
1300
|
recall@5 19% → 24% (5 found, none lost), recall@10 25% → 31%. Without the line, ranking is
|
|
1213
1301
|
unchanged byte for byte.
|
|
1214
1302
|
- **Second brain.** The same ranker on a different project's brain (1,601 cards, 19 hand-written
|
|
@@ -1240,14 +1328,17 @@ independently validated.
|
|
|
1240
1328
|
## Uninstall
|
|
1241
1329
|
|
|
1242
1330
|
```bash
|
|
1243
|
-
npx klypix-mcp uninstall --check
|
|
1244
|
-
npx klypix-mcp uninstall
|
|
1245
|
-
npx klypix-mcp uninstall unlink
|
|
1331
|
+
npx klypix-mcp uninstall --check # preview machine-global removal — writes nothing
|
|
1332
|
+
npx klypix-mcp uninstall --yes # remove the machine-global install
|
|
1333
|
+
npx klypix-mcp uninstall unlink --check # preview this project's managed files
|
|
1334
|
+
npx klypix-mcp uninstall unlink --yes # remove this project's managed files
|
|
1246
1335
|
```
|
|
1247
1336
|
|
|
1248
1337
|
It strips only KLYPIX's own entries — every other hook and setting in
|
|
1249
1338
|
`~/.claude/settings.json` stays — backs up each file it edits, and **never deletes a `.klypix`**.
|
|
1250
|
-
`--yes
|
|
1339
|
+
Removal requires `--yes`. Without it, the command prints the plan and changes nothing;
|
|
1340
|
+
there is no interactive confirmation prompt. Run the project-local form in each project you
|
|
1341
|
+
want to unlink. A preview exits non-zero while managed files remain.
|
|
1251
1342
|
|
|
1252
1343
|
Your `brain.klypix` is yours — it is a plain ZIP and stays readable with or without this package.
|
|
1253
1344
|
|
|
@@ -1266,10 +1357,9 @@ are not in the published tarball, though the publish workflow does run them as a
|
|
|
1266
1357
|
|
|
1267
1358
|
## Why this exists
|
|
1268
1359
|
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
and write.
|
|
1360
|
+
Coding tools have their own memory and instruction systems. A team using several tools still
|
|
1361
|
+
needs a shared place for decisions, reasons and corrections. Keeping that record in a file you
|
|
1362
|
+
own lets supported agents read and update it through the same interface.
|
|
1273
1363
|
|
|
1274
1364
|
**Your project, your file, any supported agent, offline.**
|
|
1275
1365
|
|
package/bin/klypix-worker.mjs
CHANGED
|
@@ -1418,13 +1418,28 @@ server.registerTool('brain_sync', {
|
|
|
1418
1418
|
const wantsContinue = HANDOFF_CONTINUE_RE.test(String(intent || ''));
|
|
1419
1419
|
const normPath = (p) => String(p || '').replace(/\\/g, '/').toLowerCase();
|
|
1420
1420
|
const mine = new Set((Array.isArray(files) ? files : []).map(normPath));
|
|
1421
|
-
const
|
|
1422
|
-
|
|
1421
|
+
const overlaps = (r) => Array.isArray(r.files) && r.files.some((p) => mine.has(normPath(p)));
|
|
1422
|
+
// "continue what codex was doing on the invoice": rank by what the
|
|
1423
|
+
// request names — the agent, words of the task, last words, files,
|
|
1424
|
+
// commit subjects — then by recency, so the one meant leads the list.
|
|
1425
|
+
const words = new Set(String(intent || '').toLowerCase().match(/[\p{L}\p{N}_-]{3,}/gu) || []);
|
|
1426
|
+
for (const w of ['continue', 'resume', 'what', 'was', 'were', 'doing', 'the', 'and', 'left', 'off', 'where', 'session', 'other', 'work', 'pick', 'carry', 'keep', 'going', 'finish', 'take', 'over', 'handoff', 'hand']) words.delete(w);
|
|
1427
|
+
const relevance = (r) => {
|
|
1428
|
+
const hay = [r.client === 'claude-code' ? 'claude' : r.client, r.intent, r.lastPrompt, r.lastWords, ...(r.files || []), ...(r.commits || []).map((c) => c.subject)].join(' ').toLowerCase();
|
|
1429
|
+
let n = 0;
|
|
1430
|
+
for (const w of words) if (hay.includes(w)) n++;
|
|
1431
|
+
return n + (overlaps(r) ? 3 : 0);
|
|
1432
|
+
};
|
|
1433
|
+
const lane = (h?.structured?.unfinished || [])
|
|
1434
|
+
.filter((r) => wantsContinue || overlaps(r))
|
|
1435
|
+
.map((r) => ({ r, score: relevance(r) }))
|
|
1436
|
+
.sort((a, b) => b.score - a.score || Number(b.r.lastSeen || 0) - Number(a.r.lastSeen || 0))
|
|
1437
|
+
.map(({ r }) => r);
|
|
1423
1438
|
if (!h?.isError && (cards.length || lane.length)) {
|
|
1424
1439
|
const now = Date.now();
|
|
1425
1440
|
handoffText = [
|
|
1426
1441
|
formatHandoffCards(cards, { now, max: 5 }),
|
|
1427
|
-
formatUnfinishedWork(lane, { now, max: 4 }),
|
|
1442
|
+
formatUnfinishedWork(lane, { now, max: 4, ranked: wantsContinue && words.size > 0 }),
|
|
1428
1443
|
].filter(Boolean).join('\n\n');
|
|
1429
1444
|
handoffData = {
|
|
1430
1445
|
cards: cards.length,
|
package/package.json
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "klypix-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.101.1",
|
|
4
4
|
"mcpName": "io.github.dahshanlabs/klypix-mcp",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "Shared project memory for Claude Code, Codex and Cursor: decisions and corrections through MCP.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "Apache-2.0",
|
|
8
8
|
"keywords": [
|
|
9
9
|
"mcp",
|
|
10
10
|
"model-context-protocol",
|
|
11
|
+
"mcp-server",
|
|
12
|
+
"claude-code",
|
|
13
|
+
"codex",
|
|
14
|
+
"cursor",
|
|
15
|
+
"shared-memory",
|
|
11
16
|
"multi-agent",
|
|
12
17
|
"agent-coordination",
|
|
13
18
|
"project-memory",
|
|
@@ -24,7 +29,7 @@
|
|
|
24
29
|
"klypix",
|
|
25
30
|
"ai"
|
|
26
31
|
],
|
|
27
|
-
"homepage": "https://
|
|
32
|
+
"homepage": "https://klypix.com/developers",
|
|
28
33
|
"repository": {
|
|
29
34
|
"type": "git",
|
|
30
35
|
"url": "https://github.com/dahshanlabs/klypix-mcp"
|
package/src/agent-presence.mjs
CHANGED
|
@@ -535,12 +535,28 @@ export function normalizeWork(raw) {
|
|
|
535
535
|
out.takenBy = { id: str(raw.takenBy.id, 160), client: str(raw.takenBy.client, 40) || 'unknown', at: ms(raw.takenBy.at) };
|
|
536
536
|
}
|
|
537
537
|
if (ms(raw.doneAt)) { out.doneAt = ms(raw.doneAt); if (raw.doneBy) out.doneBy = str(raw.doneBy, 160); }
|
|
538
|
+
// 1.99.1 (universal): a task DECLARED through brain_sync — any MCP host,
|
|
539
|
+
// hooks or not — stamps when it was declared, so a Cursor / Gemini / VS Code
|
|
540
|
+
// session that stopped mid-task is still found by the next session.
|
|
541
|
+
if (ms(raw.declaredAt)) out.declaredAt = ms(raw.declaredAt);
|
|
538
542
|
return Object.keys(out).length ? out : null;
|
|
539
543
|
}
|
|
540
544
|
export function mergeWork(previous, patch) {
|
|
541
545
|
if (patch === undefined) return normalizeWork(previous);
|
|
542
546
|
if (patch === null) return null;
|
|
543
|
-
|
|
547
|
+
const prev = normalizeWork(previous) || {};
|
|
548
|
+
const next = patch && typeof patch === 'object' ? { ...patch } : {};
|
|
549
|
+
// `addCommits` APPENDS (Codex records one commit per tool result; the
|
|
550
|
+
// Claude Stop hook replaces the full list with `commits`), newest last,
|
|
551
|
+
// de-duplicated by sha, capped like `commits`.
|
|
552
|
+
if (Array.isArray(next.addCommits)) {
|
|
553
|
+
const seen = new Set();
|
|
554
|
+
next.commits = [...(prev.commits || []), ...next.addCommits]
|
|
555
|
+
.filter((c) => c && c.sha && !seen.has(String(c.sha).slice(0, 7)) && seen.add(String(c.sha).slice(0, 7)))
|
|
556
|
+
.slice(-WORK_COMMITS_MAX);
|
|
557
|
+
delete next.addCommits;
|
|
558
|
+
}
|
|
559
|
+
return normalizeWork({ ...prev, ...next });
|
|
544
560
|
}
|
|
545
561
|
const pruneDirectory = (rows, now) => (Array.isArray(rows) ? rows : [])
|
|
546
562
|
.filter((row) => row && recipientKey(row.id) && now - Number(row.lastSeen || 0) < DIRECTORY_FRESH_MS)
|
|
@@ -1186,7 +1202,14 @@ export function upsertSession({
|
|
|
1186
1202
|
...(statusDigestHash !== undefined ? { statusDigestHash: String(statusDigestHash || '').slice(0, 16) } : {}),
|
|
1187
1203
|
// ADDITIVE unfinished-work state: merged only when this touch carries a
|
|
1188
1204
|
// patch; the `...previous` spread keeps it verbatim otherwise.
|
|
1189
|
-
...(
|
|
1205
|
+
...(() => {
|
|
1206
|
+
// A brain_sync-DECLARED task (any MCP host) is itself a work record:
|
|
1207
|
+
// stamp declaredAt beside whatever patch this touch carries.
|
|
1208
|
+
const declared = intentSource === 'declared' && intentChanged && nextIntent ? { declaredAt: now } : null;
|
|
1209
|
+
if (work === undefined && !declared) return {};
|
|
1210
|
+
const merged = mergeWork(previous.work, { ...(work && typeof work === 'object' ? work : {}), ...(declared || {}) });
|
|
1211
|
+
return merged ? { work: merged } : {};
|
|
1212
|
+
})(),
|
|
1190
1213
|
// ADDITIVE host status: stamped only when this touch carries one, kept
|
|
1191
1214
|
// verbatim otherwise, so a heartbeat never turns "working" into unknown.
|
|
1192
1215
|
...(nextHostStatus
|
|
@@ -2914,7 +2937,10 @@ export const UNFINISHED_QUIET_MS = 10 * 60 * 1000;
|
|
|
2914
2937
|
// and every record says, in text and JSON, that it is not running and holds
|
|
2915
2938
|
// no claim on any file.
|
|
2916
2939
|
export const UNFINISHED_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
2917
|
-
|
|
2940
|
+
// Evidence of real work, from ANY host: a reply tail (Claude and Codex Stop
|
|
2941
|
+
// hooks), own commits (Claude transcript, Codex tool results), a handoff card
|
|
2942
|
+
// (brain_handoff), or a task declared through brain_sync (every MCP host).
|
|
2943
|
+
export const hasWorkEvidence = (work) => Boolean(work && (work.lastWords || (Array.isArray(work.commits) && work.commits.length) || work.handoffCardId || work.declaredAt));
|
|
2918
2944
|
export function clientWord(client) {
|
|
2919
2945
|
const key = String(client || '').toLowerCase();
|
|
2920
2946
|
if (key === 'claude-code' || key === 'claude') return 'Claude Code';
|
|
@@ -3026,11 +3052,11 @@ export function stampSessionWork({ brainPath, sessionId, patch, home, now = Date
|
|
|
3026
3052
|
// one record per unfinished session, newest first, then how to take it over.
|
|
3027
3053
|
// Quoted text is another session's record and is labelled as such — never as
|
|
3028
3054
|
// the human's instructions; markers inside it were neutralized at store time.
|
|
3029
|
-
export function formatUnfinishedWork(records, { now = Date.now(), max = 5, cardTitles = null } = {}) {
|
|
3055
|
+
export function formatUnfinishedWork(records, { now = Date.now(), max = 5, cardTitles = null, ranked = false } = {}) {
|
|
3030
3056
|
const list = Array.isArray(records) ? records : [];
|
|
3031
3057
|
if (!list.length) return '';
|
|
3032
3058
|
const flat = (value) => String(value || '').replace(/\s+/g, ' ').trim();
|
|
3033
|
-
const lines = [`⏸️ Work other sessions may have left unfinished (${list.length}, stopped in the last 24h, newest first). None of them is running: they hold NO claim on any file, and a "do not touch" or WIP note one of them left no longer applies. Use this only if the human asks you to continue their work:`];
|
|
3059
|
+
const lines = [`⏸️ Work other sessions may have left unfinished (${list.length}, stopped in the last 24h, ${ranked ? 'most relevant first' : 'newest first'}). None of them is running: they hold NO claim on any file, and a "do not touch" or WIP note one of them left no longer applies. Use this only if the human asks you to continue their work:`];
|
|
3034
3060
|
for (const r of list.slice(0, max)) {
|
|
3035
3061
|
const who = `${clientWord(r.client)} ${String(r.id).slice(0, 8)}`;
|
|
3036
3062
|
const bits = [r.state === 'ended' ? 'ended' : `stopped (${r.status})`, `last active ${agoLabel(r.ageMs)}`];
|
package/src/codex-brain-hook.mjs
CHANGED
|
@@ -371,6 +371,8 @@ async function statusDigestContext({ brainPath, sessionId, prompt, sessions, cwd
|
|
|
371
371
|
// ⏸️ Unfinished work other sessions left — the engine's block (agent-presence
|
|
372
372
|
// listUnfinishedWork + formatUnfinishedWork), read-only, never throws.
|
|
373
373
|
const CONTINUE_PROMPT_RE = /\b(continue|resume|pick (it |this |that )?up|carry on|keep going|where (were|did) we|what('s| is| was) (next|left|remaining|pending)|unfinished|left off|hand[- ]?off|handed (over|off)|take over|finish (what|the work|it|this))\b/i;
|
|
374
|
+
const HANDOFF_ASK_PROMPT_RE = /\b(hand[- ]?off|hand (it|this|the work) (over|off)|before (we|i|you) (hit|reach|approach|run out of) (the )?(limit|context|window|quota)|(context|usage|token|rate) (limit|window|quota)|out of context|switch(ing)? to (claude|another (agent|session|tool)))\b/i;
|
|
375
|
+
const CODEX_HANDOFF_HINT = '🤝 KLYPIX: to hand this session\'s work to the next one (any agent — Claude Code or Codex), call brain_handoff with the task, what is done, what is next, the files and how to verify; the next session on this project sees it at its first brain_sync and takes it with {action:"take"}. Your last reply and commits are recorded automatically at each turn end regardless — but a deliberate handoff says what you know. Never make the human paste a summary into the next session.';
|
|
374
376
|
function unfinishedWorkBlock(brainPath, sessionId) {
|
|
375
377
|
try {
|
|
376
378
|
return formatUnfinishedWork(listUnfinishedWork({ brainPath, selfId: sessionId }));
|
|
@@ -390,8 +392,31 @@ function codexWorkPatch({ event, humanTurn, prompt, input }) {
|
|
|
390
392
|
const text = typeof words === 'string' ? words.trim() : '';
|
|
391
393
|
if (text) { patch.lastWords = text.length > 700 ? '…' + text.slice(-699) : text; patch.lastWordsAt = Date.now(); }
|
|
392
394
|
}
|
|
395
|
+
// 1.99.1 (universal): this session's OWN commits, from the output git
|
|
396
|
+
// printed for a commit it ran ("[branch abc1234] subject") — never a branch
|
|
397
|
+
// range, which would credit a peer's commits in a shared checkout.
|
|
398
|
+
if (event === 'PostToolUse') {
|
|
399
|
+
const commits = commitsFromToolResult(input);
|
|
400
|
+
if (commits.length) patch.addCommits = commits;
|
|
401
|
+
}
|
|
393
402
|
return Object.keys(patch).length ? patch : null;
|
|
394
403
|
}
|
|
404
|
+
function toolOutputText(input) {
|
|
405
|
+
const r = input.tool_response ?? input.toolResponse ?? input.tool_output ?? input.toolOutput;
|
|
406
|
+
if (typeof r === 'string') return r;
|
|
407
|
+
if (!r || typeof r !== 'object') return '';
|
|
408
|
+
const parts = [r.output, r.stdout, r.aggregated_output, r.formatted_output, r.text, r.content];
|
|
409
|
+
return parts.map((p) => (typeof p === 'string' ? p : Array.isArray(p) ? p.map((x) => (typeof x === 'string' ? x : x?.text || '')).join('\n') : '')).filter(Boolean).join('\n');
|
|
410
|
+
}
|
|
411
|
+
export function commitsFromToolResult(input) {
|
|
412
|
+
const ti = input.tool_input || input.toolInput || {};
|
|
413
|
+
const cmd = typeof ti === 'string' ? ti : [ti.command, ti.cmd, Array.isArray(ti.command) ? ti.command.join(' ') : ''].filter((v) => typeof v === 'string').join(' ');
|
|
414
|
+
if (!/\bgit\b[^\n]*\bcommit\b/.test(cmd)) return [];
|
|
415
|
+
const out = toolOutputText(input);
|
|
416
|
+
const commits = [];
|
|
417
|
+
for (const m of out.matchAll(/^\[[^\]\n]{1,80}?\s([0-9a-f]{7,40})\]\s+(.+)$/gm)) commits.push({ sha: m[1].slice(0, 12), subject: m[2].trim().slice(0, 120) });
|
|
418
|
+
return commits.slice(0, 6);
|
|
419
|
+
}
|
|
395
420
|
|
|
396
421
|
async function compactTaskContext(projectDir, prompt, files) {
|
|
397
422
|
if (!prompt) return '';
|
|
@@ -533,6 +558,8 @@ async function main() {
|
|
|
533
558
|
stampReceivedMessages(messages, Date.now(), undefined, sessionId),
|
|
534
559
|
// Only on a continue-shaped human prompt — SessionStart printed it once.
|
|
535
560
|
...(prompt && CONTINUE_PROMPT_RE.test(prompt) ? [unfinishedWorkBlock(brainPath, sessionId)] : []),
|
|
561
|
+
// The human asked for a handoff or named the limit (Codex has one too).
|
|
562
|
+
...(prompt && HANDOFF_ASK_PROMPT_RE.test(prompt) ? [CODEX_HANDOFF_HINT] : []),
|
|
536
563
|
], event);
|
|
537
564
|
return;
|
|
538
565
|
}
|
|
@@ -1086,6 +1086,7 @@ function workNorm(raw) {
|
|
|
1086
1086
|
if (raw.handoffCardId) out.handoffCardId = str(raw.handoffCardId, 64);
|
|
1087
1087
|
if (raw.takenBy && typeof raw.takenBy === 'object' && raw.takenBy.id) out.takenBy = { id: str(raw.takenBy.id, 160), client: str(raw.takenBy.client, 40) || 'unknown', at: ms(raw.takenBy.at) };
|
|
1088
1088
|
if (ms(raw.doneAt)) { out.doneAt = ms(raw.doneAt); if (raw.doneBy) out.doneBy = str(raw.doneBy, 160); }
|
|
1089
|
+
if (ms(raw.declaredAt)) out.declaredAt = ms(raw.declaredAt); // 1.99.1 twin: a brain_sync-declared task (any MCP host)
|
|
1089
1090
|
return Object.keys(out).length ? out : null;
|
|
1090
1091
|
}
|
|
1091
1092
|
const workMerge = (prev, patch) => (patch === undefined ? workNorm(prev) : patch === null ? null : workNorm({ ...(workNorm(prev) || {}), ...(patch && typeof patch === 'object' ? patch : {}) }));
|
|
@@ -1470,7 +1471,7 @@ const UNFINISHED_QUIET_MS = 10 * 60 * 1000;
|
|
|
1470
1471
|
// 1.99.1 twin of agent-presence UNFINISHED_WINDOW_MS / hasWorkEvidence: only a
|
|
1471
1472
|
// session that stopped in the last 24h AND left a 1.99+ work record is listed.
|
|
1472
1473
|
const UNFINISHED_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
1473
|
-
const hasWorkEvidence = (w) => Boolean(w && (w.lastWords || (Array.isArray(w.commits) && w.commits.length) || w.handoffCardId));
|
|
1474
|
+
const hasWorkEvidence = (w) => Boolean(w && (w.lastWords || (Array.isArray(w.commits) && w.commits.length) || w.handoffCardId || w.declaredAt));
|
|
1474
1475
|
const CONTINUE_RE = /\b(continue|resume|pick (it |this |that )?up|carry on|keep going|where (were|did) we|what('s| is| was) (next|left|remaining|pending)|unfinished|left off|hand[- ]?off|handed (over|off)|take over|finish (what|the work|it|this))\b/i;
|
|
1475
1476
|
const HANDOFF_ASK_RE = /\b(hand[- ]?off|hand (it|this|the work) (over|off)|before (we|i|you) (hit|reach|approach|run out of) (the )?(limit|context|window|quota)|(context|usage|token) (limit|window|quota)|out of context|switch(ing)? to (codex|another (agent|session|tool)))\b/i;
|
|
1476
1477
|
function unfinishedFooter(sid, { max = 4 } = {}) {
|