claude-code-infinite 1.0.32 → 1.0.34

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.
Files changed (99) hide show
  1. package/README.md +34 -11
  2. package/dist/cli-args.d.ts +7 -0
  3. package/dist/cli-args.d.ts.map +1 -1
  4. package/dist/cli-args.js +19 -0
  5. package/dist/cli-args.js.map +1 -1
  6. package/dist/cli-main.js +28 -10
  7. package/dist/cli-main.js.map +1 -1
  8. package/dist/count-tokens.d.ts +7 -3
  9. package/dist/count-tokens.d.ts.map +1 -1
  10. package/dist/count-tokens.js +6 -2
  11. package/dist/count-tokens.js.map +1 -1
  12. package/dist/hooks.d.ts +21 -1
  13. package/dist/hooks.d.ts.map +1 -1
  14. package/dist/hooks.js +36 -6
  15. package/dist/hooks.js.map +1 -1
  16. package/dist/host-session.d.ts +12 -0
  17. package/dist/host-session.d.ts.map +1 -0
  18. package/dist/host-session.js +12 -0
  19. package/dist/host-session.js.map +1 -0
  20. package/dist/image-refs.d.ts +24 -2
  21. package/dist/image-refs.d.ts.map +1 -1
  22. package/dist/image-refs.js +67 -20
  23. package/dist/image-refs.js.map +1 -1
  24. package/dist/index-wait-413.d.ts +26 -0
  25. package/dist/index-wait-413.d.ts.map +1 -0
  26. package/dist/index-wait-413.js +113 -0
  27. package/dist/index-wait-413.js.map +1 -0
  28. package/dist/memtree-finder.d.ts +70 -6
  29. package/dist/memtree-finder.d.ts.map +1 -1
  30. package/dist/memtree-finder.js +150 -20
  31. package/dist/memtree-finder.js.map +1 -1
  32. package/dist/memtree-handover.d.ts +13 -0
  33. package/dist/memtree-handover.d.ts.map +1 -0
  34. package/dist/memtree-handover.js +231 -0
  35. package/dist/memtree-handover.js.map +1 -0
  36. package/dist/memtree-mcp-config.d.ts +2 -2
  37. package/dist/memtree-mcp-config.d.ts.map +1 -1
  38. package/dist/memtree-mcp-config.js +8 -5
  39. package/dist/memtree-mcp-config.js.map +1 -1
  40. package/dist/memtree-mcp.d.ts +182 -2
  41. package/dist/memtree-mcp.d.ts.map +1 -1
  42. package/dist/memtree-mcp.js +357 -38
  43. package/dist/memtree-mcp.js.map +1 -1
  44. package/dist/memtree-newest.d.ts +21 -3
  45. package/dist/memtree-newest.d.ts.map +1 -1
  46. package/dist/memtree-newest.js +43 -11
  47. package/dist/memtree-newest.js.map +1 -1
  48. package/dist/memtree-times.d.ts +33 -0
  49. package/dist/memtree-times.d.ts.map +1 -1
  50. package/dist/memtree-times.js +77 -0
  51. package/dist/memtree-times.js.map +1 -1
  52. package/dist/memtree-tools.d.ts +19 -5
  53. package/dist/memtree-tools.d.ts.map +1 -1
  54. package/dist/memtree-tools.js +97 -23
  55. package/dist/memtree-tools.js.map +1 -1
  56. package/dist/memtree.d.ts +109 -3
  57. package/dist/memtree.d.ts.map +1 -1
  58. package/dist/memtree.js +382 -21
  59. package/dist/memtree.js.map +1 -1
  60. package/dist/network-failure.d.ts +28 -0
  61. package/dist/network-failure.d.ts.map +1 -0
  62. package/dist/network-failure.js +67 -0
  63. package/dist/network-failure.js.map +1 -0
  64. package/dist/open-url.d.ts +49 -0
  65. package/dist/open-url.d.ts.map +1 -0
  66. package/dist/open-url.js +100 -0
  67. package/dist/open-url.js.map +1 -0
  68. package/dist/prefix-store.d.ts +4 -0
  69. package/dist/prefix-store.d.ts.map +1 -1
  70. package/dist/prefix-store.js +8 -4
  71. package/dist/prefix-store.js.map +1 -1
  72. package/dist/project-trees.d.ts +15 -7
  73. package/dist/project-trees.d.ts.map +1 -1
  74. package/dist/project-trees.js +20 -9
  75. package/dist/project-trees.js.map +1 -1
  76. package/dist/proxy.d.ts +125 -1
  77. package/dist/proxy.d.ts.map +1 -1
  78. package/dist/proxy.js +838 -74
  79. package/dist/proxy.js.map +1 -1
  80. package/dist/reqlog.d.ts +34 -0
  81. package/dist/reqlog.d.ts.map +1 -1
  82. package/dist/reqlog.js.map +1 -1
  83. package/dist/resume-guidance.d.ts +9 -3
  84. package/dist/resume-guidance.d.ts.map +1 -1
  85. package/dist/resume-guidance.js +9 -3
  86. package/dist/resume-guidance.js.map +1 -1
  87. package/dist/route-fallback.d.ts +77 -11
  88. package/dist/route-fallback.d.ts.map +1 -1
  89. package/dist/route-fallback.js +157 -33
  90. package/dist/route-fallback.js.map +1 -1
  91. package/dist/route-identity.d.ts +6 -2
  92. package/dist/route-identity.d.ts.map +1 -1
  93. package/dist/route-identity.js +209 -16
  94. package/dist/route-identity.js.map +1 -1
  95. package/dist/subagent-hint.d.ts +28 -0
  96. package/dist/subagent-hint.d.ts.map +1 -1
  97. package/dist/subagent-hint.js +37 -0
  98. package/dist/subagent-hint.js.map +1 -1
  99. package/package.json +1 -1
@@ -18,8 +18,9 @@
18
18
  * tree the current request was compressed against. The session id is the
