@rynfar/meridian 1.61.0 → 1.62.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.
@@ -2020,6 +2020,12 @@ function consolidateMultimodalOntoLastUser(structured) {
2020
2020
  };
2021
2021
  return result;
2022
2022
  }
2023
+ function normalizeTarget(value) {
2024
+ const collapsed = value.replace(/\s+/g, " ").trim();
2025
+ if (!collapsed)
2026
+ return;
2027
+ return collapsed.length > TOOL_TARGET_MAX ? collapsed.slice(0, TOOL_TARGET_MAX - 3) + "..." : collapsed;
2028
+ }
2023
2029
  function summarizeContent(value) {
2024
2030
  if (typeof value !== "string" || value.length === 0)
2025
2031
  return;
@@ -2028,13 +2034,18 @@ function summarizeContent(value) {
2028
2034
  return;
2029
2035
  return collapsed.length > CONTENT_SUMMARY_MAX ? collapsed.slice(0, CONTENT_SUMMARY_MAX) + "..." : collapsed;
2030
2036
  }
2037
+ function editReplacementText(rec) {
2038
+ if (!rec)
2039
+ return;
2040
+ return rec.new_string ?? rec.newString ?? rec.new_text ?? rec.newText;
2041
+ }
2031
2042
  function extractContentSummary(name, input) {
2032
2043
  if (!input || typeof input !== "object")
2033
2044
  return;
2034
2045
  const rec = input;
2035
2046
  switch (name.toLowerCase()) {
2036
2047
  case "edit":
2037
- return summarizeContent(rec.newString ?? rec.new_string);
2048
+ return summarizeContent(editReplacementText(rec));
2038
2049
  case "write":
2039
2050
  return summarizeContent(rec.content);
2040
2051
  case "multiedit": {
@@ -2042,7 +2053,7 @@ function extractContentSummary(name, input) {
2042
2053
  if (!Array.isArray(edits) || edits.length === 0)
2043
2054
  return;
2044
2055
  const first = edits[0];
2045
- const firstSummary = summarizeContent(first?.newString ?? first?.new_string);
2056
+ const firstSummary = summarizeContent(editReplacementText(first));
2046
2057
  if (!firstSummary)
2047
2058
  return;
2048
2059
  return edits.length > 1 ? `${edits.length} edits; first: ${firstSummary}` : firstSummary;
@@ -2065,8 +2076,9 @@ function buildToolUseIndex(messages) {
2065
2076
  for (const key of TOOL_TARGET_KEYS) {
2066
2077
  const v = input[key];
2067
2078
  if (typeof v === "string" && v) {
2068
- target = v.length > 80 ? v.slice(0, 77) + "..." : v;
2069
- break;
2079
+ target = normalizeTarget(v);
2080
+ if (target)
2081
+ break;
2070
2082
  }
2071
2083
  }
2072
2084
  }
@@ -2091,7 +2103,7 @@ function extractSystemText(system) {
2091
2103
  return system.filter((b) => b?.type === "text" && typeof b.text === "string" && b.text).map((b) => b.text).filter((text) => !TRANSPORT_HEADER_BLOCK.test(text)).join(`
2092
2104
  `);
2093
2105
  }
2094
- var HASH_IGNORED_BLOCK_TYPES, HASH_HANDLED_BLOCK_TYPES, HASH_SERIALIZED_BLOCK_TYPES, PASSTHROUGH_CONTINUATION_LEAD_IN, MULTIMODAL_TYPES, TOOL_TARGET_KEYS, CONTENT_SUMMARY_MAX = 120, TRANSPORT_HEADER_BLOCK;
2106
+ var HASH_IGNORED_BLOCK_TYPES, HASH_HANDLED_BLOCK_TYPES, HASH_SERIALIZED_BLOCK_TYPES, PASSTHROUGH_CONTINUATION_LEAD_IN, MULTIMODAL_TYPES, TOOL_TARGET_KEYS, TOOL_TARGET_MAX = 80, CONTENT_SUMMARY_MAX = 120, TRANSPORT_HEADER_BLOCK;
2095
2107
  var init_messages = __esm(() => {
2096
2108
  HASH_IGNORED_BLOCK_TYPES = new Set(["thinking", "redacted_thinking"]);
2097
2109
  HASH_HANDLED_BLOCK_TYPES = new Set(["text", "tool_use", "tool_result"]);
@@ -2110,7 +2122,7 @@ var init_messages = __esm(() => {
2110
2122
  ]);
2111
2123
  PASSTHROUGH_CONTINUATION_LEAD_IN = "The tool calls from your previous turn were forwarded to the client, which has now executed them — " + "their results follow. The instruction to end that turn without further text applied to it alone and " + "is now discharged: continue the work and respond.";
2112
2124
  MULTIMODAL_TYPES = new Set(["image", "document", "file"]);
2113
- TOOL_TARGET_KEYS = ["filePath", "file_path", "path", "command", "pattern", "query", "url"];
2125
+ TOOL_TARGET_KEYS = ["filePath", "file_path", "path", "command", "code", "pattern", "query", "url"];
2114
2126
  TRANSPORT_HEADER_BLOCK = /^\s*x-anthropic-[a-z0-9-]*header\s*:/i;
2115
2127
  });
2116
2128
 
@@ -2942,6 +2954,124 @@ var init_pi2 = __esm(() => {
2942
2954
  };
2943
2955
  });
2944
2956
 
2957
+ // src/proxy/adapters/prime.ts
2958
+ function extractPrimeCwd(body) {
2959
+ let systemText = "";
2960
+ if (typeof body?.system === "string") {
2961
+ systemText = body.system;
2962
+ } else if (Array.isArray(body?.system)) {
2963
+ systemText = body.system.filter((b) => b?.type === "text" && b.text).map((b) => b.text).join(`
2964
+ `);
2965
+ }
2966
+ if (!systemText)
2967
+ return;
2968
+ const match2 = systemText.match(RLM_CWD_LINE) ?? systemText.match(CUSTOM_PROMPT_CWD_LINE);
2969
+ return match2?.[1]?.trim() || undefined;
2970
+ }
2971
+ function extractFileChangesFromIpythonCell(code) {
2972
+ const lines = code.split(`
2973
+ `);
2974
+ const firstMeaningful = lines.find((l) => l.trim().length > 0) ?? "";
2975
+ if (/^[ \t]*%%bash\b/.test(firstMeaningful)) {
2976
+ const bodyStart = lines.indexOf(firstMeaningful) + 1;
2977
+ return extractFileChangesFromBash(lines.slice(bodyStart).join(`
2978
+ `));
2979
+ }
2980
+ const changes = [];
2981
+ for (const line of lines) {
2982
+ const escaped = line.match(SHELL_ESCAPE_LINE);
2983
+ if (escaped?.[1])
2984
+ changes.push(...extractFileChangesFromBash(escaped[1]));
2985
+ }
2986
+ for (const m of code.matchAll(EDIT_SKILL_CALL)) {
2987
+ if (m[2])
2988
+ changes.push({ operation: "edited", path: m[2] });
2989
+ }
2990
+ return changes;
2991
+ }
2992
+ function extractPrimeFileChanges(toolName, toolInput) {
2993
+ const input = toolInput;
2994
+ if (toolName === "ipython" && typeof input?.code === "string") {
2995
+ return extractFileChangesFromIpythonCell(input.code);
2996
+ }
2997
+ const filePath = input?.path ?? input?.file_path ?? input?.filePath;
2998
+ if (toolName === "edit" && filePath) {
2999
+ return [{ operation: "edited", path: String(filePath) }];
3000
+ }
3001
+ if (toolName === "write" && filePath) {
3002
+ return [{ operation: "wrote", path: String(filePath) }];
3003
+ }
3004
+ if (toolName === "bash" && input?.command) {
3005
+ return extractFileChangesFromBash(String(input.command));
3006
+ }
3007
+ return [];
3008
+ }
3009
+ var PRIME_MCP_SERVER_NAME = "prime", PRIME_ALLOWED_MCP_TOOLS, RLM_CWD_LINE, CUSTOM_PROMPT_CWD_LINE, EDIT_SKILL_CALL, SHELL_ESCAPE_LINE, primeAdapter;
3010
+ var init_prime = __esm(() => {
3011
+ init_fileChanges();
3012
+ init_messages();
3013
+ init_tools();
3014
+ init_env();
3015
+ init_claudecode();
3016
+ PRIME_ALLOWED_MCP_TOOLS = [
3017
+ `mcp__${PRIME_MCP_SERVER_NAME}__read`,
3018
+ `mcp__${PRIME_MCP_SERVER_NAME}__write`,
3019
+ `mcp__${PRIME_MCP_SERVER_NAME}__edit`,
3020
+ `mcp__${PRIME_MCP_SERVER_NAME}__bash`,
3021
+ `mcp__${PRIME_MCP_SERVER_NAME}__glob`,
3022
+ `mcp__${PRIME_MCP_SERVER_NAME}__grep`
3023
+ ];
3024
+ RLM_CWD_LINE = /^Working directory:[ \t]*(.+)$/m;
3025
+ CUSTOM_PROMPT_CWD_LINE = /^Current working directory:[ \t]*(.+)$/m;
3026
+ EDIT_SKILL_CALL = /\bedit\s*\(\s*path\s*=\s*(['"])(.+?)\1/g;
3027
+ SHELL_ESCAPE_LINE = /^[ \t]*!(.+)$/;
3028
+ primeAdapter = {
3029
+ name: "prime",
3030
+ getSessionId(c, body) {
3031
+ return c.req.header("x-session-affinity") ?? extractClaudeCodeSessionId(body);
3032
+ },
3033
+ extractWorkingDirectory(body) {
3034
+ return extractPrimeCwd(body);
3035
+ },
3036
+ extractClientWorkingDirectory(body) {
3037
+ return extractPrimeCwd(body);
3038
+ },
3039
+ normalizeContent(content) {
3040
+ return normalizeContent(content);
3041
+ },
3042
+ getBlockedBuiltinTools() {
3043
+ return BLOCKED_BUILTIN_TOOLS;
3044
+ },
3045
+ getAgentIncompatibleTools() {
3046
+ return CLAUDE_CODE_ONLY_TOOLS;
3047
+ },
3048
+ getMcpServerName() {
3049
+ return PRIME_MCP_SERVER_NAME;
3050
+ },
3051
+ getAllowedMcpTools() {
3052
+ return PRIME_ALLOWED_MCP_TOOLS;
3053
+ },
3054
+ buildSdkAgents(_body, _mcpToolNames) {
3055
+ return {};
3056
+ },
3057
+ supportsThinking() {
3058
+ return true;
3059
+ },
3060
+ buildSdkHooks(_body, _sdkAgents) {
3061
+ return;
3062
+ },
3063
+ buildSystemContextAddendum(_body, _sdkAgents) {
3064
+ return "";
3065
+ },
3066
+ usesPassthrough() {
3067
+ return resolvePassthrough(true);
3068
+ },
3069
+ extractFileChangesFromToolUse(toolName, toolInput) {
3070
+ return extractPrimeFileChanges(toolName, toolInput);
3071
+ }
3072
+ };
3073
+ });
3074
+
2945
3075
  // src/proxy/transforms/forgecode.ts
2946
3076
  var FORGECODE_MCP_SERVER_NAME = "forgecode", FORGECODE_ALLOWED_MCP_TOOLS, forgeCodeTransforms;
2947
3077
  var init_forgecode = __esm(() => {
@@ -3279,6 +3409,7 @@ var init_detect = __esm(() => {
3279
3409
  init_crush2();
3280
3410
  init_passthrough2();
3281
3411
  init_pi2();
3412
+ init_prime();
3282
3413
  init_forgecode2();
3283
3414
  init_claudecode();
3284
3415
  init_openai();
@@ -3292,6 +3423,8 @@ var init_detect = __esm(() => {
3292
3423
  crush: crushAdapter,
3293
3424
  passthrough: passthroughAdapter,
3294
3425
  pi: piAdapter,
3426
+ prime: primeAdapter,
3427
+ "prime-agent": primeAdapter,
3295
3428
  forgecode: forgeCodeAdapter,
3296
3429
  "claude-code": claudeCodeAdapter,
3297
3430
  claudecode: claudeCodeAdapter,
@@ -3455,6 +3588,9 @@ var init_sdkFeatures = __esm(() => {
3455
3588
  cherry: {
3456
3589
  codeSystemPrompt: false
3457
3590
  },
3591
+ prime: {
3592
+ codeSystemPrompt: false
3593
+ },
3458
3594
  codex: {
3459
3595
  codeSystemPrompt: false
3460
3596
  }
@@ -6201,6 +6337,7 @@ var serve = (options, listeningListener) => {
6201
6337
  };
6202
6338
 
6203
6339
  // src/proxy/server.ts
6340
+ import { AsyncLocalStorage } from "node:async_hooks";
6204
6341
  import { homedir as homedir7 } from "node:os";
6205
6342
  import { join as join8 } from "node:path";
6206
6343
  import { query } from "@anthropic-ai/claude-agent-sdk";
@@ -11768,7 +11905,14 @@ function profileSection(q,s,pl,h){
11768
11905
  var orderIdx=(pl.profileOrder||[]).indexOf(p.id);
11769
11906
  if(orderIdx>=0)badge+='<span class="pool-chip">#'+(orderIdx+1)+' in pool</span>';
11770
11907
  var exh=(pl.exhausted||[]).filter(function(e){return e.id===p.id})[0];
11771
- if(exh)badge+=' <span class="pool-chip exhausted">exhausted · resets '+resetIn(exh.until)+'</span>';
11908
+ if(exh){
11909
+ // A billing refusal has no reset to wait for — the pool re-probes on the
11910
+ // same timer, but nothing changes until a human fixes the account.
11911
+ // Showing it as 'resets in 9m' promises a recovery that never comes.
11912
+ badge+=exh.reason==='billing_error'
11913
+ ?' <span class="pool-chip exhausted" title="Subscription or payment refused — this does not clear on its own">subscription refused</span>'
11914
+ :' <span class="pool-chip exhausted">exhausted · resets '+resetIn(exh.until)+'</span>';
11915
+ }
11772
11916
  }
11773
11917
  cards+='<div class="profile-card'+(p.isActive?' active':'')+(switchable?' switchable':'')+'"'+(switchable?' data-profile="'+esc(p.id)+'" role="button" tabindex="0"':'')+'>'
11774
11918
  +'<div class="profile-head"><span class="profile-name"><span class="prof-dot"></span>'+esc(p.label||p.id)+' '+badge+'</span>'
@@ -11954,6 +12098,16 @@ function extendedContextHint(model) {
11954
12098
  return advise("MERIDIAN_SONNET_MODEL=sonnet");
11955
12099
  return advise("MERIDIAN_1M_CONTEXT_SUPPORT=0");
11956
12100
  }
12101
+ var BILLING_SIGNALS = [
12102
+ /\b402\b(?!:\d)/,
12103
+ /billing[_ ](?:error|issue|problem|failure)/,
12104
+ /subscription (?:is |has )?(?:inactive|expired|lapsed|cancell?ed|ended|invalid|not active)/,
12105
+ /(?:expired|inactive|lapsed|invalid|no active|cancell?ed) subscription/,
12106
+ /payment (?:method|required|failed|declined|details|info)/,
12107
+ /update your payment/,
12108
+ /(?:out of|draw from|draws from) extra usage/,
12109
+ /insufficient (?:credit|funds|balance)/
12110
+ ];
11957
12111
  var HIT_YOUR_LIMIT = /hit your (?:[\w-]+ )?limit/;
11958
12112
  function classifyError(errMsg, model) {
11959
12113
  const lower = errMsg.toLowerCase();
@@ -11979,7 +12133,7 @@ function classifyError(errMsg, model) {
11979
12133
  message: `Claude Max rate limit reached. Wait a moment and try again.${hint}`
11980
12134
  };
11981
12135
  }
11982
- if (lower.includes("402") || lower.includes("billing") || lower.includes("subscription") || lower.includes("payment")) {
12136
+ if (BILLING_SIGNALS.some((rx) => rx.test(lower))) {
11983
12137
  return {
11984
12138
  status: 402,
11985
12139
  type: "billing_error",
@@ -12049,11 +12203,18 @@ function isExpiredTokenError(errMsg) {
12049
12203
  return true;
12050
12204
  return false;
12051
12205
  }
12052
- function isStaleSessionError(error) {
12206
+ function classifyResumeRefusal(error, stderr) {
12207
+ if (isBusySessionError(error, stderr))
12208
+ return "busy";
12053
12209
  if (!(error instanceof Error))
12054
- return false;
12210
+ return;
12055
12211
  const msg = error.message;
12056
- return msg.includes("No message found with message.uuid") || msg.includes("No conversation found with session ID") || msg.includes("No conversation found to continue") || msg.includes("No conversations found to resume");
12212
+ if (msg.includes("No message found with message.uuid"))
12213
+ return "missing-message";
12214
+ if (msg.includes("No conversation found with session ID") || msg.includes("No conversation found to continue") || msg.includes("No conversations found to resume")) {
12215
+ return "unresumable";
12216
+ }
12217
+ return;
12057
12218
  }
12058
12219
  function isBusySessionError(error, stderr) {
12059
12220
  const needle = "is currently running as a background agent";
@@ -12064,6 +12225,16 @@ function isRateLimitError(errMsg) {
12064
12225
  const lower = errMsg.toLowerCase();
12065
12226
  return lower.includes("429") || lower.includes("rate limit") || lower.includes("too many requests");
12066
12227
  }
12228
+ var ACCOUNT_FAILOVER_ERROR_TYPES = new Set([
12229
+ "rate_limit_error",
12230
+ "billing_error"
12231
+ ]);
12232
+ function isAccountFailoverError(errorType) {
12233
+ return typeof errorType === "string" && ACCOUNT_FAILOVER_ERROR_TYPES.has(errorType);
12234
+ }
12235
+ function isQuotaRefusal(errorType) {
12236
+ return errorType === "rate_limit_error";
12237
+ }
12067
12238
  function isExtraUsageRequiredError(errMsg) {
12068
12239
  const lower = errMsg.toLowerCase();
12069
12240
  return lower.includes("extra usage") && lower.includes("1m") || lower.includes("out of extra usage");
@@ -12088,11 +12259,34 @@ function makeRawTail(errMsg) {
12088
12259
  return;
12089
12260
  return head.length > RAW_TAIL_MAX ? head.slice(0, RAW_TAIL_MAX) : head;
12090
12261
  }
12262
+ function canRecoverCapturedToolUses(input) {
12263
+ if (!input.passthrough)
12264
+ return false;
12265
+ if (input.capturedToolUses <= 0)
12266
+ return false;
12267
+ switch (input.reason) {
12268
+ case "max_turns":
12269
+ case "upstream_idle":
12270
+ return true;
12271
+ case "aborted":
12272
+ return input.abortIsOurs;
12273
+ default:
12274
+ return false;
12275
+ }
12276
+ }
12091
12277
  function extractSdkTermination(errMsg) {
12092
12278
  const stderrTail = extractStderrTail(errMsg);
12093
12279
  const haystack = `${errMsg}
12094
12280
  ${stderrTail ?? ""}`;
12095
12281
  const lower = haystack.toLowerCase();
12282
+ if (lower.includes("upstream idle for")) {
12283
+ const m = haystack.match(/upstream idle for (\d+)ms/i);
12284
+ return {
12285
+ reason: "upstream_idle",
12286
+ ...m ? { idleMs: Number(m[1]) } : {},
12287
+ ...stderrTail ? { stderrTail } : {}
12288
+ };
12289
+ }
12096
12290
  if (lower.includes("reached maximum number of turns")) {
12097
12291
  const m = haystack.match(/Reached maximum number of turns \((\d+)\)/i);
12098
12292
  return {
@@ -19168,6 +19362,34 @@ init_opencode();
19168
19362
  init_crush();
19169
19363
  init_droid();
19170
19364
  init_pi();
19365
+
19366
+ // src/proxy/transforms/prime.ts
19367
+ init_tools();
19368
+ init_env();
19369
+ init_prime();
19370
+ function resolvePrimePassthrough() {
19371
+ return resolvePassthrough(true);
19372
+ }
19373
+ var primeTransforms = [
19374
+ {
19375
+ name: "prime-core",
19376
+ adapters: ["prime"],
19377
+ onRequest(ctx) {
19378
+ return {
19379
+ ...ctx,
19380
+ blockedTools: BLOCKED_BUILTIN_TOOLS,
19381
+ incompatibleTools: CLAUDE_CODE_ONLY_TOOLS,
19382
+ allowedMcpTools: PRIME_ALLOWED_MCP_TOOLS,
19383
+ sdkAgents: {},
19384
+ passthrough: resolvePrimePassthrough(),
19385
+ supportsThinking: true,
19386
+ extractFileChangesFromToolUse: extractPrimeFileChanges
19387
+ };
19388
+ }
19389
+ }
19390
+ ];
19391
+
19392
+ // src/proxy/transforms/registry.ts
19171
19393
  init_forgecode();
19172
19394
  init_passthrough();
19173
19395
 
@@ -19249,6 +19471,7 @@ var ADAPTER_TRANSFORMS = {
19249
19471
  crush: crushTransforms,
19250
19472
  droid: droidTransforms,
19251
19473
  pi: piTransforms,
19474
+ prime: primeTransforms,
19252
19475
  forgecode: forgeCodeTransforms,
19253
19476
  passthrough: passthroughTransforms,
19254
19477
  cherry: cherryTransforms,
@@ -19267,7 +19490,7 @@ import { join as join5, isAbsolute as isAbsolute2, extname } from "path";
19267
19490
  import { pathToFileURL } from "url";
19268
19491
 
19269
19492
  // src/proxy/plugins/validation.ts
19270
- var KNOWN_ADAPTERS = ["opencode", "openai", "jcode", "crush", "droid", "pi", "forgecode", "passthrough"];
19493
+ init_detect();
19271
19494
  var KNOWN_HOOKS = ["onRequest", "onResponse", "onTelemetry", "onSession", "onToolUse", "onToolResult", "onError"];
19272
19495
  function validateTransform(exported) {
19273
19496
  if (exported == null || typeof exported !== "object") {
@@ -19288,8 +19511,9 @@ function validateTransform(exported) {
19288
19511
  }
19289
19512
  const warnings = [];
19290
19513
  if (Array.isArray(obj.adapters)) {
19514
+ const known = listAdapterNames();
19291
19515
  for (const adapter of obj.adapters) {
19292
- if (typeof adapter === "string" && !KNOWN_ADAPTERS.includes(adapter)) {
19516
+ if (typeof adapter === "string" && !known.includes(adapter)) {
19293
19517
  warnings.push(adapter);
19294
19518
  }
19295
19519
  }
@@ -19622,6 +19846,48 @@ function computeLineageHash(messages) {
19622
19846
  function hashMessage(message) {
19623
19847
  return createHash2("sha256").update(`${message.role}:${normalizeContent(message.content)}`).digest("hex").slice(0, 32);
19624
19848
  }
19849
+ function describeShape(message) {
19850
+ const normalized = normalizeContent(message.content);
19851
+ return {
19852
+ role: message.role,
19853
+ blocks: Array.isArray(message.content) ? message.content.map((b) => String(b?.type ?? "unknown")).join(",") : typeof message.content === "string" ? "string" : "unknown",
19854
+ bytes: Buffer.byteLength(normalized, "utf8")
19855
+ };
19856
+ }
19857
+ function describeLineageMismatch(cached, messages, precomputedIncomingHashes) {
19858
+ const storedHashes = cached.messageHashes ?? [];
19859
+ const incomingHashes = precomputedIncomingHashes ?? computeMessageHashes(messages);
19860
+ const limit = Math.min(storedHashes.length, incomingHashes.length);
19861
+ let index = -1;
19862
+ for (let i = 0;i < limit; i++) {
19863
+ if (storedHashes[i] !== incomingHashes[i]) {
19864
+ index = i;
19865
+ break;
19866
+ }
19867
+ }
19868
+ const base = {
19869
+ index,
19870
+ storedCount: cached.messageCount,
19871
+ incomingCount: messages.length
19872
+ };
19873
+ if (index < 0)
19874
+ return base;
19875
+ return {
19876
+ ...base,
19877
+ storedDigest: storedHashes[index],
19878
+ incomingDigest: incomingHashes[index],
19879
+ incomingShape: messages[index] ? describeShape(messages[index]) : undefined,
19880
+ previousDigest: index > 0 ? storedHashes[index - 1] : undefined
19881
+ };
19882
+ }
19883
+ function formatLineageMismatch(mismatch) {
19884
+ if (mismatch.index < 0)
19885
+ return;
19886
+ const short = (digest) => digest ? digest.slice(0, 12) : "—";
19887
+ const trailing = mismatch.index === mismatch.storedCount - 1 ? " (trailing message only — the rest of the history matched)" : "";
19888
+ const shape = mismatch.incomingShape ? `${mismatch.incomingShape.role}[${mismatch.incomingShape.blocks}] ${mismatch.incomingShape.bytes}B` : "unknown";
19889
+ return `first mismatch at index ${mismatch.index}${trailing}: ` + `stored=${short(mismatch.storedDigest)} incoming=${short(mismatch.incomingDigest)}, ` + `incoming now ${shape}`;
19890
+ }
19625
19891
  function computeMessageHashes(messages) {
19626
19892
  if (!messages || messages.length === 0)
19627
19893
  return [];
@@ -19763,7 +20029,12 @@ function verifyLineage(cached, messages) {
19763
20029
  return { type: "undo", session: cached, prefixOverlap, rollbackUuid };
19764
20030
  }
19765
20031
  if (prefixOverlap > 0 && messages.length > cached.messageCount) {
19766
- return { type: "diverged", reason: "modified-history", prefixOverlap };
20032
+ return {
20033
+ type: "diverged",
20034
+ reason: "modified-history",
20035
+ prefixOverlap,
20036
+ mismatch: describeLineageMismatch(cached, messages, incomingHashes)
20037
+ };
19767
20038
  }
19768
20039
  return { type: "diverged", reason: "unrelated-history", prefixOverlap };
19769
20040
  }
@@ -20087,7 +20358,9 @@ function classifyLineage(state, messages, cacheKey2) {
20087
20358
  console.error(`[PROXY] ${msg}`);
20088
20359
  diagnosticLog2.lineage(msg);
20089
20360
  } else if (result.type === "diverged" && result.reason === "modified-history") {
20090
- const msg = `Stale session detected (key=${cacheKey2.slice(0, 8)}…): prefix overlap ${result.prefixOverlap || 0}/${state.messageCount}, incoming ${messages.length} msgs. Starting fresh replay.`;
20361
+ const detail = result.mismatch ? formatLineageMismatch(result.mismatch) : undefined;
20362
+ const msg = `Stale session detected (key=${cacheKey2.slice(0, 8)}…): prefix overlap ${result.prefixOverlap || 0}/${state.messageCount}, incoming ${messages.length} msgs. Starting fresh replay.` + (detail ? `
20363
+ ${detail}` : "");
20091
20364
  console.error(`[PROXY] ${msg}`);
20092
20365
  diagnosticLog2.lineage(msg);
20093
20366
  }
@@ -20422,8 +20695,8 @@ function createProxyServer(config = {}) {
20422
20695
  const sessionMcpCache = new LRUMap(getMaxSessionsLimit());
20423
20696
  const PENDING_STORE_WAIT_MS = 3000;
20424
20697
  const PENDING_STORE_AUTO_RESOLVE_MS = 1e4;
20425
- const BUSY_SESSION_MAX_RETRIES = 3;
20426
- const BUSY_SESSION_RETRY_DELAY_MS = parseInt(process.env.MERIDIAN_BUSY_RETRY_DELAY_MS ?? "500", 10);
20698
+ const RESUME_REFUSAL_MAX_RETRIES = 3;
20699
+ const RESUME_REFUSAL_RETRY_DELAY_MS = parseInt(process.env.MERIDIAN_BUSY_RETRY_DELAY_MS ?? "500", 10);
20427
20700
  const pendingSessionStores = new Map;
20428
20701
  const registerPendingStore = (key) => {
20429
20702
  let resolveFn = () => {};
@@ -20501,23 +20774,25 @@ function createProxyServer(config = {}) {
20501
20774
  });
20502
20775
  });
20503
20776
  }
20504
- async function sniffQuotaFailure(res) {
20777
+ async function sniffAccountFailure(res) {
20505
20778
  const contentType = res.headers.get("content-type") ?? "";
20506
20779
  if (!contentType.includes("text/event-stream")) {
20507
- if (res.status === 429) {
20780
+ if (!res.ok) {
20508
20781
  const body = await res.clone().json().catch(() => null);
20509
- if (body?.error?.type === "rate_limit_error")
20510
- return { failed: true, errorPayload: body, response: res };
20782
+ const errorType = body?.error?.type;
20783
+ if (isAccountFailoverError(errorType)) {
20784
+ return { failed: true, errorPayload: body, errorType, response: res };
20785
+ }
20511
20786
  }
20512
- return { failed: false, errorPayload: null, response: res };
20787
+ return { failed: false, errorPayload: null, errorType: null, response: res };
20513
20788
  }
20514
20789
  const reader = res.body?.getReader();
20515
20790
  if (!reader)
20516
- return { failed: false, errorPayload: null, response: res };
20791
+ return { failed: false, errorPayload: null, errorType: null, response: res };
20517
20792
  const decoder = new TextDecoder;
20518
20793
  const consumed = [];
20519
20794
  let text = "";
20520
- let failedPayload = null;
20795
+ let failure = null;
20521
20796
  while (true) {
20522
20797
  const { done, value } = await reader.read();
20523
20798
  if (done)
@@ -20535,16 +20810,17 @@ function createProxyServer(config = {}) {
20535
20810
  `).find((l) => l.startsWith("data: "));
20536
20811
  try {
20537
20812
  const parsed = dataLine ? JSON.parse(dataLine.slice(6)) : null;
20538
- if (parsed?.error?.type === "rate_limit_error") {
20539
- failedPayload = parsed;
20813
+ const parsedType = parsed?.error?.type;
20814
+ if (isAccountFailoverError(parsedType)) {
20815
+ failure = { payload: parsed, type: parsedType };
20540
20816
  }
20541
20817
  } catch {}
20542
20818
  }
20543
20819
  break;
20544
20820
  }
20545
- if (failedPayload) {
20821
+ if (failure) {
20546
20822
  await reader.cancel().catch(() => {});
20547
- return { failed: true, errorPayload: failedPayload, response: res };
20823
+ return { failed: true, errorPayload: failure.payload, errorType: failure.type, response: res };
20548
20824
  }
20549
20825
  const rest = new ReadableStream({
20550
20826
  start(ctrl) {
@@ -20562,33 +20838,40 @@ function createProxyServer(config = {}) {
20562
20838
  reader.cancel(reason).catch(() => {});
20563
20839
  }
20564
20840
  });
20565
- return { failed: false, errorPayload: null, response: new Response(rest, { status: res.status, headers: res.headers }) };
20841
+ return { failed: false, errorPayload: null, errorType: null, response: new Response(rest, { status: res.status, headers: res.headers }) };
20566
20842
  }
20567
20843
  async function dispatchPriority(c, orderedCandidateIds, sessionKey, wantsStream) {
20568
20844
  const bodyBuf = await c.req.arrayBuffer();
20569
20845
  let lastError = null;
20846
+ let lastStatus = 429;
20570
20847
  let previous = null;
20848
+ let previousReason = "rate_limit_error";
20571
20849
  for (const candidate of orderedCandidateIds) {
20572
20850
  const headers = new Headers(c.req.raw.headers);
20573
20851
  headers.set("x-meridian-profile", candidate);
20574
20852
  headers.set("x-meridian-priority-dispatch", "1");
20575
20853
  const inner = await app.fetch(new Request(c.req.url, { method: "POST", headers, body: bodyBuf }));
20576
- const { failed, errorPayload, response } = await sniffQuotaFailure(inner);
20577
- if (!failed) {
20854
+ const sniffed = await sniffAccountFailure(inner);
20855
+ if (!sniffed.failed) {
20578
20856
  if (sessionKey)
20579
20857
  priorityAssignments.set(sessionKey, candidate);
20580
20858
  if (previous) {
20581
- claudeLog("profile.failover", { from: previous, to: candidate, reason: "rate_limit_error", sessionKey });
20582
- plog(`[PROXY] PRIORITY failover ${previous} -> ${candidate}`);
20583
- }
20584
- return response;
20585
- }
20586
- const cooldownUntil = priorityCooldownUntil(candidate, Date.now());
20587
- priorityExhaustion.mark(candidate, cooldownUntil, "rate_limit_error");
20588
- claudeLog("priority.exhausted", { profile: candidate, until: cooldownUntil });
20589
- refinePriorityCooldown(candidate);
20590
- lastError = errorPayload;
20859
+ claudeLog("profile.failover", { from: previous, to: candidate, reason: previousReason, sessionKey });
20860
+ plog(`[PROXY] PRIORITY failover ${previous} -> ${candidate} (${previousReason})`);
20861
+ }
20862
+ return sniffed.response;
20863
+ }
20864
+ const reason = sniffed.errorType;
20865
+ const quotaRefusal = isQuotaRefusal(reason);
20866
+ const cooldownUntil = quotaRefusal ? priorityCooldownUntil(candidate, Date.now()) : Date.now() + PRIORITY_DEFAULT_COOLDOWN_MS;
20867
+ priorityExhaustion.mark(candidate, cooldownUntil, reason);
20868
+ claudeLog("priority.exhausted", { profile: candidate, until: cooldownUntil, reason });
20869
+ if (quotaRefusal)
20870
+ refinePriorityCooldown(candidate);
20871
+ lastError = sniffed.errorPayload;
20872
+ lastStatus = inner.status;
20591
20873
  previous = candidate;
20874
+ previousReason = reason;
20592
20875
  }
20593
20876
  if (wantsStream) {
20594
20877
  return new Response(`event: error
@@ -20599,7 +20882,7 @@ data: ${JSON.stringify(lastError)}
20599
20882
  headers: { "content-type": "text/event-stream; charset=utf-8", "cache-control": "no-cache" }
20600
20883
  });
20601
20884
  }
20602
- return new Response(JSON.stringify(lastError), { status: 429, headers: { "content-type": "application/json" } });
20885
+ return new Response(JSON.stringify(lastError), { status: lastStatus, headers: { "content-type": "application/json" } });
20603
20886
  }
20604
20887
  app.use("/auth/*", requireAuth);
20605
20888
  app.get("/", (c) => {
@@ -20617,6 +20900,7 @@ data: ${JSON.stringify(lastError)}
20617
20900
  const MAX_CONCURRENT_SESSIONS = parseInt((process.env.MERIDIAN_MAX_CONCURRENT ?? process.env.CLAUDE_PROXY_MAX_CONCURRENT) || "10", 10);
20618
20901
  let activeSessions = 0;
20619
20902
  const sessionQueue = [];
20903
+ const insideSessionSlot = new AsyncLocalStorage;
20620
20904
  async function acquireSession() {
20621
20905
  if (activeSessions < MAX_CONCURRENT_SESSIONS) {
20622
20906
  activeSessions++;
@@ -20797,6 +21081,25 @@ data: ${JSON.stringify(lastError)}
20797
21081
  if (lineageResult.type === "undo" && adapterBase === "opencode" && !agentSessionId) {
20798
21082
  lineageResult = { type: "diverged", reason: "missing-session-header" };
20799
21083
  }
21084
+ if (pipeline.some((t) => t.onSession)) {
21085
+ const mismatch = lineageResult.type === "diverged" ? lineageResult.mismatch : undefined;
21086
+ runTransformHook(pipeline, "onSession", {
21087
+ adapter: adapterBase,
21088
+ lineage: lineageResult.type,
21089
+ reason: lineageResult.type === "diverged" ? lineageResult.reason : undefined,
21090
+ sessionKey: profileSessionId,
21091
+ storedCount: mismatch?.storedCount,
21092
+ incomingCount: (body.messages || []).length,
21093
+ prefixOverlap: lineageResult.type === "diverged" ? lineageResult.prefixOverlap : undefined,
21094
+ mismatch: mismatch && mismatch.index >= 0 ? {
21095
+ index: mismatch.index,
21096
+ storedDigest: mismatch.storedDigest,
21097
+ incomingDigest: mismatch.incomingDigest,
21098
+ previousDigest: mismatch.previousDigest,
21099
+ incomingShape: mismatch.incomingShape
21100
+ } : undefined
21101
+ }, adapterBase);
21102
+ }
20800
21103
  const isResume = lineageResult.type === "continuation" || lineageResult.type === "compaction";
20801
21104
  const isUndo = lineageResult.type === "undo";
20802
21105
  const cachedSession = lineageResult.type !== "diverged" ? lineageResult.session : undefined;
@@ -21110,8 +21413,9 @@ data: ${JSON.stringify(lastError)}
21110
21413
  }
21111
21414
  let tokenRefreshed = false;
21112
21415
  let didFreshBaseRetry = false;
21113
- let busySessionRetries = 0;
21416
+ let resumeRefusalRetries = 0;
21114
21417
  let busySessionFork = false;
21418
+ let sawUnresumableRefusal = false;
21115
21419
  while (true) {
21116
21420
  let didYieldContent = false;
21117
21421
  const attemptStderrStart = stderrLines.length;
@@ -21174,29 +21478,33 @@ data: ${JSON.stringify(lastError)}
21174
21478
  releaseHeldDenies("non_stream_attempt_error");
21175
21479
  if (didYieldContent)
21176
21480
  throw error;
21177
- if (resumeSessionId && isBusySessionError(error, stderrLines.slice(attemptStderrStart).join(`
21178
- `))) {
21179
- if (busySessionRetries < BUSY_SESSION_MAX_RETRIES) {
21180
- busySessionRetries++;
21181
- claudeLog("session.busy_retry", { mode: "non_stream", attempt: busySessionRetries, resumeSessionId });
21182
- plog(`[PROXY] ${requestMeta.requestId} session busy (bg agent), retrying resume ${busySessionRetries}/${BUSY_SESSION_MAX_RETRIES}`);
21183
- await new Promise((resolve3) => setTimeout(resolve3, BUSY_SESSION_RETRY_DELAY_MS * busySessionRetries));
21481
+ const refusal = classifyResumeRefusal(error, resumeSessionId ? stderrLines.slice(attemptStderrStart).join(`
21482
+ `) : undefined);
21483
+ if (refusal === "unresumable")
21484
+ sawUnresumableRefusal = true;
21485
+ if (resumeSessionId && (refusal === "busy" || refusal === "unresumable")) {
21486
+ if (resumeRefusalRetries < RESUME_REFUSAL_MAX_RETRIES) {
21487
+ resumeRefusalRetries++;
21488
+ claudeLog("session.resume_retry", { mode: "non_stream", refusal, attempt: resumeRefusalRetries, resumeSessionId });
21489
+ plog(`[PROXY] ${requestMeta.requestId} resume refused (${refusal}), retrying ${resumeRefusalRetries}/${RESUME_REFUSAL_MAX_RETRIES}`);
21490
+ await new Promise((resolve3) => setTimeout(resolve3, RESUME_REFUSAL_RETRY_DELAY_MS * resumeRefusalRetries));
21184
21491
  continue;
21185
21492
  }
21186
- if (!busySessionFork) {
21493
+ if (refusal === "busy" && !busySessionFork) {
21187
21494
  busySessionFork = true;
21188
21495
  claudeLog("session.busy_fork", { mode: "non_stream", resumeSessionId });
21189
- plog(`[PROXY] ${requestMeta.requestId} session still busy after ${BUSY_SESSION_MAX_RETRIES} retries — forking session`);
21496
+ plog(`[PROXY] ${requestMeta.requestId} session still busy after ${RESUME_REFUSAL_MAX_RETRIES} retries — forking session`);
21190
21497
  continue;
21191
21498
  }
21192
21499
  }
21193
- if (isStaleSessionError(error)) {
21194
- claudeLog("session.stale_uuid_retry", {
21500
+ if (refusal === "missing-message" || sawUnresumableRefusal) {
21501
+ claudeLog("session.resume_replay", {
21195
21502
  mode: "non_stream",
21503
+ refusal,
21196
21504
  rollbackUuid: undoRollbackUuid,
21197
21505
  resumeSessionId
21198
21506
  });
21199
- plog(`[PROXY] Stale session UUID, evicting and retrying as fresh session`);
21507
+ plog(`[PROXY] ${requestMeta.requestId} session unusable (${refusal}), evicting and replaying as fresh session`);
21200
21508
  evictSession(profileSessionId, profileScopedCwd, allMessages);
21201
21509
  sdkUuidMap.length = 0;
21202
21510
  for (let i = 0;i < allMessages.length; i++)
@@ -21467,7 +21775,12 @@ data: ${JSON.stringify(lastError)}
21467
21775
  Subprocess stderr: ${stderrOutput}`;
21468
21776
  }
21469
21777
  const sdkTerm = extractSdkTermination(error instanceof Error ? error.message : String(error));
21470
- const canRecoverAsToolUse = passthrough && capturedToolUses.length > 0 && (sdkTerm.reason === "max_turns" || sdkTerm.reason === "aborted");
21778
+ const canRecoverAsToolUse = canRecoverCapturedToolUses({
21779
+ reason: sdkTerm.reason,
21780
+ passthrough,
21781
+ capturedToolUses: capturedToolUses.length,
21782
+ abortIsOurs: true
21783
+ });
21471
21784
  if (canRecoverAsToolUse) {
21472
21785
  diagnosticLog2.session(`${requestMeta.requestId} sdk_termination_recovered ${formatSdkTermination(sdkTerm, {
21473
21786
  model,
@@ -21751,8 +22064,9 @@ data: ${JSON.stringify({ type: "content_block_stop", index: idx })}
21751
22064
  }
21752
22065
  let tokenRefreshed = false;
21753
22066
  let didFreshBaseRetry = false;
21754
- let busySessionRetries = 0;
22067
+ let resumeRefusalRetries = 0;
21755
22068
  let busySessionFork = false;
22069
+ let sawUnresumableRefusal = false;
21756
22070
  while (true) {
21757
22071
  let didYieldClientEvent = false;
21758
22072
  const attemptStderrStart = stderrLines.length;
@@ -21813,29 +22127,33 @@ data: ${JSON.stringify({ type: "content_block_stop", index: idx })}
21813
22127
  const errMsg = error instanceof Error ? error.message : String(error);
21814
22128
  if (didYieldClientEvent)
21815
22129
  throw error;
21816
- if (resumeSessionId && isBusySessionError(error, stderrLines.slice(attemptStderrStart).join(`
21817
- `))) {
21818
- if (busySessionRetries < BUSY_SESSION_MAX_RETRIES) {
21819
- busySessionRetries++;
21820
- claudeLog("session.busy_retry", { mode: "stream", attempt: busySessionRetries, resumeSessionId });
21821
- plog(`[PROXY] ${requestMeta.requestId} session busy (bg agent), retrying resume ${busySessionRetries}/${BUSY_SESSION_MAX_RETRIES}`);
21822
- await new Promise((resolve3) => setTimeout(resolve3, BUSY_SESSION_RETRY_DELAY_MS * busySessionRetries));
22130
+ const refusal = classifyResumeRefusal(error, resumeSessionId ? stderrLines.slice(attemptStderrStart).join(`
22131
+ `) : undefined);
22132
+ if (refusal === "unresumable")
22133
+ sawUnresumableRefusal = true;
22134
+ if (resumeSessionId && (refusal === "busy" || refusal === "unresumable")) {
22135
+ if (resumeRefusalRetries < RESUME_REFUSAL_MAX_RETRIES) {
22136
+ resumeRefusalRetries++;
22137
+ claudeLog("session.resume_retry", { mode: "stream", refusal, attempt: resumeRefusalRetries, resumeSessionId });
22138
+ plog(`[PROXY] ${requestMeta.requestId} resume refused (${refusal}), retrying ${resumeRefusalRetries}/${RESUME_REFUSAL_MAX_RETRIES}`);
22139
+ await new Promise((resolve3) => setTimeout(resolve3, RESUME_REFUSAL_RETRY_DELAY_MS * resumeRefusalRetries));
21823
22140
  continue;
21824
22141
  }
21825
- if (!busySessionFork) {
22142
+ if (refusal === "busy" && !busySessionFork) {
21826
22143
  busySessionFork = true;
21827
22144
  claudeLog("session.busy_fork", { mode: "stream", resumeSessionId });
21828
- plog(`[PROXY] ${requestMeta.requestId} session still busy after ${BUSY_SESSION_MAX_RETRIES} retries — forking session`);
22145
+ plog(`[PROXY] ${requestMeta.requestId} session still busy after ${RESUME_REFUSAL_MAX_RETRIES} retries — forking session`);
21829
22146
  continue;
21830
22147
  }
21831
22148
  }
21832
- if (isStaleSessionError(error)) {
21833
- claudeLog("session.stale_uuid_retry", {
22149
+ if (refusal === "missing-message" || sawUnresumableRefusal) {
22150
+ claudeLog("session.resume_replay", {
21834
22151
  mode: "stream",
22152
+ refusal,
21835
22153
  rollbackUuid: undoRollbackUuid,
21836
22154
  resumeSessionId
21837
22155
  });
21838
- plog(`[PROXY] Stale session UUID, evicting and retrying as fresh session`);
22156
+ plog(`[PROXY] ${requestMeta.requestId} session unusable (${refusal}), evicting and replaying as fresh session`);
21839
22157
  evictSession(profileSessionId, profileScopedCwd, allMessages);
21840
22158
  sdkUuidMap.length = 0;
21841
22159
  for (let i = 0;i < allMessages.length; i++)
@@ -22673,7 +22991,12 @@ Subprocess stderr: ${stderrOutput}`;
22673
22991
  } : classifyError(errMsg, model);
22674
22992
  claudeLog("proxy.anthropic.error", { error: errMsg, classified: streamErr.type });
22675
22993
  const sdkTerm = extractSdkTermination(errMsg);
22676
- const canRecoverAsToolUse = (sdkTerm.reason === "max_turns" || sdkTerm.reason === "aborted" && (sawDuplicateToolUse || earlyStopFired)) && passthrough && capturedToolUses.length > 0 && messageStartEmitted;
22994
+ const canRecoverAsToolUse = canRecoverCapturedToolUses({
22995
+ reason: sdkTerm.reason,
22996
+ passthrough,
22997
+ capturedToolUses: capturedToolUses.length,
22998
+ abortIsOurs: sawDuplicateToolUse || earlyStopFired
22999
+ }) && messageStartEmitted;
22677
23000
  if (canRecoverAsToolUse) {
22678
23001
  diagnosticLog2.session(`${requestMeta.requestId} sdk_termination_recovered ${formatSdkTermination(sdkTerm, {
22679
23002
  model,
@@ -22800,7 +23123,7 @@ data: {"type":"message_stop"}
22800
23123
  error: streamErr.type
22801
23124
  });
22802
23125
  if (messageStartEmitted) {
22803
- const errorStopReason = textEventsForwarded > 0 ? "end_turn" : "max_tokens";
23126
+ const errorStopReason = "max_tokens";
22804
23127
  claudeLog("response.error_envelope", {
22805
23128
  mode: "stream",
22806
23129
  stopReason: errorStopReason,
@@ -22909,10 +23232,14 @@ data: ${JSON.stringify({
22909
23232
  const requestId = c.req.header("x-request-id") || randomUUID();
22910
23233
  const queueEnteredAt = Date.now();
22911
23234
  claudeLog("request.enter", { requestId, endpoint });
23235
+ const held = insideSessionSlot.getStore();
23236
+ if (held) {
23237
+ return handleMessages(c, { requestId, endpoint, ...held });
23238
+ }
22912
23239
  await acquireSession();
22913
23240
  const queueStartedAt = Date.now();
22914
23241
  try {
22915
- return await handleMessages(c, { requestId, endpoint, queueEnteredAt, queueStartedAt });
23242
+ return await insideSessionSlot.run({ queueEnteredAt, queueStartedAt }, () => handleMessages(c, { requestId, endpoint, queueEnteredAt, queueStartedAt }));
22916
23243
  } finally {
22917
23244
  releaseSession();
22918
23245
  }