@rynfar/meridian 1.28.1 → 1.29.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 CHANGED
@@ -11,10 +11,27 @@
11
11
 
12
12
  ---
13
13
 
14
- Meridian turns your Claude Max subscription into a local Anthropic API. Any tool that speaks the Anthropic or OpenAI protocol — OpenCode, OpenClaw, Crush, Cline, Aider, Pi, Droid, Open WebUI — connects to Meridian and gets Claude, powered by your existing subscription through the official Claude Code SDK.
14
+ Meridian bridges the Claude Code SDK to the standard Anthropic API. No OAuth interception. No binary patches. No hacks. Just pure, documented SDK calls. Any tool that speaks the Anthropic or OpenAI protocol — OpenCode, OpenClaw, Crush, Cline, Aider, Pi, Droid, Open WebUI — connects to Meridian and gets Claude, with session management, streaming, and prompt caching handled natively by the SDK.
15
15
 
16
16
  > [!IMPORTANT]
17
- > **Extra Usage billing fix (v0.x.x):** Previous versions defaulted Sonnet to `sonnet[1m]` (1M context), which is [always billed as Extra Usage](https://code.claude.com/docs/en/model-config#extended-context) on Max plans — even when regular usage isn't exhausted. Sonnet now defaults to 200k. If you're on an older version, update or set `MERIDIAN_SONNET_MODEL=sonnet` as a workaround. See [#255](https://github.com/rynfar/meridian/issues/255) for details.
17
+ > ### Meridian is unaffected by the April 5, 2025 third-party blocks
18
+ >
19
+ > On April 5, 2025, Anthropic began blocking third-party tools that bypass Claude Code by intercepting OAuth tokens and replaying them against internal API endpoints. Tools that extract `~/.claude/` credentials, proxy raw OAuth bearer tokens, or patch Claude Code binaries to redirect traffic may no longer function.
20
+ >
21
+ > **Meridian does not do any of this.** Its architecture is fundamentally different:
22
+ >
23
+ > - **SDK-native.** Every request calls [`query()`](https://docs.anthropic.com/en/docs/claude-code/sdk) from `@anthropic-ai/claude-agent-sdk` — the same function Anthropic documents for programmatic Claude Code access. No OAuth tokens are extracted, intercepted, or replayed.
24
+ > - **Real Claude Code sessions.** The SDK spawns the actual Claude Code process, manages its own authentication, and handles all communication with Anthropic's servers. Meridian's traffic doesn't *look like* Claude Code — it *is* Claude Code.
25
+ > - **Documented API surface only.** Session resume, MCP tool servers, agent definitions, thinking configuration, permission modes, tool blocking — every feature Meridian uses is a published, documented SDK option. Nothing is reverse-engineered or patched.
26
+ > - **Native benefits and controls preserved.** Prompt caching, conversation persistence, context window management, and compaction all function exactly as they do in Claude Code — because the SDK manages them directly. This means Anthropic's engineering investments in efficiency and their rate-limiting controls work as designed. Max subscription tokens flow through the correct channel, governed by the same guardrails Anthropic built into Claude Code. Meridian doesn't bypass these mechanisms; it depends on them.
27
+ >
28
+ > A small number of adjustments were made in response to the April 5th changes — notably stripping `anthropic-beta` headers that could trigger unintended Extra Usage billing on Max subscriptions ([#281](https://github.com/rynfar/meridian/issues/281)). We are also evaluating system prompt handling to ensure nothing conflicts with Claude Code's expectations. These are compatibility adjustments, not workarounds. Our philosophy is to let Claude Code be the foundation and never fight the SDK — we work with it and add our own layer on top.
29
+ >
30
+ > **Our position is straightforward.** Anthropic asks developers to use Claude Code as the harness for Max subscription access — we do. We call their SDK, respect its authentication flow, use its documented features, and operate within its designed boundaries. We are not circumventing Claude Code; we are building on top of it.
31
+ >
32
+ > What Meridian adds is a **presentation and interoperability layer**. We translate Claude Code's output into the standard Anthropic API format so developers can connect the editors, terminals, and workflows they prefer. The SDK does the work; Meridian formats the result. Developers should have the right to choose their own interface and integrate with their own tooling — that's not circumvention, it's the reason SDKs exist.
33
+ >
34
+ > For Meridian to stop working, Anthropic would need to restrict the Claude Code SDK itself or remove documented features that legitimate SDK consumers depend on. We don't believe that's the intent. We're building within Anthropic's ecosystem and constraints because we genuinely value their tools and models. We simply want the freedom to choose how we experience them — and we hope Anthropic sees that as the kind of ecosystem engagement their SDK was designed to enable.
18
35
 
19
36
  ## Quick Start
20
37
 
@@ -38,13 +55,11 @@ Meridian runs on `http://127.0.0.1:3456`. Point any Anthropic-compatible tool at
38
55
  ANTHROPIC_API_KEY=x ANTHROPIC_BASE_URL=http://127.0.0.1:3456 opencode
39
56
  ```
40
57
 
41
- The API key value doesn't matter — Meridian authenticates through your Claude Max session, not API keys.
58
+ The API key value is a placeholder — Meridian authenticates through the Claude Code SDK, not API keys. Most Anthropic-compatible tools require this field to be set, but any value works.
42
59
 
43
60
  ## Why Meridian?
44
61
 
45
- You're paying for Claude Max. It includes programmatic access through the Claude Code SDK. But your favorite coding tools expect an Anthropic API endpoint and an API key.
46
-
47
- Meridian bridges that gap. It runs locally, accepts standard Anthropic API requests, and routes them through the SDK using your Max subscription.
62
+ The Claude Code SDK provides programmatic access to Claude. But your favorite coding tools expect an Anthropic API endpoint. Meridian bridges that gap — it runs locally, accepts standard API requests, and routes them through the SDK. Claude Code does the heavy lifting; Meridian translates the output.
48
63
 
49
64
  <p align="center">
50
65
  <img src="assets/how-it-works.svg" alt="How Meridian works" width="920"/>
@@ -488,10 +503,10 @@ npm run build # build with bun + tsc
488
503
  ## FAQ
489
504
 
490
505
  **Is this allowed by Anthropic's terms?**
491
- Meridian uses the official Claude Code SDK — the same SDK Anthropic publishes for programmatic access. It authenticates through your existing Claude Max session using OAuth.
506
+ Meridian uses the official Claude Code SDK — the same SDK Anthropic publishes and documents for programmatic access. It does not intercept credentials, modify binaries, or bypass any authentication. All requests flow through the SDK's own authentication and rate-limiting mechanisms.
492
507
 
493
508
  **How is this different from using an API key?**
494
- API keys are billed per token. Claude Max is a flat monthly fee. Meridian lets you use that subscription from any compatible tool.
509
+ API keys provide direct API access billed per token. Claude Max includes programmatic access through the Claude Code SDK. Meridian translates SDK responses into the standard Anthropic API format, allowing compatible tools to connect through Claude Code.
495
510
 
496
511
  **What happens if my OAuth token expires?**
497
512
  Tokens expire roughly every 8 hours. Meridian detects the expiry, refreshes the token automatically, and retries the request — so requests continue transparently. If the refresh fails (e.g. the refresh token has expired after weeks of inactivity), Meridian returns a clear error telling you to run `claude login`.
@@ -13851,26 +13851,59 @@ function computeMessageHashes(messages) {
13851
13851
  return [];
13852
13852
  return messages.map(hashMessage);
13853
13853
  }
13854
- function measurePrefixOverlap(storedHashes, incomingSet) {
13854
+ function measurePrefixOverlap(storedHashes, incomingHashes) {
13855
13855
  let overlap = 0;
13856
- for (const h of storedHashes) {
13857
- if (incomingSet.has(h))
13856
+ const minLen = Math.min(storedHashes.length, incomingHashes.length);
13857
+ for (let i = 0;i < minLen; i++) {
13858
+ if (storedHashes[i] === incomingHashes[i])
13858
13859
  overlap++;
13859
13860
  else
13860
13861
  break;
13861
13862
  }
13862
13863
  return overlap;
13863
13864
  }
13864
- function measureSuffixOverlap(storedHashes, incomingSet) {
13865
+ function measureSuffixOverlap(storedHashes, incomingHashes) {
13866
+ if (storedHashes.length === 0 || incomingHashes.length === 0)
13867
+ return 0;
13868
+ const lastStoredHash = storedHashes[storedHashes.length - 1];
13869
+ let anchorInIncoming = -1;
13870
+ for (let i = incomingHashes.length - 1;i >= 0; i--) {
13871
+ if (incomingHashes[i] === lastStoredHash) {
13872
+ anchorInIncoming = i;
13873
+ break;
13874
+ }
13875
+ }
13876
+ if (anchorInIncoming < 0)
13877
+ return 0;
13865
13878
  let overlap = 0;
13866
- for (let i = storedHashes.length - 1;i >= 0; i--) {
13867
- if (incomingSet.has(storedHashes[i]))
13879
+ let si = storedHashes.length - 1;
13880
+ let ii = anchorInIncoming;
13881
+ while (si >= 0 && ii >= 0) {
13882
+ if (storedHashes[si] === incomingHashes[ii]) {
13868
13883
  overlap++;
13869
- else
13884
+ si--;
13885
+ ii--;
13886
+ } else {
13870
13887
  break;
13888
+ }
13871
13889
  }
13872
13890
  return overlap;
13873
13891
  }
13892
+ function findSuffixAnchorStart(storedHashes, incomingHashes, suffixOverlap) {
13893
+ if (suffixOverlap <= 0)
13894
+ return -1;
13895
+ const lastStoredHash = storedHashes[storedHashes.length - 1];
13896
+ let anchor = -1;
13897
+ for (let i = incomingHashes.length - 1;i >= 0; i--) {
13898
+ if (incomingHashes[i] === lastStoredHash) {
13899
+ anchor = i;
13900
+ break;
13901
+ }
13902
+ }
13903
+ if (anchor < 0)
13904
+ return -1;
13905
+ return anchor - suffixOverlap + 1;
13906
+ }
13874
13907
  function verifyLineage(cached, messages, cacheKey2, cache) {
13875
13908
  if (!cached.lineageHash || cached.messageCount === 0) {
13876
13909
  return { type: "continuation", session: cached };
@@ -13889,11 +13922,11 @@ function verifyLineage(cached, messages, cacheKey2, cache) {
13889
13922
  return { type: "diverged" };
13890
13923
  }
13891
13924
  const incomingHashes = computeMessageHashes(messages);
13892
- const incomingSet = new Set(incomingHashes);
13893
- const prefixOverlap = measurePrefixOverlap(cached.messageHashes, incomingSet);
13894
- const suffixOverlap = measureSuffixOverlap(cached.messageHashes, incomingSet);
13925
+ const prefixOverlap = measurePrefixOverlap(cached.messageHashes, incomingHashes);
13926
+ const suffixOverlap = measureSuffixOverlap(cached.messageHashes, incomingHashes);
13895
13927
  const MIN_STORED_FOR_COMPACTION = 6;
13896
- if (suffixOverlap >= MIN_SUFFIX_FOR_COMPACTION && cached.messageHashes.length >= MIN_STORED_FOR_COMPACTION) {
13928
+ const suffixStartInIncoming = incomingHashes.length - suffixOverlap >= 0 ? findSuffixAnchorStart(cached.messageHashes, incomingHashes, suffixOverlap) : -1;
13929
+ if (suffixOverlap >= MIN_SUFFIX_FOR_COMPACTION && cached.messageHashes.length >= MIN_STORED_FOR_COMPACTION && suffixStartInIncoming > 0) {
13897
13930
  const compactionMsg = `Compaction detected (key=${cacheKey2.slice(0, 8)}…): suffix overlap ${suffixOverlap}/${cached.messageHashes.length}. Allowing resume.`;
13898
13931
  console.error(`[PROXY] ${compactionMsg}`);
13899
13932
  diagnosticLog.lineage(compactionMsg);
@@ -14130,6 +14163,7 @@ function storeSharedSession(key, claudeSessionId, messageCount, lineageHash, mes
14130
14163
  try {
14131
14164
  const store = readStore();
14132
14165
  const existing = store[key];
14166
+ const previousClaudeSessionId = existing && existing.claudeSessionId !== claudeSessionId ? existing.claudeSessionId : existing?.previousClaudeSessionId;
14133
14167
  store[key] = {
14134
14168
  claudeSessionId,
14135
14169
  createdAt: existing?.createdAt || Date.now(),
@@ -14138,7 +14172,8 @@ function storeSharedSession(key, claudeSessionId, messageCount, lineageHash, mes
14138
14172
  lineageHash: lineageHash ?? existing?.lineageHash,
14139
14173
  messageHashes: messageHashes ?? existing?.messageHashes,
14140
14174
  sdkMessageUuids: sdkMessageUuids ?? existing?.sdkMessageUuids,
14141
- contextUsage: contextUsage ?? existing?.contextUsage
14175
+ contextUsage: contextUsage ?? existing?.contextUsage,
14176
+ ...previousClaudeSessionId ? { previousClaudeSessionId } : {}
14142
14177
  };
14143
14178
  const maxEntries = getMaxStoredSessions();
14144
14179
  const keys = Object.keys(store);
@@ -14175,6 +14210,30 @@ function evictSharedSession(key) {
14175
14210
  }
14176
14211
  }
14177
14212
  }
14213
+ function lookupSessionRecovery(key) {
14214
+ const store = readStore();
14215
+ const session = store[key];
14216
+ if (!session)
14217
+ return;
14218
+ return {
14219
+ claudeSessionId: session.claudeSessionId,
14220
+ previousClaudeSessionId: session.previousClaudeSessionId,
14221
+ createdAt: session.createdAt,
14222
+ lastUsedAt: session.lastUsedAt,
14223
+ messageCount: session.messageCount
14224
+ };
14225
+ }
14226
+ function listStoredSessions() {
14227
+ const store = readStore();
14228
+ return Object.entries(store).map(([key, session]) => ({
14229
+ key,
14230
+ claudeSessionId: session.claudeSessionId,
14231
+ previousClaudeSessionId: session.previousClaudeSessionId,
14232
+ createdAt: session.createdAt,
14233
+ lastUsedAt: session.lastUsedAt,
14234
+ messageCount: session.messageCount
14235
+ }));
14236
+ }
14178
14237
  function clearSharedSessions() {
14179
14238
  const path3 = getStorePath();
14180
14239
  try {
@@ -14588,6 +14647,15 @@ function createProxyServer(config = {}) {
14588
14647
  const requestLogLine = `${requestMeta.requestId} adapter=${adapter.name} model=${model} stream=${stream2} tools=${body.tools?.length ?? 0} lineage=${lineageType} session=${resumeSessionId?.slice(0, 8) || "new"}${isUndo && undoRollbackUuid ? ` rollback=${undoRollbackUuid.slice(0, 8)}` : ""}${agentMode ? ` agent=${agentMode}` : ""} active=${activeSessions}/${MAX_CONCURRENT_SESSIONS} msgCount=${msgCount}`;
14589
14648
  console.error(`[PROXY] ${requestLogLine} msgs=${msgSummary}`);
14590
14649
  diagnosticLog.session(`${requestLogLine}`, requestMeta.requestId);
14650
+ if (lineageResult.type === "diverged" && profileSessionId) {
14651
+ const recovery = lookupSessionRecovery(profileSessionId);
14652
+ if (recovery) {
14653
+ const prevId = recovery.previousClaudeSessionId || recovery.claudeSessionId;
14654
+ const recoveryMsg = `${requestMeta.requestId} SESSION RECOVERY: previous conversation available. Run: claude --resume ${prevId}`;
14655
+ console.error(`[PROXY] ${recoveryMsg}`);
14656
+ diagnosticLog.session(recoveryMsg, requestMeta.requestId);
14657
+ }
14658
+ }
14591
14659
  claudeLog("request.received", {
14592
14660
  model,
14593
14661
  stream: stream2,
@@ -15797,6 +15865,46 @@ data: ${JSON.stringify({
15797
15865
  }
15798
15866
  return c.json({ session_id: claudeSessionId, context_usage: session.contextUsage });
15799
15867
  });
15868
+ app.get("/v1/sessions/recover", (c) => {
15869
+ const sessions = listStoredSessions();
15870
+ if (sessions.length === 0) {
15871
+ return c.json({ error: "No sessions found in store" }, 404);
15872
+ }
15873
+ return c.json({
15874
+ sessions: sessions.map((s) => ({
15875
+ key: s.key,
15876
+ claudeSessionId: s.claudeSessionId,
15877
+ previousClaudeSessionId: s.previousClaudeSessionId,
15878
+ createdAt: new Date(s.createdAt).toISOString(),
15879
+ lastUsedAt: new Date(s.lastUsedAt).toISOString(),
15880
+ messageCount: s.messageCount,
15881
+ recoverCommand: `claude --resume ${s.claudeSessionId}`,
15882
+ ...s.previousClaudeSessionId ? {
15883
+ recoverPreviousCommand: `claude --resume ${s.previousClaudeSessionId}`
15884
+ } : {}
15885
+ }))
15886
+ });
15887
+ });
15888
+ app.get("/v1/sessions/:key/recover", (c) => {
15889
+ const key = c.req.param("key");
15890
+ const recovery = lookupSessionRecovery(key);
15891
+ if (!recovery) {
15892
+ return c.json({ error: "Session not found", key }, 404);
15893
+ }
15894
+ return c.json({
15895
+ key,
15896
+ claudeSessionId: recovery.claudeSessionId,
15897
+ previousClaudeSessionId: recovery.previousClaudeSessionId,
15898
+ createdAt: new Date(recovery.createdAt).toISOString(),
15899
+ lastUsedAt: new Date(recovery.lastUsedAt).toISOString(),
15900
+ messageCount: recovery.messageCount,
15901
+ recoverCommand: `claude --resume ${recovery.claudeSessionId}`,
15902
+ ...recovery.previousClaudeSessionId ? {
15903
+ recoverPreviousCommand: `claude --resume ${recovery.previousClaudeSessionId}`,
15904
+ note: "Previous session was replaced — if your current session has lost context, try the previous session ID."
15905
+ } : {}
15906
+ });
15907
+ });
15800
15908
  app.all("*", (c) => {
15801
15909
  console.error(`[PROXY] UNHANDLED ${c.req.method} ${c.req.url}`);
15802
15910
  return c.json({ error: { type: "not_found", message: `Endpoint not supported: ${c.req.method} ${new URL(c.req.url).pathname}` } }, 404);
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  startProxyServer
4
- } from "./cli-zcxn6xmn.js";
4
+ } from "./cli-msyx6dnk.js";
5
5
  import"./cli-g9ypdz51.js";
6
6
  import"./cli-rtab0qa6.js";
7
7
  import"./cli-m9pfb7h9.js";
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/proxy/server.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AACtE,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,CAAA;AAqBvD,OAAO,EACL,kBAAkB,EAClB,WAAW,EACX,oBAAoB,EACpB,KAAK,aAAa,EAEnB,MAAM,mBAAmB,CAAA;AAG1B,OAAO,EAA+B,iBAAiB,EAAE,mBAAmB,EAAsC,MAAM,iBAAiB,CAAA;AAEzI,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,oBAAoB,EAAE,CAAA;AAChE,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,CAAA;AACjD,YAAY,EAAE,aAAa,EAAE,CAAA;AAoG7B,wBAAgB,iBAAiB,CAAC,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM,GAAG,WAAW,CAimDhF;AAED,wBAAsB,gBAAgB,CAAC,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAiEhG"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/proxy/server.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AACtE,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,CAAA;AAqBvD,OAAO,EACL,kBAAkB,EAClB,WAAW,EACX,oBAAoB,EACpB,KAAK,aAAa,EAEnB,MAAM,mBAAmB,CAAA;AAG1B,OAAO,EAA+B,iBAAiB,EAAE,mBAAmB,EAAsC,MAAM,iBAAiB,CAAA;AAGzI,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,oBAAoB,EAAE,CAAA;AAChE,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,CAAA;AACjD,YAAY,EAAE,aAAa,EAAE,CAAA;AAoG7B,wBAAgB,iBAAiB,CAAC,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM,GAAG,WAAW,CA0pDhF;AAED,wBAAsB,gBAAgB,CAAC,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAiEhG"}
@@ -77,20 +77,37 @@ export declare function computeMessageHashes(messages: Array<{
77
77
  }>): string[];
78
78
  /**
79
79
  * Measure how many stored hashes match from the START of the stored array
80
- * against the incoming hashes (order-preserving).
80
+ * against the incoming hashes (positional comparison).
81
81
  *
82
82
  * Prefix overlap means the beginning of the conversation is intact (undo
83
83
  * changes the end but preserves the beginning).
84
+ *
85
+ * NOTE: Compares stored[i] === incoming[i] positionally. An earlier
86
+ * implementation used a Set for O(1) lookups, but that allowed a stored
87
+ * hash at position i to match an incoming hash at a completely different
88
+ * position, inflating the overlap count when duplicate messages exist
89
+ * in the conversation history.
84
90
  */
85
- export declare function measurePrefixOverlap(storedHashes: string[], incomingSet: Set<string>): number;
91
+ export declare function measurePrefixOverlap(storedHashes: string[], incomingHashes: string[]): number;
86
92
  /**
87
- * Measure how many stored hashes match from the END of the stored array
88
- * against the incoming hashes (order-preserving).
93
+ * Measure how many consecutive messages at the END of the stored array
94
+ * appear as a contiguous run in the incoming array.
89
95
  *
90
96
  * Suffix overlap means the recent conversation is intact (compaction
91
97
  * changes the beginning but preserves the end).
98
+ *
99
+ * Algorithm: find the last stored hash in the incoming array, then walk
100
+ * backward through both arrays verifying contiguous matches. This handles
101
+ * the real-world compaction pattern where new messages are appended AFTER
102
+ * the preserved suffix.
103
+ *
104
+ * NOTE: An earlier implementation used a Set for O(1) lookups, but that
105
+ * allowed a stored suffix hash to match an incoming hash at a completely
106
+ * different position — producing false compaction when duplicate messages
107
+ * exist in the conversation. The current approach verifies positional
108
+ * contiguity.
92
109
  */
93
- export declare function measureSuffixOverlap(storedHashes: string[], incomingSet: Set<string>): number;
110
+ export declare function measureSuffixOverlap(storedHashes: string[], incomingHashes: string[]): number;
94
111
  /** Cache-like interface for verifyLineage — only needs get/set/delete */
95
112
  export interface SessionCacheLike {
96
113
  delete(key: string): boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"lineage.d.ts","sourceRoot":"","sources":["../../../src/proxy/session/lineage.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAQH,4EAA4E;AAC5E,MAAM,WAAW,UAAU;IACzB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,uBAAuB,CAAC,EAAE,MAAM,CAAA;IAChC,2BAA2B,CAAC,EAAE,MAAM,CAAA;CACrC;AAED;0EAC0E;AAC1E,eAAO,MAAM,yBAAyB,IAAI,CAAA;AAE1C,MAAM,WAAW,YAAY;IAC3B,eAAe,EAAE,MAAM,CAAA;IACvB,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;IACpB;;qDAEiD;IACjD,WAAW,EAAE,MAAM,CAAA;IACnB;;kCAE8B;IAC9B,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB;;oDAEgD;IAChD,eAAe,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IACtC,iGAAiG;IACjG,YAAY,CAAC,EAAE,UAAU,CAAA;CAC1B;AAED;;;GAGG;AACH,MAAM,MAAM,aAAa,GACrB;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,OAAO,EAAE,YAAY,CAAA;CAAE,GAC/C;IAAE,IAAI,EAAE,YAAY,CAAC;IAAG,OAAO,EAAE,YAAY,CAAA;CAAE,GAC/C;IAAE,IAAI,EAAE,MAAM,CAAC;IAAS,OAAO,EAAE,YAAY,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,GACxG;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,CAAA;AAIxB;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,GAAG,CAAA;CAAE,CAAC,GAAG,MAAM,CAI1F;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,GAAG,CAAA;CAAE,GAAG,MAAM,CAK3E;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,GAAG,CAAA;CAAE,CAAC,GAAG,MAAM,EAAE,CAG9F;AAID;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,EAAE,EAAE,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,MAAM,CAO7F;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,EAAE,EAAE,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,MAAM,CAO7F;AAID,yEAAyE;AACzE,MAAM,WAAW,gBAAgB;IAC/B,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAC7B;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,YAAY,EACpB,QAAQ,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,GAAG,CAAA;CAAE,CAAC,EAC/C,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,gBAAgB,GACtB,aAAa,CAqFf"}
1
+ {"version":3,"file":"lineage.d.ts","sourceRoot":"","sources":["../../../src/proxy/session/lineage.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAQH,4EAA4E;AAC5E,MAAM,WAAW,UAAU;IACzB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,uBAAuB,CAAC,EAAE,MAAM,CAAA;IAChC,2BAA2B,CAAC,EAAE,MAAM,CAAA;CACrC;AAED;0EAC0E;AAC1E,eAAO,MAAM,yBAAyB,IAAI,CAAA;AAE1C,MAAM,WAAW,YAAY;IAC3B,eAAe,EAAE,MAAM,CAAA;IACvB,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;IACpB;;qDAEiD;IACjD,WAAW,EAAE,MAAM,CAAA;IACnB;;kCAE8B;IAC9B,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB;;oDAEgD;IAChD,eAAe,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IACtC,iGAAiG;IACjG,YAAY,CAAC,EAAE,UAAU,CAAA;CAC1B;AAED;;;GAGG;AACH,MAAM,MAAM,aAAa,GACrB;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,OAAO,EAAE,YAAY,CAAA;CAAE,GAC/C;IAAE,IAAI,EAAE,YAAY,CAAC;IAAG,OAAO,EAAE,YAAY,CAAA;CAAE,GAC/C;IAAE,IAAI,EAAE,MAAM,CAAC;IAAS,OAAO,EAAE,YAAY,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,GACxG;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,CAAA;AAIxB;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,GAAG,CAAA;CAAE,CAAC,GAAG,MAAM,CAI1F;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,GAAG,CAAA;CAAE,GAAG,MAAM,CAK3E;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,GAAG,CAAA;CAAE,CAAC,GAAG,MAAM,EAAE,CAG9F;AAID;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,EAAE,EAAE,cAAc,EAAE,MAAM,EAAE,GAAG,MAAM,CAQ7F;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,EAAE,EAAE,cAAc,EAAE,MAAM,EAAE,GAAG,MAAM,CA6B7F;AAyBD,yEAAyE;AACzE,MAAM,WAAW,gBAAgB;IAC/B,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAC7B;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,YAAY,EACpB,QAAQ,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,GAAG,CAAA;CAAE,CAAC,EAC/C,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,gBAAgB,GACtB,aAAa,CA6Ff"}
@@ -23,6 +23,10 @@ export interface StoredSession {
23
23
  sdkMessageUuids?: Array<string | null>;
24
24
  /** Last observed token usage for this Claude session */
25
25
  contextUsage?: TokenUsage;
26
+ /** Previous Claude session ID preserved when the session mapping is replaced.
27
+ * Enables recovery when a lineage bug (e.g. false compaction) causes the
28
+ * original session to be abandoned and a new one started. */
29
+ previousClaudeSessionId?: string;
26
30
  }
27
31
  /** Set an explicit session store directory. Takes priority over env var.
28
32
  * Pass null to clear. For testing only.
@@ -36,5 +40,25 @@ export declare function storeSharedSession(key: string, claudeSessionId: string,
36
40
  /** Remove a single session from the shared file store.
37
41
  * Used when a session is detected as stale (e.g. expired upstream). */
38
42
  export declare function evictSharedSession(key: string): void;
43
+ /** Look up recovery information for a session key.
44
+ * Returns the current and previous Claude session IDs, plus derived
45
+ * file paths and CLI commands for conversation recovery. */
46
+ export declare function lookupSessionRecovery(key: string): {
47
+ claudeSessionId: string;
48
+ previousClaudeSessionId?: string;
49
+ createdAt: number;
50
+ lastUsedAt: number;
51
+ messageCount: number;
52
+ } | undefined;
53
+ /** List all stored session keys and their Claude session IDs.
54
+ * Used by the recovery endpoint to find sessions by partial match. */
55
+ export declare function listStoredSessions(): Array<{
56
+ key: string;
57
+ claudeSessionId: string;
58
+ previousClaudeSessionId?: string;
59
+ createdAt: number;
60
+ lastUsedAt: number;
61
+ messageCount: number;
62
+ }>;
39
63
  export declare function clearSharedSessions(): void;
40
64
  //# sourceMappingURL=sessionStore.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sessionStore.d.ts","sourceRoot":"","sources":["../../src/proxy/sessionStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAeH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAEnD,MAAM,WAAW,aAAa;IAC5B,eAAe,EAAE,MAAM,CAAA;IACvB,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;IACpB,gFAAgF;IAChF,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,6EAA6E;IAC7E,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,iFAAiF;IACjF,eAAe,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IACtC,wDAAwD;IACxD,YAAY,CAAC,EAAE,UAAU,CAAA;CAC1B;AA0DD;;oFAEoF;AACpF,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,EAAE,IAAI,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAG7F;AAsED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAG1E;AAED,wBAAgB,6BAA6B,CAAC,eAAe,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAYhG;AAED,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,MAAM,EACX,eAAe,EAAE,MAAM,EACvB,YAAY,CAAC,EAAE,MAAM,EACrB,WAAW,CAAC,EAAE,MAAM,EACpB,aAAa,CAAC,EAAE,MAAM,EAAE,EACxB,eAAe,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,EACtC,YAAY,CAAC,EAAE,UAAU,GACxB,IAAI,CAsCN;AAED;wEACwE;AACxE,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAkBpD;AAED,wBAAgB,mBAAmB,IAAI,IAAI,CAO1C"}
1
+ {"version":3,"file":"sessionStore.d.ts","sourceRoot":"","sources":["../../src/proxy/sessionStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAeH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAEnD,MAAM,WAAW,aAAa;IAC5B,eAAe,EAAE,MAAM,CAAA;IACvB,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;IACpB,gFAAgF;IAChF,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,6EAA6E;IAC7E,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,iFAAiF;IACjF,eAAe,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IACtC,wDAAwD;IACxD,YAAY,CAAC,EAAE,UAAU,CAAA;IACzB;;kEAE8D;IAC9D,uBAAuB,CAAC,EAAE,MAAM,CAAA;CACjC;AA0DD;;oFAEoF;AACpF,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,EAAE,IAAI,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAG7F;AAsED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAG1E;AAED,wBAAgB,6BAA6B,CAAC,eAAe,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAYhG;AAED,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,MAAM,EACX,eAAe,EAAE,MAAM,EACvB,YAAY,CAAC,EAAE,MAAM,EACrB,WAAW,CAAC,EAAE,MAAM,EACpB,aAAa,CAAC,EAAE,MAAM,EAAE,EACxB,eAAe,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,EACtC,YAAY,CAAC,EAAE,UAAU,GACxB,IAAI,CA8CN;AAED;wEACwE;AACxE,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAkBpD;AAED;;6DAE6D;AAC7D,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG;IAClD,eAAe,EAAE,MAAM,CAAA;IACvB,uBAAuB,CAAC,EAAE,MAAM,CAAA;IAChC,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;CACrB,GAAG,SAAS,CAWZ;AAED;uEACuE;AACvE,wBAAgB,kBAAkB,IAAI,KAAK,CAAC;IAC1C,GAAG,EAAE,MAAM,CAAA;IACX,eAAe,EAAE,MAAM,CAAA;IACvB,uBAAuB,CAAC,EAAE,MAAM,CAAA;IAChC,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;CACrB,CAAC,CAUD;AAED,wBAAgB,mBAAmB,IAAI,IAAI,CAO1C"}
package/dist/server.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  getMaxSessionsLimit,
7
7
  hashMessage,
8
8
  startProxyServer
9
- } from "./cli-zcxn6xmn.js";
9
+ } from "./cli-msyx6dnk.js";
10
10
  import"./cli-g9ypdz51.js";
11
11
  import"./cli-rtab0qa6.js";
12
12
  import"./cli-m9pfb7h9.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rynfar/meridian",
3
- "version": "1.28.1",
3
+ "version": "1.29.1",
4
4
  "description": "Local Anthropic API powered by your Claude Max subscription. One subscription, every agent.",
5
5
  "type": "module",
6
6
  "main": "./dist/server.js",
@@ -24,7 +24,7 @@
24
24
  "build": "rm -rf dist && bun build bin/cli.ts src/proxy/server.ts --outdir dist --target node --splitting --external @anthropic-ai/claude-agent-sdk --entry-naming '[name].js' && tsc -p tsconfig.build.json",
25
25
  "postbuild": "node --check dist/cli.js && node --check dist/server.js && test -f dist/proxy/server.d.ts",
26
26
  "prepublishOnly": "bun run build",
27
- "test": "bun test --path-ignore-patterns '**/*session-store*' --path-ignore-patterns '**/*proxy-async-ops*' --path-ignore-patterns '**/*proxy-extra-usage-fallback*' --path-ignore-patterns '**/*models-auth-status*' --path-ignore-patterns '**/*proxy-context-usage-store*' --path-ignore-patterns '**/*proxy-passthrough-thinking*' --path-ignore-patterns '**/*profile-switch-integration*' && bun test src/__tests__/profile-switch-integration.test.ts && bun test src/__tests__/proxy-extra-usage-fallback.test.ts && bun test src/__tests__/proxy-async-ops.test.ts && bun test src/__tests__/proxy-session-store.test.ts && bun test src/__tests__/session-store-pruning.test.ts && bun test src/__tests__/proxy-session-store-locking.test.ts && bun test src/__tests__/proxy-context-usage-store.test.ts && bun test src/__tests__/models-auth-status.test.ts && bun test src/__tests__/proxy-passthrough-thinking.test.ts",
27
+ "test": "bun test --path-ignore-patterns '**/*session-store*' --path-ignore-patterns '**/*proxy-async-ops*' --path-ignore-patterns '**/*proxy-extra-usage-fallback*' --path-ignore-patterns '**/*models-auth-status*' --path-ignore-patterns '**/*proxy-context-usage-store*' --path-ignore-patterns '**/*proxy-passthrough-thinking*' --path-ignore-patterns '**/*profile-switch-integration*' --path-ignore-patterns '**/*session-recovery*' && bun test src/__tests__/profile-switch-integration.test.ts && bun test src/__tests__/proxy-extra-usage-fallback.test.ts && bun test src/__tests__/proxy-async-ops.test.ts && bun test src/__tests__/proxy-session-store.test.ts && bun test src/__tests__/session-store-pruning.test.ts && bun test src/__tests__/proxy-session-store-locking.test.ts && bun test src/__tests__/proxy-context-usage-store.test.ts && bun test src/__tests__/models-auth-status.test.ts && bun test src/__tests__/proxy-passthrough-thinking.test.ts && bun test src/__tests__/proxy-session-recovery.test.ts",
28
28
  "typecheck": "tsc --noEmit",
29
29
  "proxy:direct": "bun run ./bin/cli.ts"
30
30
  },