19
19
  * `CLAUDE_CODE_SESSION_ID` Claude Code sets for MCP servers. Reads of this
20
20
  * session's current tree require that exact session on both the proxy's pointer
21
- * and the page; missing or mismatched identity fails closed, and the host must
22
- * restart this server with the current id when switching sessions. The page JSON
21
+ * and the page; missing or mismatched identity fails closed. Claude Code does not
22
+ * restart this server when its process switches sessions, so each tool call first
23
+ * asks the proxy which session that process is on (`refreshHostSession`). The page JSON
23
24
  * is fetched through the proxy's key-free `/memtree/<id>.json` relay and cached
24
25
  * once complete.
25
26
  *
@@ -32,11 +33,14 @@
32
33
  * and falls back to searching the page JSON locally on a server without that
33
34
  * endpoint.
34
35
  */
36
+ import { HOST_TOKEN_ENV, HOST_TOKEN_HEADER } from "./host-session.js";
35
37
  import fs from "node:fs";
36
38
  import { fileURLToPath } from "node:url";
37
- import { FINDER_SEARCH_DEFAULT_LIMIT, FINDER_SEARCH_MAX_LIMIT, formatSearchResults, searchMode, formatSessions, searchQuery, SESSIONS_DEFAULT_LIMIT, SESSIONS_MAX_LIMIT, sessionsQuery, } from "./memtree-finder.js";
39
+ import { FINDER_SEARCH_DEFAULT_LIMIT, FINDER_SEARCH_MAX_LIMIT, formatSearchResults, searchMode, formatSessions, RECENT_SEARCH_WINDOWS_DAYS, recentSearchArgs, recentSearchFailedNote, recentSearchNote, searchQuery, SESSIONS_DEFAULT_LIMIT, SESSIONS_MAX_LIMIT, sessionsQuery, } from "./memtree-finder.js";
38
40
  import { formatLines, formatNode, formatSearch, formatSearchHits, formatTail, MemtreeIndex, parseNodeAddress, serverSearchHits, serverTailHits, READ_LINES_MAX_CHARS, READ_LINES_MAX_LINES, SEARCH_DEFAULT_LIMIT, SEARCH_MAX_LIMIT, ToolInputError, } from "./memtree-tools.js";
39
41
  import { CLIENT_VERSION } from "./memtree.js";
42
+ import { formatHandover, formatUserMessages, HANDOVER_DEFAULT_DAYS, HANDOVER_MAX_DAYS, handoverQuery, USER_MESSAGES_MAX_LIMIT, userMessagesQuery, } from "./memtree-handover.js";
43
+ import { formatTime } from "./memtree-times.js";
40
44
  export const MEMTREE_MCP_SERVER_NAME = "memtree";
41
45
  /**
42
46
  * The version of the ccc whose proxy this server talks to, set in the config
@@ -54,17 +58,71 @@ export function versionMismatchNote(proxyVersion, ownVersion = CLIENT_VERSION) {
54
58
  const safe = proxyVersion.replace(/[^0-9A-Za-z.+-]/g, "").slice(0, 40);
55
59
  return `(These MemTree tools are ccc ${ownVersion} but this session runs ccc ${safe}: ccc was updated during the session. Restart ccc to use one version.)`;
56
60
  }
61
+ /** How often a tool call may re-read the installed package.json (StaleToolsNotice). */
62
+ const STALE_CHECK_INTERVAL_MS = 60_000;
63
+ /**
64
+ * Says once per session, after a tool's answer, that ccc was updated on disk since
65
+ * this server started: Claude Code keeps an MCP server for the whole session, so
66
+ * the tools stay the version it loaded (an older one may lack message times) until
67
+ * ccc restarts. Silent when the versions match or package.json is unreadable.
68
+ */
69
+ export class StaleToolsNotice {
70
+ readInstalled;
71
+ own;
72
+ now;
73
+ startedAt;
74
+ said = false;
75
+ checkedAt = -Infinity;
76
+ constructor(readInstalled = installedVersion, own = CLIENT_VERSION, now = Date.now, startedAt = new Date()) {
77
+ this.readInstalled = readInstalled;
78
+ this.own = own;
79
+ this.now = now;
80
+ this.startedAt = startedAt;
81
+ }
82
+ next() {
83
+ if (this.said || this.now() - this.checkedAt < STALE_CHECK_INTERVAL_MS)
84
+ return undefined;
85
+ this.checkedAt = this.now();
86
+ const installed = this.readInstalled();
87
+ if (!installed || installed === this.own)
88
+ return undefined;
89
+ this.said = true;
90
+ return staleToolsNote(installed, this.own, this.startedAt);
91
+ }
92
+ }
93
+ export function staleToolsNote(installed, own, startedAt) {
94
+ const safe = installed.replace(/[^0-9A-Za-z.+-]/g, "").slice(0, 40);
95
+ const loaded = formatTime(startedAt.toISOString());
96
+ return `(These MemTree tools are ccc ${own}${loaded ? `, loaded ${loaded}` : ""}; ccc ${safe} is installed: restart ccc to use its newer MemTree tools.)`;
97
+ }
98
+ /** The version in this install's package.json now (ccc may have been updated since it started). */
99
+ export function installedVersion() {
100
+ try {
101
+ const version = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf-8")).version;
102
+ return typeof version === "string" ? version : undefined;
103
+ }
104
+ catch {
105
+ return undefined;
106
+ }
107
+ }
57
108
  /** Sent to MemTree as `x-memtree-tools` when this server is configured. */
58
109
  export const MEMTREE_TOOL_NAMES = [
59
110
  "search",
60
111
  "read_node",
61
112
  "read_lines",
62
113
  "list",
114
+ "user_messages",
115
+ "handover",
63
116
  ];
64
117
  /** `tree` value naming this session's own current tree. */
65
118
  export const CURRENT_TREE = "current";
66
119
  const LATEST_PROTOCOL_VERSION = "2025-06-18";
