@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 +23 -8
- package/dist/{cli-zcxn6xmn.js → cli-msyx6dnk.js} +120 -12
- package/dist/cli.js +1 -1
- package/dist/proxy/server.d.ts.map +1 -1
- package/dist/proxy/session/lineage.d.ts +22 -5
- package/dist/proxy/session/lineage.d.ts.map +1 -1
- package/dist/proxy/sessionStore.d.ts +24 -0
- package/dist/proxy/sessionStore.d.ts.map +1 -1
- package/dist/server.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -11,10 +11,27 @@
|
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
-
Meridian
|
|
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
|
-
>
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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,
|
|
13854
|
+
function measurePrefixOverlap(storedHashes, incomingHashes) {
|
|
13855
13855
|
let overlap = 0;
|
|
13856
|
-
|
|
13857
|
-
|
|
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,
|
|
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
|
-
|
|
13867
|
-
|
|
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
|
-
|
|
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
|
|
13893
|
-
const
|
|
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
|
-
|
|
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 +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;
|
|
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 (
|
|
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[],
|
|
91
|
+
export declare function measurePrefixOverlap(storedHashes: string[], incomingHashes: string[]): number;
|
|
86
92
|
/**
|
|
87
|
-
* Measure how many
|
|
88
|
-
*
|
|
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[],
|
|
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
|
|
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;
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rynfar/meridian",
|
|
3
|
-
"version": "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
|
},
|