@rynfar/meridian 1.57.0 → 1.58.0

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
@@ -141,9 +141,9 @@ This error class ([#516](https://github.com/rynfar/meridian/issues/516), histori
141
141
  If you still hit the error on a current release, first check `GET /v1/usage/quota` to rule out genuinely exhausted quota, then try disabling the connecting client's system prompt for the affected adapter while keeping the Claude Code prompt enabled (in the `/settings` UI under **SDK Feature Toggles**, or `PATCH /settings/api/features/<adapter>` with `{"clientSystemPrompt":false,"codeSystemPrompt":true}`) — and please report it on [#516](https://github.com/rynfar/meridian/issues/516) with your plan type, since remaining occurrences are likely account-cohort specific (Team plans are treated differently by the API).
142
142
 
143
143
  **I'm hitting rate limits on 1M context. What do I do?**
144
- Meridian defaults Sonnet to 200k context because Sonnet 1M is always billed as Extra Usage on Max plans — even when regular usage isn't exhausted. This is [Anthropic's intended billing model](https://code.claude.com/docs/en/model-config#extended-context), not a bug. Set `MERIDIAN_SONNET_MODEL=sonnet[1m]` to opt in if you have Extra Usage enabled and understand the billing implications. Opus defaults to 1M context, which is included with Max/Team/Enterprise subscriptions at no extra cost. Note: there is a [known upstream bug](https://github.com/anthropics/claude-code/issues/39841) where Claude Code incorrectly gates Opus 1M behind Extra Usage on Max — this is Anthropic's to fix.
144
+ Meridian defaults Sonnet to 200k context because Sonnet 1M is always billed as Extra Usage on Max plans — even when regular usage isn't exhausted. This is [Anthropic's intended billing model](https://code.claude.com/docs/en/model-config#extended-context), not a bug. Set `MERIDIAN_SONNET_MODEL=sonnet[1m]` to opt in if you have Extra Usage enabled and understand the billing implications. Opus defaults to 1M context, which is included with Max/Team/Enterprise subscriptions at no extra cost. Fable defaults to 1M too — verified as included on Max and Team accounts at no Extra Usage cost — and Mythos, which rides the Fable tier, inherits the same default. Note: there is a [known upstream bug](https://github.com/anthropics/claude-code/issues/39841) where Claude Code incorrectly gates Opus 1M behind Extra Usage on Max — this is Anthropic's to fix.
145
145
 
146
- To turn off 1M context entirely for **every** model (so Meridian never requests the extended window), set `MERIDIAN_1M_CONTEXT_SUPPORT=0`. Meridian also auto-detects the "out of extra usage" error, falls back to the 200k model, and skips 1M for an hour — so it self-heals after the first occurrence even without the env var.
146
+ To turn off 1M context entirely for **every** model (so Meridian never requests the extended window), set `MERIDIAN_1M_CONTEXT_SUPPORT=0`. To back off a single tier instead — without giving up the other tier's included 1M context — set `MERIDIAN_FABLE_MODEL=fable` or `MERIDIAN_OPUS_MODEL=opus` (both also accept the `CLAUDE_PROXY_` prefix). Meridian also auto-detects the "out of extra usage" error, falls back to the 200k model, and skips 1M for an hour — so it self-heals after the first occurrence even without the env var.
147
147
 
148
148
  **Why does the health endpoint show `"plugin": "not-configured"`?**
149
149
  You haven't run `meridian setup`. Without the plugin, OpenCode requests won't have session tracking or subagent model selection. Run `meridian setup` and restart OpenCode.
@@ -1,4 +1,5 @@
1
1
  import {
2
+ AssignmentStore,
2
3
  ProfileExhaustion,
3
4
  choosePriorityProfile,
4
5
  getActiveProfileId,
@@ -9,7 +10,7 @@ import {
9
10
  resolveProfile,
10
11
  restoreActiveProfile,
11
12
  setActiveProfile
12
- } from "./cli-ngtexmne.js";
13
+ } from "./cli-h6hfkg3s.js";
13
14
  import {
14
15
  isTrackedPlugin,
15
16
  recordError,
@@ -47,11 +48,11 @@ import {
47
48
  resolvePassthrough,
48
49
  resolveSdkModelDefaults,
49
50
  stripExtendedContext
50
- } from "./cli-bsg2dd52.js";
51
+ } from "./cli-p3ggjwgn.js";
51
52
  import {
52
53
  getSetting,
53
54
  setSetting
54
- } from "./cli-340h1chz.js";
55
+ } from "./cli-vj9cv18n.js";
55
56
  import {
56
57
  checkPluginConfigured
57
58
  } from "./cli-je60fevk.js";
@@ -59,11 +60,13 @@ import {
59
60
  claudeLog,
60
61
  createPlatformCredentialStore,
61
62
  ensureFreshToken,
63
+ getAuthRenewalStatus,
62
64
  refreshOAuthToken,
65
+ resolveRenewalWarnDays,
63
66
  startBackgroundRefresh,
64
67
  stopBackgroundRefresh,
65
68
  withClaudeLogContext
66
- } from "./cli-aq5zz92m.js";
69
+ } from "./cli-khhjyk04.js";
67
70
  import {
68
71
  __commonJS,
69
72
  __esm,
@@ -4364,24 +4367,40 @@ import { query } from "@anthropic-ai/claude-agent-sdk";
4364
4367
 
4365
4368
  // src/proxy/rateLimitStore.ts
4366
4369
  class RateLimitStore {
4367
- entries = new Map;
4368
- record(info, observedAt = Date.now()) {
4370
+ byProfile = new Map;
4371
+ record(profileId, info, observedAt = Date.now()) {
4369
4372
  if (!info || typeof info !== "object")
4370
4373
  return;
4371
4374
  const key = info.rateLimitType ?? "default";
4372
- this.entries.set(key, { ...info, observedAt });
4373
- }
4374
- getAll() {
4375
- return Array.from(this.entries.values()).sort((a, b) => b.observedAt - a.observedAt);
4376
- }
4377
- get(key) {
4378
- return this.entries.get(key);
4379
- }
4380
- get size() {
4381
- return this.entries.size;
4375
+ let buckets = this.byProfile.get(profileId);
4376
+ if (!buckets) {
4377
+ buckets = new Map;
4378
+ this.byProfile.set(profileId, buckets);
4379
+ }
4380
+ buckets.set(key, { ...info, observedAt });
4382
4381
  }
4383
- clear() {
4384
- this.entries.clear();
4382
+ getAll(profileId) {
4383
+ const buckets = this.byProfile.get(profileId);
4384
+ if (!buckets)
4385
+ return [];
4386
+ return Array.from(buckets.values()).sort((a, b) => b.observedAt - a.observedAt);
4387
+ }
4388
+ get(profileId, key) {
4389
+ return this.byProfile.get(profileId)?.get(key);
4390
+ }
4391
+ size(profileId) {
4392
+ if (profileId !== undefined)
4393
+ return this.byProfile.get(profileId)?.size ?? 0;
4394
+ let total = 0;
4395
+ for (const buckets of this.byProfile.values())
4396
+ total += buckets.size;
4397
+ return total;
4398
+ }
4399
+ clear(profileId) {
4400
+ if (profileId !== undefined)
4401
+ this.byProfile.delete(profileId);
4402
+ else
4403
+ this.byProfile.clear();
4385
4404
  }
4386
4405
  }
4387
4406
  var rateLimitStore = new RateLimitStore;
@@ -11086,13 +11105,25 @@ function buildModelList(isMaxSubscription, now = Math.floor(Date.now() / 1000))
11086
11105
  }
11087
11106
 
11088
11107
  // src/proxy/openaiResponses.ts
11108
+ function itemDiscriminator(item) {
11109
+ if (typeof item !== "object" || item === null)
11110
+ return;
11111
+ const record = item;
11112
+ if (typeof record.type === "string")
11113
+ return record.type;
11114
+ if (record.type === undefined && "role" in record)
11115
+ return "message";
11116
+ return;
11117
+ }
11089
11118
  function partsToText(content) {
11119
+ if (content === undefined)
11120
+ return "";
11090
11121
  if (typeof content === "string")
11091
11122
  return content;
11092
11123
  return content.filter((p) => typeof p.text === "string").map((p) => p.text).join("");
11093
11124
  }
11094
11125
  function partsToBlocks(content) {
11095
- if (typeof content === "string")
11126
+ if (content === undefined || typeof content === "string")
11096
11127
  return null;
11097
11128
  const hasImage = content.some((p) => p.type === "input_image");
11098
11129
  if (!hasImage)
@@ -11141,7 +11172,7 @@ function translateResponsesToAnthropic(body) {
11141
11172
  }
11142
11173
  };
11143
11174
  for (const item of items) {
11144
- switch (item.type) {
11175
+ switch (itemDiscriminator(item)) {
11145
11176
  case "message": {
11146
11177
  const msg = item;
11147
11178
  if (msg.role === "developer" || msg.role === "system") {
@@ -11833,6 +11864,12 @@ function getConversationFingerprint(messages, workingDirectory) {
11833
11864
  ${text.slice(0, 2000)}` : text.slice(0, 2000);
11834
11865
  return createHash("sha256").update(seed).digest("hex").slice(0, 16);
11835
11866
  }
11867
+ function getPriorityAssignmentKey(sessionId, messages, workingDirectory) {
11868
+ if (sessionId)
11869
+ return sessionId;
11870
+ const fingerprint = getConversationFingerprint(messages, workingDirectory);
11871
+ return fingerprint ? `fp:${fingerprint}` : null;
11872
+ }
11836
11873
 
11837
11874
  // src/proxy/adapters/opencode.ts
11838
11875
  init_env();
@@ -19189,36 +19226,34 @@ function findSuffixAnchorStart(storedHashes, incomingHashes, suffixOverlap) {
19189
19226
  return -1;
19190
19227
  return anchor - suffixOverlap + 1;
19191
19228
  }
19192
- function verifyLineage(cached, messages, cacheKey2, cache) {
19229
+ function verifyLineage(cached, messages) {
19193
19230
  if (!cached.lineageHash || cached.messageCount === 0) {
19194
- return { type: "continuation", session: cached };
19231
+ return { type: "diverged", reason: "unverifiable" };
19195
19232
  }
19196
19233
  const prefix = messages.slice(0, cached.messageCount);
19197
19234
  const prefixHash = computeLineageHash(prefix);
19198
19235
  if (prefixHash === cached.lineageHash) {
19199
19236
  if (messages.length <= cached.messageCount) {
19200
- cache.delete(cacheKey2);
19201
- return { type: "diverged" };
19237
+ return { type: "diverged", reason: "replayed-request" };
19202
19238
  }
19203
- return { type: "continuation", session: cached };
19239
+ return { type: "continuation", session: cached, resumeFrom: cached.messageCount };
19204
19240
  }
19205
19241
  if (!cached.messageHashes || cached.messageHashes.length === 0) {
19206
- cache.delete(cacheKey2);
19207
- return { type: "diverged" };
19242
+ return { type: "diverged", reason: "unverifiable" };
19208
19243
  }
19209
19244
  const incomingHashes = computeMessageHashes(messages);
19210
19245
  const prefixOverlap = measurePrefixOverlap(cached.messageHashes, incomingHashes);
19211
19246
  const suffixOverlap = measureSuffixOverlap(cached.messageHashes, incomingHashes);
19212
19247
  const MIN_STORED_FOR_COMPACTION = 6;
19213
19248
  const suffixStartInIncoming = incomingHashes.length - suffixOverlap >= 0 ? findSuffixAnchorStart(cached.messageHashes, incomingHashes, suffixOverlap) : -1;
19214
- if (suffixOverlap >= MIN_SUFFIX_FOR_COMPACTION && cached.messageHashes.length >= MIN_STORED_FOR_COMPACTION && suffixStartInIncoming > 0) {
19215
- const compactionMsg = `Compaction detected (key=${cacheKey2.slice(0, 8)}…): suffix overlap ${suffixOverlap}/${cached.messageHashes.length}. Allowing resume.`;
19216
- console.error(`[PROXY] ${compactionMsg}`);
19217
- diagnosticLog2.lineage(compactionMsg);
19218
- cached.lineageHash = computeLineageHash(messages);
19219
- cached.messageHashes = incomingHashes;
19220
- cached.messageCount = messages.length;
19221
- return { type: "compaction", session: cached };
19249
+ const compactionResumeFrom = suffixStartInIncoming + suffixOverlap;
19250
+ if (suffixOverlap >= MIN_SUFFIX_FOR_COMPACTION && cached.messageHashes.length >= MIN_STORED_FOR_COMPACTION && suffixStartInIncoming > 0 && compactionResumeFrom < messages.length) {
19251
+ return {
19252
+ type: "compaction",
19253
+ session: cached,
19254
+ resumeFrom: compactionResumeFrom,
19255
+ suffixOverlap
19256
+ };
19222
19257
  }
19223
19258
  if (prefixOverlap > 0 && suffixOverlap === 0 && messages.length <= cached.messageCount) {
19224
19259
  let rollbackUuid;
@@ -19230,22 +19265,12 @@ function verifyLineage(cached, messages, cacheKey2, cache) {
19230
19265
  }
19231
19266
  }
19232
19267
  }
19233
- const undoMsg = `Undo detected (key=${cacheKey2.slice(0, 8)}…): prefix overlap ${prefixOverlap}/${cached.messageHashes.length}, rollback UUID: ${rollbackUuid || "none (legacy session)"}.`;
19234
- console.error(`[PROXY] ${undoMsg}`);
19235
- diagnosticLog2.lineage(undoMsg);
19236
19268
  return { type: "undo", session: cached, prefixOverlap, rollbackUuid };
19237
19269
  }
19238
19270
  if (prefixOverlap > 0 && messages.length > cached.messageCount) {
19239
- const modifiedMsg = `Modified continuation (key=${cacheKey2.slice(0, 8)}…): prefix overlap ${prefixOverlap}/${cached.messageHashes.length}, incoming ${messages.length} msgs. Allowing resume.`;
19240
- console.error(`[PROXY] ${modifiedMsg}`);
19241
- diagnosticLog2.lineage(modifiedMsg);
19242
- cached.lineageHash = computeLineageHash(messages.slice(0, messages.length));
19243
- cached.messageHashes = incomingHashes;
19244
- cached.messageCount = messages.length;
19245
- return { type: "continuation", session: cached };
19271
+ return { type: "diverged", reason: "modified-history", prefixOverlap };
19246
19272
  }
19247
- cache.delete(cacheKey2);
19248
- return { type: "diverged" };
19273
+ return { type: "diverged", reason: "unrelated-history", prefixOverlap };
19249
19274
  }
19250
19275
 
19251
19276
  // src/proxy/sessionStore.ts
@@ -19546,11 +19571,28 @@ function touchSession(state) {
19546
19571
  state.lastAccess = Date.now();
19547
19572
  return state;
19548
19573
  }
19574
+ function classifyLineage(state, messages, cacheKey2) {
19575
+ const result = verifyLineage(state, messages);
19576
+ if (result.type === "compaction") {
19577
+ const msg = `Compaction detected (key=${cacheKey2.slice(0, 8)}…): suffix overlap ${result.suffixOverlap}/${state.messageCount}, resume from incoming message ${result.resumeFrom}.`;
19578
+ console.error(`[PROXY] ${msg}`);
19579
+ diagnosticLog2.lineage(msg);
19580
+ } else if (result.type === "undo") {
19581
+ const msg = `Undo detected (key=${cacheKey2.slice(0, 8)}…): prefix overlap ${result.prefixOverlap}/${state.messageCount}, rollback UUID: ${result.rollbackUuid || "none (legacy session)"}.`;
19582
+ console.error(`[PROXY] ${msg}`);
19583
+ diagnosticLog2.lineage(msg);
19584
+ } else if (result.type === "diverged" && result.reason === "modified-history") {
19585
+ const msg = `Stale session detected (key=${cacheKey2.slice(0, 8)}…): prefix overlap ${result.prefixOverlap || 0}/${state.messageCount}, incoming ${messages.length} msgs. Starting fresh replay.`;
19586
+ console.error(`[PROXY] ${msg}`);
19587
+ diagnosticLog2.lineage(msg);
19588
+ }
19589
+ return result;
19590
+ }
19549
19591
  function lookupSession(sessionId, messages, workingDirectory) {
19550
19592
  if (sessionId) {
19551
19593
  const cached = sessionCache.get(sessionId);
19552
19594
  if (cached) {
19553
- const result = verifyLineage(cached, messages, sessionId, sessionCache);
19595
+ const result = classifyLineage(cached, messages, sessionId);
19554
19596
  if (result.type === "continuation" || result.type === "compaction")
19555
19597
  touchSession(result.session);
19556
19598
  return result;
@@ -19566,19 +19608,19 @@ function lookupSession(sessionId, messages, workingDirectory) {
19566
19608
  sdkMessageUuids: shared.sdkMessageUuids,
19567
19609
  contextUsage: shared.contextUsage
19568
19610
  };
19569
- const result = verifyLineage(state, messages, sessionId, sessionCache);
19611
+ const result = classifyLineage(state, messages, sessionId);
19570
19612
  if (result.type === "continuation" || result.type === "compaction") {
19571
19613
  sessionCache.set(sessionId, state);
19572
19614
  }
19573
19615
  return result;
19574
19616
  }
19575
- return { type: "diverged" };
19617
+ return { type: "diverged", reason: "not-found" };
19576
19618
  }
19577
19619
  const fp = getConversationFingerprint(messages, workingDirectory);
19578
19620
  if (fp) {
19579
19621
  const cached = fingerprintCache.get(fp);
19580
19622
  if (cached) {
19581
- const result = verifyLineage(cached, messages, fp, fingerprintCache);
19623
+ const result = classifyLineage(cached, messages, fp);
19582
19624
  if (result.type === "continuation" || result.type === "compaction")
19583
19625
  touchSession(result.session);
19584
19626
  return result;
@@ -19594,14 +19636,14 @@ function lookupSession(sessionId, messages, workingDirectory) {
19594
19636
  sdkMessageUuids: shared.sdkMessageUuids,
19595
19637
  contextUsage: shared.contextUsage
19596
19638
  };
19597
- const result = verifyLineage(state, messages, fp, fingerprintCache);
19639
+ const result = classifyLineage(state, messages, fp);
19598
19640
  if (result.type === "continuation" || result.type === "compaction") {
19599
19641
  fingerprintCache.set(fp, state);
19600
19642
  }
19601
19643
  return result;
19602
19644
  }
19603
19645
  }
19604
- return { type: "diverged" };
19646
+ return { type: "diverged", reason: "not-found" };
19605
19647
  }
19606
19648
  function getSessionByClaudeId(claudeSessionId) {
19607
19649
  let newest;
@@ -19905,8 +19947,8 @@ function createProxyServer(config = {}) {
19905
19947
  app.use("/settings", requireAuth);
19906
19948
  app.use("/design-login", requireAuth);
19907
19949
  const priorityExhaustion = new ProfileExhaustion;
19908
- const priorityAssignments = new Map;
19909
19950
  const PRIORITY_ASSIGNMENTS_MAX = 5000;
19951
+ const priorityAssignments = new AssignmentStore(PRIORITY_ASSIGNMENTS_MAX);
19910
19952
  const PRIORITY_DEFAULT_COOLDOWN_MS = 10 * 60000;
19911
19953
  const PRIORITY_COOLDOWN_CAP_MS = 6 * 60 * 60000;
19912
19954
  function priorityProfileOrderSetting() {
@@ -19916,11 +19958,33 @@ function createProxyServer(config = {}) {
19916
19958
  const setting = getSetting("profileOrder");
19917
19959
  return Array.isArray(setting) && setting.length > 0 ? setting : undefined;
19918
19960
  }
19919
- function priorityCooldownUntil(now) {
19920
- const fiveHour = rateLimitStore.getAll().find((e) => e.rateLimitType === "five_hour" && (e.resetsAt ?? 0) > now);
19961
+ function priorityCooldownUntil(profileId, now) {
19962
+ const fiveHour = rateLimitStore.getAll(profileId).find((e) => e.rateLimitType === "five_hour" && (e.resetsAt ?? 0) > now && (e.status === "rejected" || (e.utilization ?? 0) >= 1));
19921
19963
  const until = fiveHour?.resetsAt ?? now + PRIORITY_DEFAULT_COOLDOWN_MS;
19922
19964
  return Math.min(until, now + PRIORITY_COOLDOWN_CAP_MS);
19923
19965
  }
19966
+ function refinePriorityCooldown(profileId) {
19967
+ const target = getEffectiveProfiles(finalConfig.profiles).find((p) => p.id === profileId);
19968
+ fetchOAuthUsage({ profileId, claudeConfigDir: target?.claudeConfigDir, force: true }).then((usage) => {
19969
+ if (!usage || usage.stale)
19970
+ return;
19971
+ const fiveHour = usage.windows.find((w) => w.type === "five_hour");
19972
+ if (!fiveHour || (fiveHour.utilization ?? 0) < 1)
19973
+ return;
19974
+ const now = Date.now();
19975
+ const resetsAt = fiveHour.resetsAt;
19976
+ if (!resetsAt || resetsAt <= now)
19977
+ return;
19978
+ const until = Math.min(resetsAt, now + PRIORITY_COOLDOWN_CAP_MS);
19979
+ priorityExhaustion.mark(profileId, until, "rate_limit_error");
19980
+ claudeLog("priority.cooldown_refined", { profile: profileId, until, source: "oauth_usage" });
19981
+ }).catch((err) => {
19982
+ claudeLog("priority.cooldown_refine_failed", {
19983
+ profile: profileId,
19984
+ error: err instanceof Error ? err.message : String(err)
19985
+ });
19986
+ });
19987
+ }
19924
19988
  async function sniffQuotaFailure(res) {
19925
19989
  const contentType = res.headers.get("content-type") ?? "";
19926
19990
  if (!contentType.includes("text/event-stream")) {
@@ -19995,22 +20059,18 @@ function createProxyServer(config = {}) {
19995
20059
  const inner = await app.fetch(new Request(c.req.url, { method: "POST", headers, body: bodyBuf }));
19996
20060
  const { failed, errorPayload, response } = await sniffQuotaFailure(inner);
19997
20061
  if (!failed) {
19998
- if (sessionKey) {
20062
+ if (sessionKey)
19999
20063
  priorityAssignments.set(sessionKey, candidate);
20000
- if (priorityAssignments.size > PRIORITY_ASSIGNMENTS_MAX) {
20001
- const oldest = priorityAssignments.keys().next().value;
20002
- if (oldest !== undefined)
20003
- priorityAssignments.delete(oldest);
20004
- }
20005
- }
20006
20064
  if (previous) {
20007
20065
  claudeLog("profile.failover", { from: previous, to: candidate, reason: "rate_limit_error", sessionKey });
20008
20066
  plog(`[PROXY] PRIORITY failover ${previous} -> ${candidate}`);
20009
20067
  }
20010
20068
  return response;
20011
20069
  }
20012
- priorityExhaustion.mark(candidate, priorityCooldownUntil(Date.now()), "rate_limit_error");
20013
- claudeLog("priority.exhausted", { profile: candidate, until: priorityCooldownUntil(Date.now()) });
20070
+ const cooldownUntil = priorityCooldownUntil(candidate, Date.now());
20071
+ priorityExhaustion.mark(candidate, cooldownUntil, "rate_limit_error");
20072
+ claudeLog("priority.exhausted", { profile: candidate, until: cooldownUntil });
20073
+ refinePriorityCooldown(candidate);
20014
20074
  lastError = errorPayload;
20015
20075
  previous = candidate;
20016
20076
  }
@@ -20098,7 +20158,8 @@ data: ${JSON.stringify(lastError)}
20098
20158
  const { order, unknown } = resolvePriorityOrder(effectivePool.map((p) => p.id), priorityProfileOrderSetting());
20099
20159
  if (unknown.length > 0)
20100
20160
  claudeLog("priority.unknown_order_ids", { unknown });
20101
- const sessionKey = adapter.getSessionId(c, body) || null;
20161
+ const assignmentCwd = adapter.extractClientWorkingDirectory?.(body) ?? adapter.extractWorkingDirectory(body);
20162
+ const sessionKey = getPriorityAssignmentKey(adapter.getSessionId(c, body), body.messages, assignmentCwd);
20102
20163
  const assigned = sessionKey ? priorityAssignments.get(sessionKey) : undefined;
20103
20164
  let first;
20104
20165
  if (assigned && order.includes(assigned) && !priorityExhaustion.isExhausted(assigned)) {
@@ -20224,14 +20285,15 @@ data: ${JSON.stringify(lastError)}
20224
20285
  claudeLog("session.pending_store_awaited", { waitedMs: Date.now() - waitStart });
20225
20286
  }
20226
20287
  }
20227
- let lineageResult = isIndependentSession ? { type: "diverged" } : lookupSession(profileSessionId, body.messages || [], profileScopedCwd);
20288
+ let lineageResult = isIndependentSession ? { type: "diverged", reason: "independent-request" } : lookupSession(profileSessionId, body.messages || [], profileScopedCwd);
20228
20289
  if (lineageResult.type === "undo" && adapterBase === "opencode" && !agentSessionId) {
20229
- lineageResult = { type: "diverged" };
20290
+ lineageResult = { type: "diverged", reason: "missing-session-header" };
20230
20291
  }
20231
20292
  const isResume = lineageResult.type === "continuation" || lineageResult.type === "compaction";
20232
20293
  const isUndo = lineageResult.type === "undo";
20233
20294
  const cachedSession = lineageResult.type !== "diverged" ? lineageResult.session : undefined;
20234
20295
  const resumeSessionId = cachedSession?.claudeSessionId;
20296
+ const resumeFrom = lineageResult.type === "continuation" || lineageResult.type === "compaction" ? lineageResult.resumeFrom : undefined;
20235
20297
  const undoRollbackUuid = isUndo && lineageResult.type === "undo" ? lineageResult.rollbackUuid : undefined;
20236
20298
  const msgSummary = body.messages?.map((m) => {
20237
20299
  const contentTypes = Array.isArray(m.content) ? m.content.map((b) => b.type).join(",") : "string";
@@ -20274,9 +20336,8 @@ data: ${JSON.stringify(lastError)}
20274
20336
  if (isUndo && undoRollbackUuid) {
20275
20337
  messagesToConvert = getLastUserMessage(allMessages);
20276
20338
  } else if (isResume) {
20277
- const knownCount = cachedSession.messageCount || 0;
20278
- if (knownCount > 0 && knownCount < allMessages.length) {
20279
- messagesToConvert = allMessages.slice(knownCount);
20339
+ if (resumeFrom !== undefined && resumeFrom < allMessages.length) {
20340
+ messagesToConvert = allMessages.slice(resumeFrom);
20280
20341
  } else {
20281
20342
  messagesToConvert = getLastUserMessage(allMessages);
20282
20343
  }
@@ -20569,7 +20630,7 @@ data: ${JSON.stringify(lastError)}
20569
20630
  advisorModel
20570
20631
  }, requestAbort.controller))) {
20571
20632
  if (event.type === "rate_limit_event") {
20572
- rateLimitStore.record(event.rate_limit_info);
20633
+ rateLimitStore.record(profile.id, event.rate_limit_info);
20573
20634
  }
20574
20635
  if (event.type === "assistant" && !event.error) {
20575
20636
  didYieldContent = true;
@@ -21151,7 +21212,7 @@ data: ${JSON.stringify({ type: "content_block_stop", index: idx })}
21151
21212
  advisorModel
21152
21213
  }, requestAbort.controller))) {
21153
21214
  if (event.type === "rate_limit_event") {
21154
- rateLimitStore.record(event.rate_limit_info);
21215
+ rateLimitStore.record(profile.id, event.rate_limit_info);
21155
21216
  }
21156
21217
  if (event.type === "stream_event") {
21157
21218
  didYieldClientEvent = true;
@@ -22229,13 +22290,17 @@ data: ${JSON.stringify({
22229
22290
  }, 503);
22230
22291
  }
22231
22292
  const claudeExecutableInfo = getResolvedClaudeExecutableInfo();
22293
+ const warnDays = resolveRenewalWarnDays(process.env.MERIDIAN_AUTH_RENEWAL_WARN_DAYS);
22294
+ const renewalConfigDir = profileEnvOverrides?.CLAUDE_CONFIG_DIR;
22295
+ const renewal = await getAuthRenewalStatus(renewalConfigDir ? createPlatformCredentialStore({ claudeConfigDir: renewalConfigDir }) : undefined, warnDays).catch(() => ({ renewalRequiredSoon: false }));
22232
22296
  return c.json({
22233
22297
  status: "healthy",
22234
22298
  version: serverVersion,
22235
22299
  auth: {
22236
22300
  loggedIn: true,
22237
22301
  email: auth.email,
22238
- subscriptionType: auth.subscriptionType
22302
+ subscriptionType: auth.subscriptionType,
22303
+ ...renewal
22239
22304
  },
22240
22305
  mode: envBool("PASSTHROUGH") ? "passthrough" : "internal",
22241
22306
  ...claudeExecutableInfo ? { claudeExecutable: claudeExecutableInfo } : {},
@@ -22302,7 +22367,6 @@ data: ${JSON.stringify({
22302
22367
  const previousProfile = getActiveProfileId() ?? null;
22303
22368
  setActiveProfile(body.profile);
22304
22369
  clearSessionCache();
22305
- rateLimitStore.clear();
22306
22370
  claudeLog("profile.switched", {
22307
22371
  from: previousProfile,
22308
22372
  to: body.profile,
@@ -22356,7 +22420,7 @@ data: ${JSON.stringify({
22356
22420
  const store = credentialStoreForProfile(profile);
22357
22421
  const success = store ? await refreshOAuthToken(store) : false;
22358
22422
  if (success) {
22359
- rateLimitStore.clear();
22423
+ rateLimitStore.clear(profile.id);
22360
22424
  return c.json({ success: true, message: "OAuth token refreshed successfully", profile: profile.id });
22361
22425
  }
22362
22426
  return c.json({ success: false, message: "Token refresh failed. If the problem persists, run 'claude login'." }, 500);
@@ -22579,11 +22643,11 @@ data: ${JSON.stringify({ response: { id: responseId, status: "failed", error: {
22579
22643
  return c.json({ object: "list", data: buildModelList(isMax) });
22580
22644
  });
22581
22645
  app.get("/v1/usage/quota", async (c) => {
22582
- const sdkEntries = rateLimitStore.getAll().filter((entry) => entry.rateLimitType !== undefined);
22583
22646
  const requestedProfile = c.req.query("profile");
22584
22647
  const profilesList = getEffectiveProfiles(finalConfig.profiles);
22585
22648
  const targetProfileId = requestedProfile || getActiveProfileId() || finalConfig.defaultProfile || profilesList[0]?.id || null;
22586
22649
  const targetProfile = targetProfileId ? profilesList.find((p) => p.id === targetProfileId) : undefined;
22650
+ const sdkEntries = rateLimitStore.getAll(targetProfileId ?? "default").filter((entry) => entry.rateLimitType !== undefined);
22587
22651
  const oauth = await fetchOAuthUsage({
22588
22652
  profileId: targetProfileId ?? undefined,
22589
22653
  claudeConfigDir: targetProfile?.claudeConfigDir
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  getSetting,
3
3
  setSetting
4
- } from "./cli-340h1chz.js";
4
+ } from "./cli-vj9cv18n.js";
5
5
 
6
6
  // src/proxy/profiles.ts
7
7
  import { existsSync, readFileSync } from "node:fs";
@@ -98,6 +98,34 @@ class ProfileExhaustion {
98
98
  }
99
99
  }
100
100
 
101
+ class AssignmentStore {
102
+ max;
103
+ entries = new Map;
104
+ constructor(max) {
105
+ this.max = max;
106
+ }
107
+ get(key) {
108
+ const value = this.entries.get(key);
109
+ if (value === undefined)
110
+ return;
111
+ this.entries.delete(key);
112
+ this.entries.set(key, value);
113
+ return value;
114
+ }
115
+ set(key, value) {
116
+ this.entries.delete(key);
117
+ this.entries.set(key, value);
118
+ if (this.entries.size > this.max) {
119
+ const oldest = this.entries.keys().next().value;
120
+ if (oldest !== undefined)
121
+ this.entries.delete(oldest);
122
+ }
123
+ }
124
+ get size() {
125
+ return this.entries.size;
126
+ }
127
+ }
128
+
101
129
  // src/proxy/profiles.ts
102
130
  var CONFIG_FILE = join(homedir(), ".config", "meridian", "profiles.json");
103
131
  var DISK_CACHE_TTL_MS = 5000;
@@ -213,4 +241,4 @@ function listProfiles(profiles, defaultProfile) {
213
241
  }));
214
242
  }
215
243
 
216
- export { getRoutingMode, resolvePriorityOrder, choosePriorityProfile, ProfileExhaustion, loadProfilesFromDisk, setActiveProfile, getActiveProfileId, resetActiveProfile, restoreActiveProfile, enableDiskProfileDiscovery, getEffectiveProfiles, hasProfiles, resolveProfile, listProfiles };
244
+ export { getRoutingMode, resolvePriorityOrder, choosePriorityProfile, ProfileExhaustion, AssignmentStore, loadProfilesFromDisk, setActiveProfile, getActiveProfileId, resetActiveProfile, restoreActiveProfile, enableDiskProfileDiscovery, getEffectiveProfiles, hasProfiles, resolveProfile, listProfiles };
@@ -235,16 +235,19 @@ async function doRefresh(store) {
235
235
  }
236
236
  const now = Date.now();
237
237
  const expiresAt = tokenData.expires_at ?? (tokenData.expires_in ? now + tokenData.expires_in * 1000 : now + 8 * 60 * 60 * 1000);
238
+ const refreshTokenExpiresAtRaw = tokenData.refresh_token_expires_at ?? (tokenData.refresh_token_expires_in ? now + tokenData.refresh_token_expires_in * 1000 : undefined);
239
+ const refreshTokenExpiresAt = refreshTokenExpiresAtRaw && refreshTokenExpiresAtRaw > now ? refreshTokenExpiresAtRaw : undefined;
238
240
  credentials.claudeAiOauth = {
239
241
  ...credentials.claudeAiOauth,
240
242
  accessToken: tokenData.access_token,
241
243
  refreshToken: tokenData.refresh_token ?? refreshToken,
242
- expiresAt
244
+ expiresAt,
245
+ ...refreshTokenExpiresAt ? { refreshTokenExpiresAt } : {}
243
246
  };
244
247
  const written = await store.write(credentials);
245
248
  if (!written)
246
249
  return false;
247
- claudeLog("token_refresh.success", { expiresAt });
250
+ claudeLog("token_refresh.success", { expiresAt, refreshTokenExpiresAt });
248
251
  return true;
249
252
  }
250
253
  async function ensureFreshToken(store, bufferMs = 5 * 60 * 1000) {
@@ -257,6 +260,62 @@ async function ensureFreshToken(store, bufferMs = 5 * 60 * 1000) {
257
260
  return true;
258
261
  return refreshOAuthToken(s);
259
262
  }
263
+ var DEFAULT_RENEWAL_WARN_DAYS = 3;
264
+ function resolveRenewalWarnDays(raw) {
265
+ const parsed = raw ? Number(raw) : NaN;
266
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_RENEWAL_WARN_DAYS;
267
+ }
268
+ var RENEWAL_EXPIRY_TTL_MS = 5 * 60000;
269
+ var renewalExpiryCache = new Map;
270
+ var renewalExpiryInflight = new Map;
271
+ function resetAuthRenewalCache() {
272
+ renewalExpiryCache.clear();
273
+ renewalExpiryInflight.clear();
274
+ }
275
+ async function readRefreshTokenExpiry(s) {
276
+ const key = s.refreshKey;
277
+ if (key) {
278
+ const cached = renewalExpiryCache.get(key);
279
+ if (cached && Date.now() - cached.at < RENEWAL_EXPIRY_TTL_MS)
280
+ return cached.value;
281
+ const inflight = renewalExpiryInflight.get(key);
282
+ if (inflight)
283
+ return inflight;
284
+ }
285
+ const read = (async () => {
286
+ let credentials = null;
287
+ try {
288
+ credentials = await s.read();
289
+ } catch {
290
+ return;
291
+ }
292
+ const value = credentials?.claudeAiOauth?.refreshTokenExpiresAt;
293
+ if (key)
294
+ renewalExpiryCache.set(key, { value, at: Date.now() });
295
+ return value;
296
+ })();
297
+ if (!key)
298
+ return read;
299
+ renewalExpiryInflight.set(key, read);
300
+ try {
301
+ return await read;
302
+ } finally {
303
+ renewalExpiryInflight.delete(key);
304
+ }
305
+ }
306
+ async function getAuthRenewalStatus(store, warnDays = DEFAULT_RENEWAL_WARN_DAYS) {
307
+ const s = store ?? createPlatformCredentialStore();
308
+ const refreshTokenExpiresAt = await readRefreshTokenExpiry(s);
309
+ if (!refreshTokenExpiresAt)
310
+ return { renewalRequiredSoon: false };
311
+ const msRemaining = refreshTokenExpiresAt - Date.now();
312
+ const daysUntilRenewal = Math.ceil(msRemaining / 86400000);
313
+ return {
314
+ refreshTokenExpiresAt,
315
+ daysUntilRenewal,
316
+ renewalRequiredSoon: daysUntilRenewal <= warnDays
317
+ };
318
+ }
260
319
  var scheduledRefreshTimer = null;
261
320
  var scheduledRefreshActive = false;
262
321
  var scheduledRefreshGeneration = 0;
@@ -315,4 +374,4 @@ function resetInflightRefresh() {
315
374
  inflightRefreshByKey.clear();
316
375
  }
317
376
 
318
- export { withClaudeLogContext, claudeLog, configDirToKeychainService, configDirToCredentialsFile, serializeCredentials, createPlatformCredentialStore, credentialsFilePathForProfile, refreshOAuthToken, ensureFreshToken, startBackgroundRefresh, stopBackgroundRefresh, isBackgroundRefreshActive, resetInflightRefresh };
377
+ export { withClaudeLogContext, claudeLog, configDirToKeychainService, configDirToCredentialsFile, serializeCredentials, createPlatformCredentialStore, credentialsFilePathForProfile, refreshOAuthToken, ensureFreshToken, DEFAULT_RENEWAL_WARN_DAYS, resolveRenewalWarnDays, resetAuthRenewalCache, getAuthRenewalStatus, startBackgroundRefresh, stopBackgroundRefresh, isBackgroundRefreshActive, resetInflightRefresh };