67
120
  const FETCH_TIMEOUT_MS = 60_000;
121
+ /**
122
+ * A handover pack's first build runs a model call on the server (up to 90 s, plus loading
123
+ * the tree): wait longer for it. Past this, the server still finishes and caches the pack.
124
+ */
125
+ const HANDOVER_TIMEOUT_MS = 150_000;
68
126
  const PREFIX_RETRY_INTERVAL_MS = 45_000;
69
127
  /**
70
128
  * The MCP server's instructions, which Claude Code places in the system prompt
@@ -92,16 +150,20 @@ export const MEMTREE_MCP_INSTRUCTIONS = "This session runs in Claude Code Infini
92
150
  "and failed), search MemTree before re-deriving it from the code or guessing. The code shows what exists; the " +
93
151
  "memory shows what was decided and why. Read the exact lines (read_lines) when precision matters.\n\n" +
94
152
  "search with \"tree\": \"current\" looks in this session's MemTree; without tree it searches all of the user's " +
95
- "sessions, and list shows those " +
96
- "sessions by time and project. Text mode (default, free) matches exact words, ids and paths; vector mode matches " +
97
- "meaning: its first page is charged even when the query embedding is cached; cursor continuation is free, " +
98
- "including when the embedding must be regenerated. Hits may include an address (<tree>#<node>) and its path from the root. If the address is unavailable, read_lines with the hit’s tree.ref and range reads its exact lines. With an address: " +
153
+ "ccc sessions on any machine (not Codex or plain claude sessions), and list shows those sessions by time and " +
154
+ "project. Text mode (default) matches exact words, ids and paths; vector mode matches meaning. A search costs " +
155
+ "$0.0005 either way: its first page is charged even when cached; cursor continuation is free, even if " +
156
+ "re-embedded, as is a text search of one tree. Hits may include an address (<tree>#<node>) and its path from the root. If the address is unavailable, read_lines with the hit’s tree.ref and range reads its exact lines. With an address: " +
99
157
  "read_node {\"node\": address} opens it with its children, and the path's addresses lead to its parent and " +
100
158
  "siblings; read_lines {\"node\": address} reads a leaf's exact lines. Cursors page over live results, not a " +
101
159
  "frozen snapshot. A tree trails its conversation: the newest messages (often how a session ended) are in no tree " +
102
160
  "yet. A text search of one tree also matches them (listed after the tree's hits), list shows how each session " +
103
161
  "ended, and read_lines {\"tail\": true} reads them (with tree for another session's page; large tool calls are " +
104
- "shortened, \"full\": true reads them whole).";
162
+ "shortened, \"full\": true reads them whole).\n\n" +
163
+ "To take over another session's work, call handover with its session id first; user_messages lists only the " +
164
+ "person's own messages over a time range.\n\n" +
165
+ "A hand-off or kickoff prompt you write for another agent or machine names this session's id and MemTree link " +
166
+ "(list marks this session), so that agent can read this work with list, search and read_node.";
105
167
  const TREE_DESC = "A tree reference from list or search (keep its style and own/served suffix; legacy request ids work), or \"current\" for this session's tree.";
106
168
  /**
107
169
  * Claude Code's per-tool key (2.1.296): the tool is in the prompt from the
@@ -123,13 +185,14 @@ export const MEMTREE_TOOLS = [
123
185
  name: "search",
124
186
  description: "Find what this conversation doesn't show: exact details from earlier in this session that the memory only " +
125
187
  "summarizes, or earlier work from the user's other sessions. Search one tree (tree, \"current\" for this session) " +
126
- "or, without tree, all of the user's sessions. Use it before re-deriving or guessing something from earlier work. " +
127
- "mode \"text\" (default, free) matches words, ids, paths and errors (in one tree: node summaries and transcript " +
128
- "lines); mode \"vector\" matches meaning, ranked per embedding model; its first page is charged even when the " +
129
- "query embedding is cached, and cursor continuation is free. Cursors page over live results, not a frozen snapshot. " +
188
+ "or, without tree, all of the user's ccc sessions on any machine (not Codex or plain claude sessions). " +
189
+ "Use it before re-deriving or guessing something from earlier work. " +
190
+ "mode \"text\" (default) matches words, ids, paths and errors (in one tree: node summaries and transcript " +
191
+ "lines); mode \"vector\" matches meaning, ranked per embedding model. A search costs $0.0005 either way: its " +
192
+ "first page is charged even when cached; cursor continuation and a text search of one tree are free. Cursors page over live results, not a frozen snapshot. " +
130
193
  "Hits include a snippet and range; their node address (<tree>#<node>) and path may be unavailable. Without an address, use read_lines with the hit’s tree.ref and range. " +
131
194
  "A text search of one tree also lists matching newest messages no tree covers yet (read them with read_lines tail). " +
132
- "Results show when the messages were written (per transcript block). " +
195
+ "Results show when the messages were written, each matched line its own message's time where recorded. " +
133
196
  // Live on every request, unlike the server instructions and Claude Code's
134
197
  // deferred-tools list, which a resumed session keeps from its first day.
135
198
  MEMTREE_LOADED_NOTE,
@@ -138,7 +201,7 @@ export const MEMTREE_TOOLS = [
138
201
  properties: {
139
202
  query: { type: "string", description: "Distinctive words (text; \"quoted phrase\", or, -word across sessions) or a question (vector)." },
140
203
  tree: { type: "string", description: `Optional. ${TREE_DESC} Omit to search all sessions.` },
141
- mode: { type: "string", enum: ["text", "vector"], description: "text (default, exact words, free) or vector (meaning, first page charged)." },
204
+ mode: { type: "string", enum: ["text", "vector"], description: "text (default, exact words) or vector (meaning); first page charged, except text in one tree." },
142
205
  project: { type: "string", description: "Without tree: only sessions in this working directory or git repository (owner/repo, or just repo)." },
143
206
  since: { type: "string", description: "Without tree: only requests at or after this ISO 8601 time or date." },
144
207
  until: { type: "string", description: "Without tree: only requests before this ISO 8601 time or date." },
@@ -152,7 +215,7 @@ export const MEMTREE_TOOLS = [
152
215
  name: "read_node",
153
216
  description: "Read one MemTree node: its summary, its path from the root (ancestor summaries), its children (id, summary, leaf or branch) and, " +
154
217
  "for a leaf, the transcript line range it covers. Give node (an address from search) or id (0 is the root) with an optional tree. " +
155
- "Results show when the messages were written (per transcript block).",
218
+ "Results show when the messages were written (a leaf: its own messages, where recorded).",
156
219
  inputSchema: {
157
220
  type: "object",
158
221
  properties: {
@@ -169,7 +232,7 @@ export const MEMTREE_TOOLS = [
169
232
  "With tail: true, read the messages after the tree that no tree covers yet (the end of a session usually is there): " +
170
233
  "the newest ones, or start/end message positions; add tree for another session's page. Tool calls and results over " +
171
234
  "2,000 characters or 40 lines come shortened to their first and last lines; add full: true to read them whole. " +
172
- "Results show when the messages were written (per transcript block).",
235
+ "Results show when the messages were written: each message header its own time, where recorded.",
173
236
  inputSchema: {
174
237
  type: "object",
175
238
  properties: {
@@ -185,7 +248,8 @@ export const MEMTREE_TOOLS = [
185
248
  },
186
249
  {
187
250
  name: "list",
188
- description: "List the user's own MemTree sessions (this one and others), most recently active first: title (the tree's root summary), " +
251
+ description: "List the user's own MemTree sessions, most recently active first: every ccc session on any machine using this " +
252
+ "account (this one marked; Codex and plain claude sessions are not included). Each: title (the tree's root summary), " +
189
253
  "first message, how it ended (last user message, the tools after it and the last reply, with the time), times, " +
190
254
  "project (directory, git repo, branch, commit), models, request count and the latest tree's reference. " +
191
255
  "Filter by time, project or words in the title. Free. Open a session's tree with read_node {\"tree\": <ref>, \"id\": 0} or search {\"tree\": <ref>, ...}.",
@@ -201,18 +265,61 @@ export const MEMTREE_TOOLS = [
201
265
  },
202
266
  },
203
267
  },
268
+ {
269
+ name: "user_messages",
270
+ description: "The person's own messages in a session over a time range: what they typed, commands they ran, words sent while " +
271
+ "the agent worked, and board messages. Tool results, reminders, prompts the agent scheduled and other agents' " +
272
+ "messages are left out. Use it to learn what the user asked for, decided and corrected. Covers the tree and its " +
273
+ "newest messages no tree covers yet. Returns the newest page, oldest first; cursor lists older ones. Each " +
274
+ "message says where read_lines shows its context. Results show when each message was written.",
275
+ inputSchema: {
276
+ type: "object",
277
+ properties: {
278
+ tree: { type: "string", description: `${TREE_DESC} A session id works too. Omit for this session.` },
279
+ since: { type: "string", description: "Only messages at or after this ISO 8601 time or date (UTC unless an offset is given)." },
280
+ until: { type: "string", description: "Only messages before this ISO 8601 time or date." },
281
+ query: { type: "string", description: "Only messages holding every one of these words." },
282
+ cursor: { type: "string", description: "next cursor from a previous call: older messages (same other arguments)." },
283
+ limit: { type: "number", description: `Messages per page (default 50, at most ${USER_MESSAGES_MAX_LIMIT}).` },
284
+ full: { type: "boolean", description: "Long messages and pastes whole (default: shortened)." },
285
+ context: { type: "boolean", description: "Also the agent's last words before each message (what it answered)." },
286
+ },
287
+ },
288
+ },
289
+ {
290
+ name: "handover",
291
+ description: "A handover pack for taking over or continuing a session's work: the person's messages over its last days (each " +
292
+ "with the agent's words it answered), how the session ended, and a model's reading of their goals, standing " +
293
+ "instructions, decisions with reasons and open questions, each citing its messages. Call it first when you take " +
294
+ "over another session (tree: its session id or tree). The model's lists can be wrong: check one against the " +
295
+ "messages it cites before acting on it. A first call for a session can take up to a minute.",
296
+ inputSchema: {
297
+ type: "object",
298
+ properties: {
299
+ tree: { type: "string", description: `${TREE_DESC} A session id works too. Omit for this session.` },
300
+ days: { type: "number", description: `Days before the session's newest message to cover (default ${HANDOVER_DEFAULT_DAYS}, at most ${HANDOVER_MAX_DAYS}).` },
301
+ since: { type: "string", description: "Instead of days: messages at or after this ISO 8601 time or date." },
302
+ until: { type: "string", description: "Only messages before this ISO 8601 time or date." },
303
+ },
304
+ },
305
+ },
204
306
  ].map((tool) => ({ ...tool, _meta: MEMTREE_TOOL_META }));
205
307
  /** A request id in either spelling (UUID or the short leading-hex form). */
206
308
  const TREE_ID_RE = /^[A-Za-z0-9-]{1,64}$/;
309
+ /** Pages of `list` results a session-id-as-tree lookup reads at most. */
310
+ const SESSION_TREE_MAX_PAGES = 3;
311
+ /** How long a session-id-as-tree answer is reused (its newest tree can change). */
312
+ const SESSION_TREE_TTL_MS = 60_000;
313
+ const SESSION_TREES_CACHED = 64;
207
314
  /** Other sessions' pages kept in memory at once (each can be several MB). */
208
315
  const OTHER_TREES_CACHED = 4;
209
316
  /** GET JSON from the loopback proxy; never throws. */
210
- async function proxyGetJson(fetchImpl, url, headers = {}) {
317
+ async function proxyGetJson(fetchImpl, url, headers = {}, timeoutMs = FETCH_TIMEOUT_MS) {
211
318
  let response;
212
319
  try {
213
320
  response = await fetchImpl(url, {
214
321
  headers: { accept: "application/json", ...headers },
215
- signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
322
+ signal: AbortSignal.timeout(timeoutMs),
216
323
  });
217
324
  }
218
325
  catch (err) {
@@ -238,6 +345,41 @@ async function proxyGetJson(fetchImpl, url, headers = {}) {
238
345
  return { ok: false, status: response.status, error: "response was not JSON" };
239
346
  }
240
347
  }
348
+ /**
349
+ * Sent with each tool's proxy reads: which tool (`page` is a whole tree's `.json`) and
350
+ * the calling session, which the proxy passes on so the server's one content-free log
351
+ * line per call can say which tool read what (polychat memtree_tool_log.py). Not to be
352
+ * confused with `x-memtree-tools`, the list of configured tools sent on compress calls.
353
+ */
354
+ export const MEMTREE_TOOL_HEADER = "x-memtree-tool";
355
+ export function toolHeaders(tool, deps) {
356
+ const headers = { [MEMTREE_TOOL_HEADER]: tool };
357
+ if (deps.sessionId)
358
+ headers["x-claude-code-session-id"] = deps.sessionId;
359
+ return headers;
360
+ }
361
+ /** How long a tool call waits to learn its process's session before using its own id. */
362
+ const HOST_SESSION_TIMEOUT_MS = 2_000;
363
+ /**
364
+ * Point `deps.sessionId` at the session this server's Claude Code process is on now.
365
+ * Claude Code starts the server with the session id it had at launch and does not
366
+ * restart it when the process moves to another session (a resume through the picker,
367
+ * /resume, /clear), so that id can name a session with no page ("no MemTree yet",
368
+ * 2026-11-09). The proxy of the ccc that launched this Claude Code knows the process's
369
+ * session from its SessionStart hook and answers only that process (`hostPid`: this
370
+ * server's parent, with the token from ccc's MCP config). Without a token (an older
371
+ * ccc's config: its proxy has no such route) nothing is asked; any other answer
372
+ * (another process, no session start seen) leaves the id as it is.
373
+ */
374
+ export async function refreshHostSession(deps, hostPid = process.ppid) {
375
+ const base = deps.proxyUrl?.replace(/\/$/, "");
376
+ if (!base || !deps.hostToken)
377
+ return;
378
+ const answer = await proxyGetJson(deps.fetch ?? fetch, `${base}/memtree/host-session?host_pid=${hostPid}`, { [HOST_TOKEN_HEADER]: deps.hostToken }, HOST_SESSION_TIMEOUT_MS);
379
+ const sessionId = answer.ok ? answer.body?.session_id : undefined;
380
+ if (typeof sessionId === "string" && sessionId)
381
+ deps.sessionId = sessionId;
382
+ }
241
383
  function proxyBase(deps) {
242
384
  const base = deps.proxyUrl?.replace(/\/$/, "");
243
385
  if (!base) {
@@ -260,6 +402,8 @@ function checkTreeId(tree) {
260
402
  export class CurrentTree {
261
403
  deps;
262
404
  cached;
405
+ /** The session `cached` was read for: a page is never reused across sessions. */
406
+ cachedSession;
263
407
  /** Temporary prefix pages: when each may be fetched again. */
264
408
  prefixRetryAt = new Map();
265
409
  /** The last fetched temporary prefix per page, served during its cooldown. */
@@ -267,6 +411,8 @@ export class CurrentTree {
267
411
  inFlight;
268
412
  /** Other trees, least recently used first. */
269
413
  others = new Map();
414
+ /** Pages whose per-message times were incomplete: when to refetch them (timesDue). */
415
+ timesRetryAt = new WeakMap();
270
416
  /** Set once the server answered 404 to a tree search the page JSON could answer. */
271
417
  serverSearchMissing = false;
272
418
  fetchImpl;
@@ -307,14 +453,14 @@ export class CurrentTree {
307
453
  if (limit !== undefined && Number.isFinite(limit))
308
454
  params.set("limit", String(limit));
309
455
  const base = proxyBase(this.deps);
310
- const answer = await proxyGetJson(this.fetchImpl, `${base}/memtree/${encodeURIComponent(id)}/search?${params}`);
456
+ const answer = await proxyGetJson(this.fetchImpl, `${base}/memtree/${encodeURIComponent(id)}/search?${params}`, toolHeaders("search", this.deps));
311
457
  const parsed = answer.ok ? serverSearchHits(answer.body) : undefined;
312
458
  if (parsed) {
313
459
  const session = answer.body.session_id;
314
460
  if (!other && session !== undefined)
315
461
  this.checkSession(session, this.requireSessionId());
316
462
  if (other || session !== undefined) {
317
- return formatSearchHits(parsed.hits, query, parsed.terms, other, serverTailHits(answer.body));
463
+ return formatSearchHits(parsed.hits, query, parsed.terms, other, serverTailHits(answer.body), parsed.treeTimes);
318
464
  }
319
465
  // Older servers omit identity: search only the session-validated page.
320
466
  }
@@ -344,7 +490,7 @@ export class CurrentTree {
344
490
  params.set("full", "true");
345
491
  const query = params.toString() ? `?${params}` : "";
346
492
  const base = proxyBase(this.deps);
347
- const answer = await proxyGetJson(this.fetchImpl, `${base}/memtree/${encodeURIComponent(id)}/messages${query}`);
493
+ const answer = await proxyGetJson(this.fetchImpl, `${base}/memtree/${encodeURIComponent(id)}/messages${query}`, toolHeaders("read_lines", this.deps));
348
494
  if (answer.status === 404) {
349
495
  return "No un-indexed messages after this tree: everything the server has recorded is in the tree (or the server predates tails).";
350
496
  }
@@ -357,20 +503,70 @@ export class CurrentTree {
357
503
  this.checkSession(answer.body.session_id, this.requireSessionId());
358
504
  return formatTail(answer.body, other);
359
505
  }
506
+ /**
507
+ * `/memtree/<id>/<route>`, never cached (the newest messages grow). This session's
508
+ * tree must be this session's (as tail); a 404 without the server's own detail means
509
+ * the server or this ccc's proxy predates the route.
510
+ */
511
+ async serverRead(route, tree, params) {
512
+ const other = checkTreeId(tree);
513
+ const id = other ?? (await this.currentId());
514
+ const query = params.toString() ? `?${params}` : "";
515
+ const answer = await proxyGetJson(this.fetchImpl, `${proxyBase(this.deps)}/memtree/${encodeURIComponent(id)}/${route}${query}`, toolHeaders(route, this.deps), route === "handover" ? HANDOVER_TIMEOUT_MS : FETCH_TIMEOUT_MS);
516
+ if (answer.status === 202)
517
+ throw new ToolInputError("That MemTree is still being built; try again in a minute.");
518
+ if (answer.status === 0 && route === "handover" && /timeout|abort/i.test(answer.error ?? "")) {
519
+ throw new ToolInputError("handover: the pack is still being prepared on the server; call handover again in a minute.");
520
+ }
521
+ if (answer.status === 404 && /^HTTP 404 Not Found$/.test(answer.error ?? "")) {
522
+ throw new ToolInputError(`${route}: this MemTree server (or this ccc) does not have it yet; update ccc or try later.`);
523
+ }
524
+ if (!answer.ok)
525
+ throw new ToolInputError(`${route}: ${(answer.error ?? "").replace(/^HTTP (400|404) /, "")}`);
526
+ if (!other)
527
+ this.checkSession(answer.body.session_id, this.requireSessionId());
528
+ return answer.body;
529
+ }
360
530
  async loadCurrent() {
361
531
  const id = await this.currentId();
362
532
  const sessionId = this.requireSessionId();
363
- if (this.cached?.id === id)
533
+ if (this.cached?.id === id && !this.timesDue(this.cached.index))
364
534
  return this.cached.index;
365
- const index = await this.loadPage(id, "this session", sessionId);
535
+ const stale = this.cached?.id === id ? this.cached.index : undefined;
536
+ const index = await this.refreshed(stale, () => this.loadPage(id, "this session", sessionId));
366
537
  if (!isTemporaryPrefix(index, id))
367
538
  this.cached = { id, index };
368
539
  return index;
369
540
  }
541
+ /**
542
+ * Whether a cached page's per-message times were incomplete (`line_times_incomplete`:
543
+ * the server's times lookup was slow) and are due a refetch. The page itself is
544
+ * readable meanwhile, so it stays cached; only its times are retried.
545
+ */
546
+ timesDue(index) {
547
+ const at = this.timesRetryAt.get(index);
548
+ return at !== undefined && this.now() >= at;
549
+ }
550
+ /** `load()`, or the readable `stale` copy when that fails (its times retried later). */
551
+ async refreshed(stale, load) {
552
+ if (!stale)
553
+ return load();
554
+ try {
555
+ return await load();
556
+ }
557
+ catch {
558
+ this.timesRetryAt.set(stale, this.now() + PREFIX_RETRY_INTERVAL_MS);
559
+ return stale;
560
+ }
561
+ }
370
562
  requireSessionId() {
371
563
  const sessionId = this.deps.sessionId;
372
564
  if (!sessionId)
373
565
  throw new ToolInputError("MemTree unavailable: the calling session id is missing");
566
+ if (this.cachedSession !== sessionId) {
567
+ this.cached = undefined; // the process moved to another session (refreshHostSession)
568
+ this.cachedSession = sessionId;
569
+ }
374
570
  return sessionId;
375
571
  }
376
572
  /** The proxy's current page for the calling session; fails closed on any other session. */
@@ -407,12 +603,13 @@ export class CurrentTree {
407
603
  }
408
604
  async getOther(id) {
409
605
  const hit = this.others.get(id);
410
- if (hit) {
606
+ if (hit && !this.timesDue(hit)) {
411
607
  this.others.delete(id);
412
608
  this.others.set(id, hit);
413
609
  return hit;
414
610
  }
415
- const index = await this.loadPage(id, `tree ${id}`);
611
+ const index = await this.refreshed(hit, () => this.loadPage(id, `tree ${id}`));
612
+ this.others.delete(id);
416
613
  if (!isTemporaryPrefix(index, id))
417
614
  this.others.set(id, index);
418
615
  while (this.others.size > OTHER_TREES_CACHED) {
@@ -445,7 +642,7 @@ export class CurrentTree {
445
642
  if (retryAt !== undefined)
446
643
  this.prefixRetryAt.set(id, now + PREFIX_RETRY_INTERVAL_MS);
447
644
  const base = proxyBase(this.deps);
448
- const page = await proxyGetJson(this.fetchImpl, `${base}/memtree/${encodeURIComponent(id)}.json`);
645
+ const page = await proxyGetJson(this.fetchImpl, `${base}/memtree/${encodeURIComponent(id)}.json`, toolHeaders("page", this.deps));
449
646
  if (!page.ok)
450
647
  throw new ToolInputError(`MemTree unavailable (page ${id}: ${page.error})`);
451
648
  const json = page.body;
@@ -457,6 +654,9 @@ export class CurrentTree {
457
654
  throw new ToolInputError("MemTree unavailable: page belongs to a different or unknown session");
458
655
  }
459
656
  const index = new MemtreeIndex(json);
657
+ // Readable now; its incomplete per-message times are refetched after the cooldown.
658
+ if (json.line_times_incomplete === true)
659
+ this.timesRetryAt.set(index, this.now() + PREFIX_RETRY_INTERVAL_MS);
460
660
  if (isTemporaryPrefix(index, id)) {
461
661
  this.prefixRetryAt.set(id, this.now() + PREFIX_RETRY_INTERVAL_MS);
462
662
  this.prefixes.delete(id);
@@ -501,20 +701,83 @@ export class SessionFinder {
501
701
  }
502
702
  async listSessions(args) {
503
703
  const body = await this.get(`/memtree/sessions${sessionsQuery(args)}`, "list");
504
- return formatSessions(body, args);
704
+ return formatSessions(body, args, this.deps.sessionId);
705
+ }
706
+ sessionTrees = new Map();
707
+ /** sessionTree, remembered for SESSION_TREE_TTL_MS (hits and misses: legacy ids repeat). */
708
+ async sessionTree(sessionId) {
709
+ const known = this.sessionTrees.get(sessionId);
710
+ if (known && this.now() - known.at < SESSION_TREE_TTL_MS)
711
+ return known.ref;
712
+ const ref = await this.lookupSessionTree(sessionId);
713
+ this.sessionTrees.set(sessionId, { ref, at: this.now() });
714
+ while (this.sessionTrees.size > SESSION_TREES_CACHED) {
715
+ this.sessionTrees.delete(this.sessionTrees.keys().next().value);
716
+ }
717
+ return ref;
718
+ }
719
+ async lookupSessionTree(sessionId) {
720
+ // `q` also matches titles and first messages, so sessions quoting the id (a hand-off)
721
+ // can fill the first page: follow the cursor, a few pages at most.
722
+ let cursor;
723
+ for (let page = 0; page < SESSION_TREE_MAX_PAGES; page++) {
724
+ const query = sessionsQuery({ q: sessionId, limit: SESSIONS_MAX_LIMIT, cursor });
725
+ const answer = await this.fetch(`/memtree/sessions${query}`, "list");
726
+ if (!answer.ok)
727
+ return undefined;
728
+ const body = answer.body;
729
+ if (!Array.isArray(body?.sessions))
730
+ return undefined;
731
+ const item = body.sessions.find((s) => s?.session_id === sessionId);
732
+ if (item) {
733
+ const ref = item.latest_tree?.ref ?? item.latest_tree?.request_id;
734
+ return typeof ref === "string" && TREE_ID_RE.test(ref) && ref !== sessionId ? ref : undefined;
735
+ }
736
+ if (typeof body.next_cursor !== "string" || !body.next_cursor || body.next_cursor === cursor) {
737
+ return undefined;
738
+ }
739
+ cursor = body.next_cursor;
740
+ }
741
+ return undefined;
505
742
  }
506
743
  async searchSessions(args, tree) {
507
- const body = await this.get(`/memtree/search${searchQuery(args, tree)}`, "search");
744
+ const first = await this.fetch(`/memtree/search${searchQuery(args, tree)}`, "search");
745
+ let answer = first;
746
+ let narrowed;
747
+ const tried = [];
748
+ for (const days of RECENT_SEARCH_WINDOWS_DAYS) {
749
+ const recent = recentSearchArgs(args, tree, answer.status, this.now(), days);
750
+ if (!recent)
751
+ break;
752
+ narrowed = { args: recent, days };
753
+ tried.push(days);
754
+ answer = await this.fetch(`/memtree/search${searchQuery(recent, tree)}`, "search");
755
+ }
756
+ // No window answered: the first refusal says how to narrow the search, plus what ccc
757
+ // tried and, when the last try failed otherwise, how.
758
+ const body = this.bodyOf(answer.ok ? answer : narrowed
759
+ ? { ...first, error: `${first.error}${recentSearchFailedNote(tried, answer)}` } : first, "search");
508
760
  if (tree && (!body || typeof body !== "object" || body.tree !== tree)) {
509
761
  throw new ToolInputError("search: server did not confirm the requested tree; update the MemTree server before scoped search");
510
762
  }
511
- return formatSearchResults(body, { ...args, tree });
763
+ const text = formatSearchResults(body, {
764
+ ...(narrowed?.args ?? args), tree,
765
+ });
766
+ if (!narrowed)
767
+ return text;
768
+ const pages = typeof body?.next_cursor === "string";
769
+ return `${recentSearchNote(narrowed.args.since, narrowed.days, pages)}\n\n${text}`;
770
+ }
771
+ now() {
772
+ return this.deps.now?.() ?? Date.now();
512
773
  }
513
774
  async get(pathAndQuery, tool) {
514
- const headers = {};
515
- if (this.deps.sessionId)
516
- headers["x-claude-code-session-id"] = this.deps.sessionId;
517
- const answer = await proxyGetJson(this.fetchImpl, `${proxyBase(this.deps)}${pathAndQuery}`, headers);
775
+ return this.bodyOf(await this.fetch(pathAndQuery, tool), tool);
776
+ }
777
+ fetch(pathAndQuery, tool) {
778
+ return proxyGetJson(this.fetchImpl, `${proxyBase(this.deps)}${pathAndQuery}`, toolHeaders(tool, this.deps));
779
+ }
780
+ bodyOf(answer, tool) {
518
781
  if (answer.ok)
519
782
  return answer.body;
520
783
  if (answer.status === 404) {
@@ -529,7 +792,9 @@ export class SessionFinder {
529
792
  */
530
793
  export async function handleMcpMessage(input, tree, finder,
531
794
  /** versionMismatchNote: appended to failed tool calls. */
532
- versionNote) {
795
+ versionNote,
796
+ /** A note to append once, to whichever tool answer comes first (StaleToolsNotice). */
797
+ notice) {
533
798
  if (!isRpcRequest(input)) {
534
799
  return { jsonrpc: "2.0", id: null, error: { code: -32600, message: "Invalid Request" } };
535
800
  }
@@ -558,16 +823,20 @@ versionNote) {
558
823
  case "tools/call": {
559
824
  const name = message.params?.name;
560
825
  const args = (message.params?.arguments ?? {});
826
+ const withNotice = (text) => {
827
+ const note = notice?.next();
828
+ return note ? `${text}\n\n${note}` : text;
829
+ };
561
830
  try {
562
831
  const text = await callTool(String(name), args, tree, finder);
563
- return reply({ content: [{ type: "text", text }] });
832
+ return reply({ content: [{ type: "text", text: withNotice(text) }] });
564
833
  }
565
834
  catch (err) {
566
835
  if (err instanceof UnknownToolError)
567
836
  return fail(-32602, err.message);
568
837
  const message = err instanceof Error ? err.message : String(err);
569
838
  const text = versionNote ? `${message}\n${versionNote}` : message;
570
- return reply({ content: [{ type: "text", text }], isError: true });
839
+ return reply({ content: [{ type: "text", text: withNotice(text) }], isError: true });
571
840
  }
572
841
  }
573
842
  default:
@@ -589,6 +858,35 @@ function isRpcRequest(value) {
589
858
  class UnknownToolError extends Error {
590
859
  }
591
860
  async function callTool(name, args, tree, finder) {
861
+ const session = await sessionAsTree(args.tree, finder);
862
+ if (session) {
863
+ const text = await dispatchTool(name, { ...args, tree: session.ref }, tree, finder);
864
+ return `(${session.id} is a session id: this reads its newest tree, ${session.ref}.)\n\n${text}`;
865
+ }
866
+ return dispatchTool(name, args, tree, finder);
867
+ }
868
+ /** A bare UUID: a session id, or a legacy request id (a ref ends in -v<N>-own/served). */
869
+ const BARE_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
870
+ /**
871
+ * A `tree` that is a session id, resolved to that session's newest tree. Hand-offs name
872
+ * session ids (the instructions ask them to), and receiving agents pass them as `tree`:
873
+ * the page lookup then fails with 404, or a tail reads nothing (xsession benchmark,
874
+ * 2026-11-06). Undefined for anything else, including a legacy request id.
875
+ */
876
+ async function sessionAsTree(value, finder) {
877
+ if (typeof value !== "string" || !BARE_UUID_RE.test(value.trim()) || !finder?.sessionTree) {
878
+ return undefined;
879
+ }
880
+ const id = value.trim();
881
+ try {
882
+ const ref = await finder.sessionTree(id);
883
+ return ref ? { id, ref } : undefined;
884
+ }
885
+ catch {
886
+ return undefined;
887
+ }
888
+ }
889
+ async function dispatchTool(name, args, tree, finder) {
592
890
  switch (name) {
593
891
  case "search":
594
892
  return searchTool(args, tree, finder);
@@ -602,6 +900,9 @@ async function callTool(name, args, tree, finder) {
602
900
  if (!finder)
603
901
  throw new ToolInputError("list is not available here");
604
902
  return finder.listSessions(args);
903
+ case "user_messages":
904
+ case "handover":
905
+ return treeRouteTool(name, args, tree);
605
906
  default:
606
907
  throw new UnknownToolError(`Unknown tool: ${name}`);
607
908
  }
@@ -642,6 +943,16 @@ function nodeTarget(args, field) {
642
943
  function treeArgument(tree) {
643
944
  return tree === CURRENT_TREE ? undefined : checkTreeId(tree);
644
945
  }
946
+ /** `user_messages` / `handover`: the server's answer for the tree, formatted. */
947
+ async function treeRouteTool(name, args, tree) {
948
+ if (!tree.serverRead)
949
+ throw new ToolInputError(`${name} is not available here`);
950
+ const target = treeArgument(args.tree);
951
+ if (name === "user_messages") {
952
+ return formatUserMessages(await tree.serverRead(name, target, userMessagesQuery(args)), args, target);
953
+ }
954
+ return formatHandover(await tree.serverRead(name, target, handoverQuery(args)), target);
955
+ }
645
956
  /** read_lines by leaf address (its range unless block/start/end are given), by range, or the tail. */
646
957
  async function readLinesTool(args, tree) {
647
958
  if (args.tail === true) {
@@ -679,10 +990,12 @@ export function runMemtreeMcpServer(env = process.env) {
679
990
  const deps = {
680
991
  proxyUrl: env.CCC_MEMTREE_PROXY || env.ANTHROPIC_BASE_URL,
681
992
  sessionId: env.CLAUDE_CODE_SESSION_ID || undefined,
993
+ hostToken: env[HOST_TOKEN_ENV] || undefined,
682
994
  };
683
995
  const tree = new CurrentTree(deps);
684
996
  const finder = new SessionFinder(deps);
685
997
  const versionNote = versionMismatchNote(env[PROXY_VERSION_ENV]);
998
+ const notice = new StaleToolsNotice();
686
999
  if (versionNote)
687
1000
  process.stderr.write(`memtree MCP: ${versionNote}\n`);
688
1001
  let buffer = "";
@@ -707,7 +1020,10 @@ export function runMemtreeMcpServer(env = process.env) {
707
1020
  continue;
708
1021
  }
709
1022
  queue = queue.then(async () => {
710
- const response = await handleMcpMessage(message, tree, finder, versionNote).catch((err) => ({
1023
+ // Calls run one at a time, so each reads the session its process is on now.
1024
+ if (isToolCall(message))
1025
+ await refreshHostSession(deps);
1026
+ const response = await handleMcpMessage(message, tree, finder, versionNote, notice).catch((err) => ({
711
1027
  jsonrpc: "2.0",
712
1028
  id: isRpcRequest(message) ? message.id ?? null : null,
713
1029
  error: { code: -32603, message: String(err) },
@@ -721,6 +1037,9 @@ export function runMemtreeMcpServer(env = process.env) {
721
1037
  void queue.then(() => process.exit(0));
722
1038
  });
723
1039
  }
1040
+ function isToolCall(message) {
1041
+ return isRpcRequest(message) && message.method === "tools/call";
1042
+ }
724
1043
  function isMainModule() {
725
1044
  try {
726
1045
  const self = fs.realpathSync(fileURLToPath(import.meta.url));