@rynfar/meridian 1.70.0 → 1.71.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/dist/{cli-198xnjcn.js → cli-6c6dj69q.js} +173 -14
- package/dist/cli.js +1 -1
- package/dist/meridian/index.js +4 -1
- package/dist/meridian-v2/index.js +126 -10
- package/dist/meridian-v2.js +133 -10
- package/dist/proxy/errors.d.ts +55 -0
- package/dist/proxy/errors.d.ts.map +1 -1
- package/dist/proxy/requestAbort.d.ts +36 -0
- package/dist/proxy/requestAbort.d.ts.map +1 -1
- package/dist/proxy/server.d.ts.map +1 -1
- package/dist/server.js +1 -1
- package/package.json +1 -1
- package/plugin/meridian-v2.ts +224 -10
- package/plugin/priority-attestation.ts +11 -1
|
@@ -6870,6 +6870,13 @@ function ordinalSuffix(n) {
|
|
|
6870
6870
|
function linkRequestAbort(signal) {
|
|
6871
6871
|
const controller = new AbortController;
|
|
6872
6872
|
let attached = false;
|
|
6873
|
+
const linkedAt = Date.now();
|
|
6874
|
+
let cause;
|
|
6875
|
+
const classify = () => {
|
|
6876
|
+
if (!controller.signal.aborted)
|
|
6877
|
+
return "none";
|
|
6878
|
+
return cause ?? "unknown_abort";
|
|
6879
|
+
};
|
|
6873
6880
|
const abort = (reason) => {
|
|
6874
6881
|
if (!controller.signal.aborted)
|
|
6875
6882
|
controller.abort(reason);
|
|
@@ -6889,6 +6896,19 @@ function linkRequestAbort(signal) {
|
|
|
6889
6896
|
return;
|
|
6890
6897
|
signal.removeEventListener("abort", forwardAbort);
|
|
6891
6898
|
attached = false;
|
|
6899
|
+
},
|
|
6900
|
+
setCause: (labeled) => {
|
|
6901
|
+
cause ??= labeled;
|
|
6902
|
+
},
|
|
6903
|
+
abortSnapshot: () => {
|
|
6904
|
+
const snapshot = {
|
|
6905
|
+
cause: classify(),
|
|
6906
|
+
aborted: controller.signal.aborted
|
|
6907
|
+
};
|
|
6908
|
+
if (controller.signal.aborted) {
|
|
6909
|
+
snapshot.elapsedMs = Math.max(0, Date.now() - linkedAt);
|
|
6910
|
+
}
|
|
6911
|
+
return snapshot;
|
|
6892
6912
|
}
|
|
6893
6913
|
};
|
|
6894
6914
|
}
|
|
@@ -23873,6 +23893,45 @@ function canRecoverCapturedToolUses(input) {
|
|
|
23873
23893
|
return false;
|
|
23874
23894
|
}
|
|
23875
23895
|
}
|
|
23896
|
+
function isStreamedToolBlockComplete(record2) {
|
|
23897
|
+
if (!record2.forwardedStart)
|
|
23898
|
+
return false;
|
|
23899
|
+
if (!record2.naturalStop)
|
|
23900
|
+
return false;
|
|
23901
|
+
if (record2.startedInputObject)
|
|
23902
|
+
return true;
|
|
23903
|
+
if (!record2.json.trim())
|
|
23904
|
+
return false;
|
|
23905
|
+
try {
|
|
23906
|
+
const parsed = JSON.parse(record2.json);
|
|
23907
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed);
|
|
23908
|
+
} catch {
|
|
23909
|
+
return false;
|
|
23910
|
+
}
|
|
23911
|
+
}
|
|
23912
|
+
function canRecoverUncapturedToolUses(input) {
|
|
23913
|
+
if (!input.uncapturedRecoveryEnabled)
|
|
23914
|
+
return false;
|
|
23915
|
+
if (!input.passthrough)
|
|
23916
|
+
return false;
|
|
23917
|
+
if (input.reason !== "max_turns")
|
|
23918
|
+
return false;
|
|
23919
|
+
if (input.attemptedMaxTurns !== 1)
|
|
23920
|
+
return false;
|
|
23921
|
+
if (input.capturedToolUses > 0)
|
|
23922
|
+
return false;
|
|
23923
|
+
if (input.streamedToolUses <= 0)
|
|
23924
|
+
return false;
|
|
23925
|
+
if (input.droppedToolUseIds > 0)
|
|
23926
|
+
return false;
|
|
23927
|
+
if (input.sawDuplicateToolUse)
|
|
23928
|
+
return false;
|
|
23929
|
+
if (input.forceSingleToolUse)
|
|
23930
|
+
return false;
|
|
23931
|
+
if (input.earlyStopFired)
|
|
23932
|
+
return false;
|
|
23933
|
+
return true;
|
|
23934
|
+
}
|
|
23876
23935
|
function extractSdkTermination(errMsg) {
|
|
23877
23936
|
const stderrTail = extractStderrTail(errMsg);
|
|
23878
23937
|
const haystack = `${errMsg}
|
|
@@ -23937,6 +23996,9 @@ function formatSdkTermination(t, ctx) {
|
|
|
23937
23996
|
parts.push(`deferred=${ctx.hasDeferredTools}`);
|
|
23938
23997
|
if (ctx.sdkSessionId)
|
|
23939
23998
|
parts.push(`session=${ctx.sdkSessionId.slice(0, 8)}`);
|
|
23999
|
+
if (ctx.abort) {
|
|
24000
|
+
parts.push(`abort=${ctx.abort.cause}`);
|
|
24001
|
+
}
|
|
23940
24002
|
if (t.rawTail)
|
|
23941
24003
|
parts.push(`raw=${JSON.stringify(t.rawTail)}`);
|
|
23942
24004
|
if (t.stderrTail)
|
|
@@ -36925,6 +36987,7 @@ function createProxyServer(config2 = {}) {
|
|
|
36925
36987
|
let durableWritesRevoked = false;
|
|
36926
36988
|
let inFlightRequests = 0;
|
|
36927
36989
|
const activeRequestAborts = new Set;
|
|
36990
|
+
const activeShutdownLabels = new Map;
|
|
36928
36991
|
const internalHopToken = randomUUID6();
|
|
36929
36992
|
const errorEnvelope = (shape, type, message) => shape === "anthropic" ? { type: "error", error: { type, message } } : { error: { type, message, code: null } };
|
|
36930
36993
|
const DRAIN_MESSAGE = "Meridian is shutting down and is not accepting new requests. Retry against another instance.";
|
|
@@ -37216,6 +37279,7 @@ function createProxyServer(config2 = {}) {
|
|
|
37216
37279
|
body: options.body,
|
|
37217
37280
|
forcedProfileId: candidate,
|
|
37218
37281
|
turnWatchdogSignal: options.turnWatchdogSignal,
|
|
37282
|
+
requestAbortLink: options.requestAbortLink,
|
|
37219
37283
|
forceFreshPriorityReplay: priorityPublication !== undefined && (options.durableRoute?.forceFreshReplay === true || options.currentProfileId !== undefined && candidate !== options.currentProfileId),
|
|
37220
37284
|
priorityPublication,
|
|
37221
37285
|
priorityAttemptExposure: exposure
|
|
@@ -37293,7 +37357,7 @@ data: ${JSON.stringify(lastError)}
|
|
|
37293
37357
|
const handleMessages = async (c, requestMeta, options) => {
|
|
37294
37358
|
const requestStartAt = requestMeta.queueEnteredAt;
|
|
37295
37359
|
const requestSignal = options.turnWatchdogSignal ? AbortSignal.any([c.req.raw.signal, options.turnWatchdogSignal]) : c.req.raw.signal;
|
|
37296
|
-
const requestAbort = linkRequestAbort(requestSignal);
|
|
37360
|
+
const requestAbort = options.requestAbortLink ?? linkRequestAbort(requestSignal);
|
|
37297
37361
|
let streamOwnsAbortLink = false;
|
|
37298
37362
|
return withClaudeLogContext({ requestId: requestMeta.requestId, endpoint: requestMeta.endpoint }, async () => {
|
|
37299
37363
|
const adapter = detectAdapter(c);
|
|
@@ -37628,6 +37692,7 @@ data: ${JSON.stringify(lastError)}
|
|
|
37628
37692
|
wantsStream: body.stream === true,
|
|
37629
37693
|
currentProfileId: assignedProfile,
|
|
37630
37694
|
turnWatchdogSignal: options.turnWatchdogSignal,
|
|
37695
|
+
requestAbortLink: options.requestAbortLink,
|
|
37631
37696
|
publicationTurn,
|
|
37632
37697
|
claimTurn: trustedTurn,
|
|
37633
37698
|
durableRoute
|
|
@@ -38310,6 +38375,7 @@ data: ${JSON.stringify(lastError)}
|
|
|
38310
38375
|
name: toolName,
|
|
38311
38376
|
reason: exceedsForcedSingle ? "forced_single" : "same_tool_repeat"
|
|
38312
38377
|
});
|
|
38378
|
+
requestAbort.setCause("passthrough_single_step");
|
|
38313
38379
|
requestAbort.abort("passthrough single-step complete");
|
|
38314
38380
|
} else {
|
|
38315
38381
|
capturedSignatures.add(signature);
|
|
@@ -38880,7 +38946,8 @@ Subprocess stderr: ${stderrOutput}`;
|
|
|
38880
38946
|
requestSource,
|
|
38881
38947
|
isResume,
|
|
38882
38948
|
hasDeferredTools,
|
|
38883
|
-
sdkSessionId: currentSessionId || resumeSessionId
|
|
38949
|
+
sdkSessionId: currentSessionId || resumeSessionId,
|
|
38950
|
+
abort: requestAbort.abortSnapshot()
|
|
38884
38951
|
})} captured=${capturedToolUses.length}`, requestMeta.requestId);
|
|
38885
38952
|
claudeLog("passthrough.max_turns_recovered", {
|
|
38886
38953
|
mode: "non_stream",
|
|
@@ -39165,11 +39232,14 @@ Subprocess stderr: ${stderrOutput}`;
|
|
|
39165
39232
|
let nextPassthroughToolCallAssistantUuid;
|
|
39166
39233
|
let nextPassthroughToolCallIds;
|
|
39167
39234
|
let sawCanonicalResult = false;
|
|
39235
|
+
const uncapturedToolRecoveryEnabled = env("PASSTHROUGH_UNCAPTURED_TOOL_RECOVERY") === "1";
|
|
39236
|
+
const streamedToolBlockRecords = new Map;
|
|
39168
39237
|
const silentTurnRecoveryEnabled = env("SILENT_TURN_RECOVERY") !== "0";
|
|
39169
39238
|
let silentTurnRecoveryAttempted = false;
|
|
39170
39239
|
let silentTurnRecovered = false;
|
|
39171
39240
|
const streamedToolUseIds = new Set;
|
|
39172
39241
|
let pendingTerminalDelta = null;
|
|
39242
|
+
let lastAttemptMaxTurns;
|
|
39173
39243
|
let pendingStructuredFrames = [];
|
|
39174
39244
|
let pendingStructuredTextLength = 0;
|
|
39175
39245
|
let terminalDeltaSent = false;
|
|
@@ -39313,6 +39383,7 @@ data: ${JSON.stringify({ type: "content_block_stop", index: idx })}
|
|
|
39313
39383
|
advisorModel
|
|
39314
39384
|
}, requestAbort.controller);
|
|
39315
39385
|
attemptMaxTurns = attemptQuery.options.maxTurns;
|
|
39386
|
+
lastAttemptMaxTurns = attemptMaxTurns;
|
|
39316
39387
|
for await (const event of runSdkQueryAttempt(attemptQuery, requestAbort.controller.signal, requestMeta, "stream", managedSdkAttemptLocators())) {
|
|
39317
39388
|
if (event.type === "rate_limit_event") {
|
|
39318
39389
|
rateLimitStore.record(profile.id, event.rate_limit_info);
|
|
@@ -39760,6 +39831,9 @@ data: ${JSON.stringify({ type: "content_block_stop", index: idx })}
|
|
|
39760
39831
|
if (eventType === "content_block_stop") {
|
|
39761
39832
|
flushToolArguments(clientIdx);
|
|
39762
39833
|
passthroughToolBlockNames.delete(eventIndex);
|
|
39834
|
+
const record3 = streamedToolBlockRecords.get(clientIdx);
|
|
39835
|
+
if (record3)
|
|
39836
|
+
record3.naturalStop = true;
|
|
39763
39837
|
}
|
|
39764
39838
|
}
|
|
39765
39839
|
if (eventType === "content_block_delta" && event.delta?.type === "text_delta" && shouldInjectSilentTurn({
|
|
@@ -39793,6 +39867,34 @@ data: ${JSON.stringify(event)}
|
|
|
39793
39867
|
if (typeof idx === "number")
|
|
39794
39868
|
openClientBlocks.delete(idx);
|
|
39795
39869
|
}
|
|
39870
|
+
if (passthrough && uncapturedToolRecoveryEnabled) {
|
|
39871
|
+
const clientIdx = eventIndex !== undefined ? sdkToClientIndex.get(eventIndex) ?? eventIndex : undefined;
|
|
39872
|
+
if (clientIdx !== undefined) {
|
|
39873
|
+
if (eventType === "content_block_start") {
|
|
39874
|
+
const block = event.content_block;
|
|
39875
|
+
if (block?.type === "tool_use" && typeof block?.id === "string" && block.id) {
|
|
39876
|
+
streamedToolBlockRecords.set(clientIdx, {
|
|
39877
|
+
id: block.id,
|
|
39878
|
+
name: block.name,
|
|
39879
|
+
json: "",
|
|
39880
|
+
startedInputObject: block.input !== undefined && block.input !== null,
|
|
39881
|
+
forwardedStart: true,
|
|
39882
|
+
naturalStop: false
|
|
39883
|
+
});
|
|
39884
|
+
}
|
|
39885
|
+
} else if (eventType === "content_block_delta") {
|
|
39886
|
+
const delta = event.delta;
|
|
39887
|
+
const record3 = streamedToolBlockRecords.get(clientIdx);
|
|
39888
|
+
if (record3 && delta?.type === "input_json_delta" && typeof delta.partial_json === "string") {
|
|
39889
|
+
record3.json += delta.partial_json;
|
|
39890
|
+
}
|
|
39891
|
+
} else if (eventType === "content_block_stop") {
|
|
39892
|
+
const record3 = streamedToolBlockRecords.get(clientIdx);
|
|
39893
|
+
if (record3)
|
|
39894
|
+
record3.naturalStop = true;
|
|
39895
|
+
}
|
|
39896
|
+
}
|
|
39897
|
+
}
|
|
39796
39898
|
if (passthrough && eventType === "message_delta" && event.delta?.stop_reason === "tool_use" && streamedToolUseIds.size > 0) {
|
|
39797
39899
|
flushOpenClientBlocks("drain_close");
|
|
39798
39900
|
if (earlyStopEnabled) {
|
|
@@ -40469,8 +40571,40 @@ Subprocess stderr: ${stderrOutput}`;
|
|
|
40469
40571
|
capturedToolUses: capturedToolUses.length,
|
|
40470
40572
|
abortIsOurs: sawDuplicateToolUse
|
|
40471
40573
|
}) && messageStartEmitted;
|
|
40574
|
+
const uncapturedEligible = (() => {
|
|
40575
|
+
if (!canRecoverUncapturedToolUses({
|
|
40576
|
+
reason: sdkTerm.reason,
|
|
40577
|
+
passthrough,
|
|
40578
|
+
capturedToolUses: capturedToolUses.length,
|
|
40579
|
+
streamedToolUses: streamedToolUseIds.size,
|
|
40580
|
+
droppedToolUseIds: droppedToolUseIds.size,
|
|
40581
|
+
sawDuplicateToolUse,
|
|
40582
|
+
forceSingleToolUse,
|
|
40583
|
+
earlyStopFired,
|
|
40584
|
+
uncapturedRecoveryEnabled: uncapturedToolRecoveryEnabled,
|
|
40585
|
+
attemptedMaxTurns: lastAttemptMaxTurns
|
|
40586
|
+
}))
|
|
40587
|
+
return false;
|
|
40588
|
+
if (!messageStartEmitted || streamClosed || pendingTerminalDelta)
|
|
40589
|
+
return false;
|
|
40590
|
+
if (durableWritesRevoked)
|
|
40591
|
+
return false;
|
|
40592
|
+
if (requestAbort.abortSnapshot().aborted)
|
|
40593
|
+
return false;
|
|
40594
|
+
for (const record3 of streamedToolBlockRecords.values()) {
|
|
40595
|
+
if (!isStreamedToolBlockComplete(record3))
|
|
40596
|
+
return false;
|
|
40597
|
+
const declared = requestTools.some((t) => t.name === record3.name);
|
|
40598
|
+
if (!declared)
|
|
40599
|
+
return false;
|
|
40600
|
+
}
|
|
40601
|
+
if (streamedToolBlockRecords.size !== streamedToolUseIds.size)
|
|
40602
|
+
return false;
|
|
40603
|
+
return true;
|
|
40604
|
+
})();
|
|
40605
|
+
const uncapturedRecoveryActive = uncapturedEligible;
|
|
40472
40606
|
const recoverableCheckpoint = canRecoverAsToolUse && sdkTerm.reason === "max_turns" && Boolean(currentSessionId) && Boolean(nextPassthroughToolCallAssistantUuid) && Boolean(nextPassthroughToolCallIds?.length) && earlyStopFired && !isIndependentSession && !sawDuplicateToolUse;
|
|
40473
|
-
const mustEvictBeforeRecoveredTerminal = !isIndependentSession && canRecoverAsToolUse && !recoverableCheckpoint;
|
|
40607
|
+
const mustEvictBeforeRecoveredTerminal = !isIndependentSession && canRecoverAsToolUse && !recoverableCheckpoint || !isIndependentSession && uncapturedRecoveryActive && !recoverableCheckpoint;
|
|
40474
40608
|
if (mustEvictBeforeRecoveredTerminal || !isIndependentSession && passthrough && streamedToolUseIds.size > 0 && !sawCanonicalResult && !recoverableCheckpoint) {
|
|
40475
40609
|
const evicted = evictSession2(profileSessionId, profileScopedCwd, lineageMessages, mappingExpectedGeneration);
|
|
40476
40610
|
if (mustEvictBeforeRecoveredTerminal && !evicted) {
|
|
@@ -40478,15 +40612,16 @@ Subprocess stderr: ${stderrOutput}`;
|
|
|
40478
40612
|
}
|
|
40479
40613
|
claudeLog("passthrough.noncanonical_session_evicted", { mode: "stream", reason: "drain_error" });
|
|
40480
40614
|
}
|
|
40481
|
-
if (canRecoverAsToolUse) {
|
|
40615
|
+
if (canRecoverAsToolUse || uncapturedRecoveryActive) {
|
|
40482
40616
|
idleStalls.clear(idleStallSessionKey);
|
|
40483
|
-
diagnosticLog2.session(`${requestMeta.requestId} sdk_termination_recovered ${formatSdkTermination(sdkTerm, {
|
|
40617
|
+
diagnosticLog2.session(`${requestMeta.requestId} sdk_termination_recovered${uncapturedRecoveryActive ? "_uncaptured" : ""} ${formatSdkTermination(sdkTerm, {
|
|
40484
40618
|
model,
|
|
40485
40619
|
requestSource,
|
|
40486
40620
|
isResume,
|
|
40487
40621
|
hasDeferredTools,
|
|
40488
|
-
sdkSessionId: currentSessionId || resumeSessionId
|
|
40489
|
-
|
|
40622
|
+
sdkSessionId: currentSessionId || resumeSessionId,
|
|
40623
|
+
abort: requestAbort.abortSnapshot()
|
|
40624
|
+
})} captured=${capturedToolUses.length}${uncapturedRecoveryActive ? ` completed=${streamedToolBlockRecords.size}` : ""}`, requestMeta.requestId);
|
|
40490
40625
|
flushOpenClientBlocks("recovery");
|
|
40491
40626
|
const unseenToolUses = capturedToolUses.filter((tu) => !streamedToolUseIds.has(tu.id));
|
|
40492
40627
|
for (let i = 0;i < unseenToolUses.length; i++) {
|
|
@@ -40627,7 +40762,8 @@ data: {"type":"message_stop"}
|
|
|
40627
40762
|
requestSource,
|
|
40628
40763
|
isResume,
|
|
40629
40764
|
hasDeferredTools,
|
|
40630
|
-
sdkSessionId: currentSessionId || resumeSessionId
|
|
40765
|
+
sdkSessionId: currentSessionId || resumeSessionId,
|
|
40766
|
+
abort: requestAbort.abortSnapshot()
|
|
40631
40767
|
})} blocks=${nextClientBlockIndex}`, requestMeta.requestId);
|
|
40632
40768
|
claudeLog("passthrough.capped_turn_truncated", {
|
|
40633
40769
|
mode: "stream",
|
|
@@ -40700,7 +40836,8 @@ data: {"type":"message_stop"}
|
|
|
40700
40836
|
requestSource,
|
|
40701
40837
|
isResume,
|
|
40702
40838
|
hasDeferredTools,
|
|
40703
|
-
sdkSessionId: currentSessionId || resumeSessionId
|
|
40839
|
+
sdkSessionId: currentSessionId || resumeSessionId,
|
|
40840
|
+
abort: requestAbort.abortSnapshot()
|
|
40704
40841
|
})} envelope=${messageStartEmitted ? "open" : "unopened"} blocks=${contentBlocksForwarded} ` + `text=${textEventsForwarded} tools=${capturedToolUses.length}/${streamedToolUseIds.size}`, requestMeta.requestId);
|
|
40705
40842
|
const streamErrTotalMs = Date.now() - requestStartAt;
|
|
40706
40843
|
const streamErrQueueWaitMs = totalQueueWaitMs(requestMeta);
|
|
@@ -40780,15 +40917,18 @@ data: ${JSON.stringify({
|
|
|
40780
40917
|
await abandonManagedFork("stream_complete_without_commit");
|
|
40781
40918
|
if (priorityRollbackRetirement)
|
|
40782
40919
|
await priorityRollbackRetirement;
|
|
40783
|
-
|
|
40920
|
+
if (!streamOwnsAbortLink)
|
|
40921
|
+
requestAbort.detach();
|
|
40784
40922
|
}
|
|
40785
40923
|
})().finally(() => {
|
|
40786
40924
|
resolveStreamCompletion();
|
|
40787
40925
|
});
|
|
40788
40926
|
},
|
|
40789
40927
|
cancel(reason) {
|
|
40928
|
+
requestAbort.setCause("stream_cancel");
|
|
40790
40929
|
requestAbort.abort(reason);
|
|
40791
|
-
|
|
40930
|
+
if (!streamOwnsAbortLink)
|
|
40931
|
+
requestAbort.detach();
|
|
40792
40932
|
requestMeta.cascadeSubtreeCancel?.("stream_cancel");
|
|
40793
40933
|
if (!isIndependentSession && (!managedForkTarget || managedForkPublished || clientAssistantContentExposed)) {
|
|
40794
40934
|
evictSession2(profileSessionId, profileScopedCwd, lineageMessages, mappingExpectedGeneration);
|
|
@@ -40821,7 +40961,8 @@ data: ${JSON.stringify({
|
|
|
40821
40961
|
claudeLog("proxy.error", { error: errMsg, classified: classified.type });
|
|
40822
40962
|
const sdkTerm = extractSdkTermination(errMsg);
|
|
40823
40963
|
diagnosticLog2.error(`${requestMeta.requestId} ${formatSdkTermination(sdkTerm, {
|
|
40824
|
-
requestSource: c.req.header("x-meridian-source")?.slice(0, 64) || undefined
|
|
40964
|
+
requestSource: c.req.header("x-meridian-source")?.slice(0, 64) || undefined,
|
|
40965
|
+
abort: requestAbort.abortSnapshot()
|
|
40825
40966
|
})}`, requestMeta.requestId);
|
|
40826
40967
|
const errorQueueWaitMs = totalQueueWaitMs(requestMeta);
|
|
40827
40968
|
const errorTotalMs = Date.now() - requestStartAt;
|
|
@@ -40908,6 +41049,14 @@ data: ${JSON.stringify({
|
|
|
40908
41049
|
};
|
|
40909
41050
|
const turnWatchdogAbort = new AbortController;
|
|
40910
41051
|
activeRequestAborts.add(turnWatchdogAbort);
|
|
41052
|
+
const requestSignalForLink = AbortSignal.any([c.req.raw.signal, turnWatchdogAbort.signal]);
|
|
41053
|
+
const labelClientAbort = () => {
|
|
41054
|
+
if (!turnWatchdogAbort.signal.aborted)
|
|
41055
|
+
requestAbortLink?.setCause("client_abort");
|
|
41056
|
+
};
|
|
41057
|
+
c.req.raw.signal.addEventListener("abort", labelClientAbort, { once: true });
|
|
41058
|
+
const requestAbortLink = linkRequestAbort(requestSignalForLink);
|
|
41059
|
+
activeShutdownLabels.set(turnWatchdogAbort, () => requestAbortLink.setCause("process_shutdown"));
|
|
40911
41060
|
let finished = false;
|
|
40912
41061
|
let leaseReleased = false;
|
|
40913
41062
|
let retainSessionTurnFence = false;
|
|
@@ -40936,6 +41085,8 @@ data: ${JSON.stringify({
|
|
|
40936
41085
|
if (finished)
|
|
40937
41086
|
return;
|
|
40938
41087
|
finished = true;
|
|
41088
|
+
requestAbortLink.detach();
|
|
41089
|
+
c.req.raw.signal.removeEventListener("abort", labelClientAbort);
|
|
40939
41090
|
if (retainSessionTurnFence && (sessionTurnLease || crossProcessTurnLease)) {
|
|
40940
41091
|
leaseReleased = true;
|
|
40941
41092
|
if (leaseWatchdog)
|
|
@@ -40950,6 +41101,7 @@ data: ${JSON.stringify({
|
|
|
40950
41101
|
sessionTreeRegistration?.release();
|
|
40951
41102
|
sessionTreeRegistration = undefined;
|
|
40952
41103
|
activeRequestAborts.delete(turnWatchdogAbort);
|
|
41104
|
+
activeShutdownLabels.delete(turnWatchdogAbort);
|
|
40953
41105
|
inFlightRequests--;
|
|
40954
41106
|
};
|
|
40955
41107
|
let body;
|
|
@@ -40981,7 +41133,10 @@ data: ${JSON.stringify({
|
|
|
40981
41133
|
requestId,
|
|
40982
41134
|
sessionKey: agentSessionId,
|
|
40983
41135
|
parentKey: adapter.getParentSessionId?.(c, body),
|
|
40984
|
-
abort: (reason) =>
|
|
41136
|
+
abort: (reason) => {
|
|
41137
|
+
requestAbortLink.setCause("subtree_cancel");
|
|
41138
|
+
turnWatchdogAbort.abort(reason);
|
|
41139
|
+
}
|
|
40985
41140
|
});
|
|
40986
41141
|
subtreeSessionKey = agentSessionId;
|
|
40987
41142
|
const clientSignal = c.req.raw.signal;
|
|
@@ -41002,6 +41157,7 @@ data: ${JSON.stringify({
|
|
|
41002
41157
|
leaseWatchdog = setTimeout(() => {
|
|
41003
41158
|
claudeLog("session.turn_watchdog_abort", { requestId, heldMs: SESSION_TURN_MAX_HOLD_MS });
|
|
41004
41159
|
plog(`[PROXY] ${requestId} session turn exceeded ${SESSION_TURN_MAX_HOLD_MS}ms — aborting without releasing its fencing lease`);
|
|
41160
|
+
requestAbortLink.setCause("session_watchdog");
|
|
41005
41161
|
turnWatchdogAbort.abort(new Error("Session turn exceeded its maximum hold time"));
|
|
41006
41162
|
}, SESSION_TURN_MAX_HOLD_MS);
|
|
41007
41163
|
leaseWatchdog.unref?.();
|
|
@@ -41076,7 +41232,8 @@ data: ${JSON.stringify({
|
|
|
41076
41232
|
};
|
|
41077
41233
|
const response = await handleMessages(c, requestMeta, {
|
|
41078
41234
|
body,
|
|
41079
|
-
turnWatchdogSignal: turnWatchdogAbort.signal
|
|
41235
|
+
turnWatchdogSignal: turnWatchdogAbort.signal,
|
|
41236
|
+
requestAbortLink
|
|
41080
41237
|
});
|
|
41081
41238
|
const completion = responseCompletions.get(response);
|
|
41082
41239
|
if (completion) {
|
|
@@ -41901,6 +42058,8 @@ data: ${JSON.stringify({ response: { id: responseId, status: "failed", error: {
|
|
|
41901
42058
|
},
|
|
41902
42059
|
forceAbortInFlight: () => {
|
|
41903
42060
|
durableWritesRevoked = true;
|
|
42061
|
+
for (const label of activeShutdownLabels.values())
|
|
42062
|
+
label();
|
|
41904
42063
|
for (const controller of activeRequestAborts) {
|
|
41905
42064
|
controller.abort(new Error("Proxy shutdown grace period elapsed"));
|
|
41906
42065
|
}
|
package/dist/cli.js
CHANGED
package/dist/meridian/index.js
CHANGED
|
@@ -13,9 +13,12 @@ var MAX_HEADER_BYTES = 768;
|
|
|
13
13
|
var MAX_PAYLOAD_BYTES = 384;
|
|
14
14
|
var TURN_DIGEST_PATTERN = /^[A-Za-z0-9_-]{43}$/;
|
|
15
15
|
var SAFE_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
16
|
-
function
|
|
16
|
+
function meridianConfigDirectory() {
|
|
17
17
|
return process.env.MERIDIAN_CONFIG_DIR ?? join(homedir(), ".config", "meridian");
|
|
18
18
|
}
|
|
19
|
+
function configDirectory() {
|
|
20
|
+
return meridianConfigDirectory();
|
|
21
|
+
}
|
|
19
22
|
function priorityAttestationKeyPath() {
|
|
20
23
|
return join(configDirectory(), PRIORITY_ATTESTATION_KEY_FILE);
|
|
21
24
|
}
|
|
@@ -20192,6 +20192,10 @@ var Info6 = exports_Schema.Struct({
|
|
|
20192
20192
|
})
|
|
20193
20193
|
})));
|
|
20194
20194
|
|
|
20195
|
+
// plugin/meridian-v2.ts
|
|
20196
|
+
import { readFileSync as readFileSync2, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
20197
|
+
import { join as join3 } from "node:path";
|
|
20198
|
+
|
|
20195
20199
|
// plugin/priority-attestation.ts
|
|
20196
20200
|
import { createHash, createHmac } from "node:crypto";
|
|
20197
20201
|
import { readFileSync } from "node:fs";
|
|
@@ -20207,9 +20211,12 @@ var MAX_HEADER_BYTES = 768;
|
|
|
20207
20211
|
var MAX_PAYLOAD_BYTES = 384;
|
|
20208
20212
|
var TURN_DIGEST_PATTERN = /^[A-Za-z0-9_-]{43}$/;
|
|
20209
20213
|
var SAFE_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
20210
|
-
function
|
|
20214
|
+
function meridianConfigDirectory() {
|
|
20211
20215
|
return process.env.MERIDIAN_CONFIG_DIR ?? join2(homedir(), ".config", "meridian");
|
|
20212
20216
|
}
|
|
20217
|
+
function configDirectory() {
|
|
20218
|
+
return meridianConfigDirectory();
|
|
20219
|
+
}
|
|
20213
20220
|
function priorityAttestationKeyPath() {
|
|
20214
20221
|
return join2(configDirectory(), PRIORITY_ATTESTATION_KEY_FILE);
|
|
20215
20222
|
}
|
|
@@ -20307,6 +20314,9 @@ var PARENT_SESSION_ONE_SHOTS = new Set(["title", "summary"]);
|
|
|
20307
20314
|
var ATTACHED_COMPACTION_AGENT = "compaction";
|
|
20308
20315
|
var MODEL_DISCOVERY_TIMEOUT_MS = 3000;
|
|
20309
20316
|
var PROVIDER_READY_POLL_MS = 25;
|
|
20317
|
+
var CATALOG_CACHE_FILE = "opencode-v2-catalog.json";
|
|
20318
|
+
var CATALOG_CACHE_VERSION = 1;
|
|
20319
|
+
var CATALOG_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
20310
20320
|
var MERIDIAN_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
20311
20321
|
var SESSION_AFFINITY_HEADERS = [
|
|
20312
20322
|
"x-opencode-session",
|
|
@@ -20465,15 +20475,109 @@ async function loadMeridianModels(catalog, signal, fetcher = globalThis.fetch) {
|
|
|
20465
20475
|
if (configured.length > 0) {
|
|
20466
20476
|
const discovered = await Promise.all(configured.map(async ({ providerID, baseURL }) => {
|
|
20467
20477
|
const models = await fetchMeridianModels(baseURL, signal, fetcher);
|
|
20468
|
-
return models ? { providerID, models } : undefined;
|
|
20478
|
+
return models ? { providerID, baseURL: typeof baseURL === "string" ? baseURL : undefined, models } : undefined;
|
|
20469
20479
|
}));
|
|
20470
|
-
return
|
|
20480
|
+
return {
|
|
20481
|
+
configured: configured.map((provider) => provider.providerID),
|
|
20482
|
+
discovered: discovered.flatMap((result3) => result3 ? [result3] : [])
|
|
20483
|
+
};
|
|
20471
20484
|
}
|
|
20472
20485
|
if (Date.now() >= deadline)
|
|
20473
|
-
return [];
|
|
20486
|
+
return { configured: [], discovered: [] };
|
|
20474
20487
|
await new Promise((resolve2) => setTimeout(resolve2, PROVIDER_READY_POLL_MS));
|
|
20475
20488
|
}
|
|
20476
|
-
return [];
|
|
20489
|
+
return { configured: [], discovered: [] };
|
|
20490
|
+
}
|
|
20491
|
+
function catalogCachePath() {
|
|
20492
|
+
return join3(meridianConfigDirectory(), CATALOG_CACHE_FILE);
|
|
20493
|
+
}
|
|
20494
|
+
function parseCatalogCache(value3, now3) {
|
|
20495
|
+
const entries3 = new Map;
|
|
20496
|
+
if (!isRecord(value3) || value3.version !== CATALOG_CACHE_VERSION)
|
|
20497
|
+
return entries3;
|
|
20498
|
+
if (!isRecord(value3.providers))
|
|
20499
|
+
return entries3;
|
|
20500
|
+
for (const [providerID, entry] of Object.entries(value3.providers)) {
|
|
20501
|
+
if (!MERIDIAN_PROVIDERS.has(providerID) || !isRecord(entry))
|
|
20502
|
+
continue;
|
|
20503
|
+
const { baseURL, fetchedAt } = entry;
|
|
20504
|
+
if (typeof baseURL !== "string" || meridianModelsURL(baseURL) === undefined)
|
|
20505
|
+
continue;
|
|
20506
|
+
if (typeof fetchedAt !== "number" || !Number.isSafeInteger(fetchedAt) || fetchedAt <= 0)
|
|
20507
|
+
continue;
|
|
20508
|
+
if (fetchedAt > now3 || now3 - fetchedAt > CATALOG_CACHE_TTL_MS)
|
|
20509
|
+
continue;
|
|
20510
|
+
const models = parseCachedModels(entry.models);
|
|
20511
|
+
if (!models || models.length === 0)
|
|
20512
|
+
continue;
|
|
20513
|
+
entries3.set(providerID, { baseURL, models });
|
|
20514
|
+
}
|
|
20515
|
+
return entries3;
|
|
20516
|
+
}
|
|
20517
|
+
function parseCachedModels(value3) {
|
|
20518
|
+
if (!Array.isArray(value3))
|
|
20519
|
+
return;
|
|
20520
|
+
const models = [];
|
|
20521
|
+
const ids = new Set;
|
|
20522
|
+
for (const item of value3) {
|
|
20523
|
+
if (!isRecord(item))
|
|
20524
|
+
return;
|
|
20525
|
+
const { id: id2, name, contextWindow, efforts } = item;
|
|
20526
|
+
if (typeof id2 !== "string" || id2.length === 0 || id2.length > 256 || /[^\x21-\x7E]/.test(id2) || ids.has(id2) || typeof name !== "string" || name.trim().length === 0 || name.length > 256 || typeof contextWindow !== "number" || !Number.isSafeInteger(contextWindow) || contextWindow <= 0 || !Array.isArray(efforts) || efforts.some((effort) => typeof effort !== "string" || !MERIDIAN_EFFORTS.includes(effort))) {
|
|
20527
|
+
return;
|
|
20528
|
+
}
|
|
20529
|
+
ids.add(id2);
|
|
20530
|
+
models.push({ id: id2, name, contextWindow, efforts });
|
|
20531
|
+
}
|
|
20532
|
+
return models;
|
|
20533
|
+
}
|
|
20534
|
+
function serializeCatalogCache(discovered, now3) {
|
|
20535
|
+
const providers = {};
|
|
20536
|
+
for (const entry of discovered) {
|
|
20537
|
+
if (typeof entry.baseURL !== "string" || entry.models.length === 0)
|
|
20538
|
+
continue;
|
|
20539
|
+
providers[entry.providerID] = { baseURL: entry.baseURL, fetchedAt: now3, models: entry.models };
|
|
20540
|
+
}
|
|
20541
|
+
return `${JSON.stringify({ version: CATALOG_CACHE_VERSION, providers }, null, 2)}
|
|
20542
|
+
`;
|
|
20543
|
+
}
|
|
20544
|
+
function removeCatalogCache(remove4 = (path) => rmSync(path, { force: true })) {
|
|
20545
|
+
try {
|
|
20546
|
+
remove4(catalogCachePath());
|
|
20547
|
+
} catch {}
|
|
20548
|
+
}
|
|
20549
|
+
function readCatalogCache(now3, read = (path) => readFileSync2(path, "utf-8")) {
|
|
20550
|
+
try {
|
|
20551
|
+
return parseCatalogCache(JSON.parse(read(catalogCachePath())), now3);
|
|
20552
|
+
} catch {
|
|
20553
|
+
return new Map;
|
|
20554
|
+
}
|
|
20555
|
+
}
|
|
20556
|
+
function writeCatalogCache(discovered, now3, write = (path, contents) => {
|
|
20557
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
20558
|
+
writeFileSync(temporary, contents, { encoding: "utf-8", mode: 384 });
|
|
20559
|
+
renameSync(temporary, path);
|
|
20560
|
+
}) {
|
|
20561
|
+
try {
|
|
20562
|
+
const contents = serializeCatalogCache(discovered, now3);
|
|
20563
|
+
if (contents.includes('"providers": {}'))
|
|
20564
|
+
return;
|
|
20565
|
+
write(catalogCachePath(), contents);
|
|
20566
|
+
} catch {}
|
|
20567
|
+
}
|
|
20568
|
+
function resolveCatalogModels(catalog, discovered, cached3) {
|
|
20569
|
+
if (discovered.length > 0)
|
|
20570
|
+
return [...discovered];
|
|
20571
|
+
const seeded = [];
|
|
20572
|
+
for (const providerID of MERIDIAN_PROVIDERS) {
|
|
20573
|
+
const entry = cached3.get(providerID);
|
|
20574
|
+
if (!entry)
|
|
20575
|
+
continue;
|
|
20576
|
+
if (!catalog.provider.get(providerID))
|
|
20577
|
+
continue;
|
|
20578
|
+
seeded.push({ providerID, baseURL: entry.baseURL, models: [...entry.models] });
|
|
20579
|
+
}
|
|
20580
|
+
return seeded;
|
|
20477
20581
|
}
|
|
20478
20582
|
function applyMeridianModels(catalog, providerID, models) {
|
|
20479
20583
|
for (const model of models) {
|
|
@@ -20533,19 +20637,31 @@ var MeridianV2Plugin = define({
|
|
|
20533
20637
|
const modelDiscoveryController = new AbortController;
|
|
20534
20638
|
let discoveredModels = [];
|
|
20535
20639
|
let discoveryStarted = false;
|
|
20640
|
+
const cachedModels = readCatalogCache(Date.now());
|
|
20536
20641
|
registered.push(await context3.catalog.transform((catalog) => {
|
|
20537
|
-
for (const
|
|
20538
|
-
applyMeridianModels(catalog,
|
|
20642
|
+
for (const entry of resolveCatalogModels(catalog, discoveredModels, cachedModels)) {
|
|
20643
|
+
applyMeridianModels(catalog, entry.providerID, entry.models);
|
|
20539
20644
|
}
|
|
20540
20645
|
}));
|
|
20541
20646
|
const discoverModels = async () => {
|
|
20542
20647
|
if (discoveryStarted || modelDiscoveryController.signal.aborted)
|
|
20543
20648
|
return false;
|
|
20544
|
-
const
|
|
20545
|
-
if (
|
|
20649
|
+
const loaded = await loadMeridianModels(context3.catalog, modelDiscoveryController.signal).catch(() => ({ configured: [], discovered: [] }));
|
|
20650
|
+
if (modelDiscoveryController.signal.aborted)
|
|
20651
|
+
return false;
|
|
20652
|
+
if (loaded.configured.length === 0) {
|
|
20653
|
+
if (cachedModels.size > 0) {
|
|
20654
|
+
cachedModels.clear();
|
|
20655
|
+
removeCatalogCache();
|
|
20656
|
+
await context3.catalog.reload();
|
|
20657
|
+
}
|
|
20658
|
+
return false;
|
|
20659
|
+
}
|
|
20660
|
+
if (loaded.discovered.length === 0)
|
|
20546
20661
|
return false;
|
|
20547
20662
|
discoveryStarted = true;
|
|
20548
|
-
discoveredModels =
|
|
20663
|
+
discoveredModels = loaded.discovered;
|
|
20664
|
+
writeCatalogCache(loaded.discovered, Date.now());
|
|
20549
20665
|
await context3.catalog.reload();
|
|
20550
20666
|
return true;
|
|
20551
20667
|
};
|
package/dist/meridian-v2.js
CHANGED
|
@@ -20181,6 +20181,10 @@ var Info6 = exports_Schema.Struct({
|
|
|
20181
20181
|
})
|
|
20182
20182
|
})));
|
|
20183
20183
|
|
|
20184
|
+
// plugin/meridian-v2.ts
|
|
20185
|
+
import { readFileSync as readFileSync2, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
20186
|
+
import { join as join3 } from "node:path";
|
|
20187
|
+
|
|
20184
20188
|
// plugin/priority-attestation.ts
|
|
20185
20189
|
import { createHash, createHmac } from "node:crypto";
|
|
20186
20190
|
import { readFileSync } from "node:fs";
|
|
@@ -20196,9 +20200,12 @@ var MAX_HEADER_BYTES = 768;
|
|
|
20196
20200
|
var MAX_PAYLOAD_BYTES = 384;
|
|
20197
20201
|
var TURN_DIGEST_PATTERN = /^[A-Za-z0-9_-]{43}$/;
|
|
20198
20202
|
var SAFE_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
20199
|
-
function
|
|
20203
|
+
function meridianConfigDirectory() {
|
|
20200
20204
|
return process.env.MERIDIAN_CONFIG_DIR ?? join2(homedir(), ".config", "meridian");
|
|
20201
20205
|
}
|
|
20206
|
+
function configDirectory() {
|
|
20207
|
+
return meridianConfigDirectory();
|
|
20208
|
+
}
|
|
20202
20209
|
function priorityAttestationKeyPath() {
|
|
20203
20210
|
return join2(configDirectory(), PRIORITY_ATTESTATION_KEY_FILE);
|
|
20204
20211
|
}
|
|
@@ -20297,6 +20304,9 @@ var PARENT_SESSION_ONE_SHOTS = new Set(["title", "summary"]);
|
|
|
20297
20304
|
var ATTACHED_COMPACTION_AGENT = "compaction";
|
|
20298
20305
|
var MODEL_DISCOVERY_TIMEOUT_MS = 3000;
|
|
20299
20306
|
var PROVIDER_READY_POLL_MS = 25;
|
|
20307
|
+
var CATALOG_CACHE_FILE = "opencode-v2-catalog.json";
|
|
20308
|
+
var CATALOG_CACHE_VERSION = 1;
|
|
20309
|
+
var CATALOG_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
20300
20310
|
var MERIDIAN_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
20301
20311
|
var MERIDIAN_V2_EFFORTS = MERIDIAN_EFFORTS;
|
|
20302
20312
|
var SESSION_AFFINITY_HEADERS = [
|
|
@@ -20456,15 +20466,109 @@ async function loadMeridianModels(catalog, signal, fetcher = globalThis.fetch) {
|
|
|
20456
20466
|
if (configured.length > 0) {
|
|
20457
20467
|
const discovered = await Promise.all(configured.map(async ({ providerID, baseURL }) => {
|
|
20458
20468
|
const models = await fetchMeridianModels(baseURL, signal, fetcher);
|
|
20459
|
-
return models ? { providerID, models } : undefined;
|
|
20469
|
+
return models ? { providerID, baseURL: typeof baseURL === "string" ? baseURL : undefined, models } : undefined;
|
|
20460
20470
|
}));
|
|
20461
|
-
return
|
|
20471
|
+
return {
|
|
20472
|
+
configured: configured.map((provider) => provider.providerID),
|
|
20473
|
+
discovered: discovered.flatMap((result3) => result3 ? [result3] : [])
|
|
20474
|
+
};
|
|
20462
20475
|
}
|
|
20463
20476
|
if (Date.now() >= deadline)
|
|
20464
|
-
return [];
|
|
20477
|
+
return { configured: [], discovered: [] };
|
|
20465
20478
|
await new Promise((resolve2) => setTimeout(resolve2, PROVIDER_READY_POLL_MS));
|
|
20466
20479
|
}
|
|
20467
|
-
return [];
|
|
20480
|
+
return { configured: [], discovered: [] };
|
|
20481
|
+
}
|
|
20482
|
+
function catalogCachePath() {
|
|
20483
|
+
return join3(meridianConfigDirectory(), CATALOG_CACHE_FILE);
|
|
20484
|
+
}
|
|
20485
|
+
function parseCatalogCache(value3, now3) {
|
|
20486
|
+
const entries3 = new Map;
|
|
20487
|
+
if (!isRecord(value3) || value3.version !== CATALOG_CACHE_VERSION)
|
|
20488
|
+
return entries3;
|
|
20489
|
+
if (!isRecord(value3.providers))
|
|
20490
|
+
return entries3;
|
|
20491
|
+
for (const [providerID, entry] of Object.entries(value3.providers)) {
|
|
20492
|
+
if (!MERIDIAN_PROVIDERS.has(providerID) || !isRecord(entry))
|
|
20493
|
+
continue;
|
|
20494
|
+
const { baseURL, fetchedAt } = entry;
|
|
20495
|
+
if (typeof baseURL !== "string" || meridianModelsURL(baseURL) === undefined)
|
|
20496
|
+
continue;
|
|
20497
|
+
if (typeof fetchedAt !== "number" || !Number.isSafeInteger(fetchedAt) || fetchedAt <= 0)
|
|
20498
|
+
continue;
|
|
20499
|
+
if (fetchedAt > now3 || now3 - fetchedAt > CATALOG_CACHE_TTL_MS)
|
|
20500
|
+
continue;
|
|
20501
|
+
const models = parseCachedModels(entry.models);
|
|
20502
|
+
if (!models || models.length === 0)
|
|
20503
|
+
continue;
|
|
20504
|
+
entries3.set(providerID, { baseURL, models });
|
|
20505
|
+
}
|
|
20506
|
+
return entries3;
|
|
20507
|
+
}
|
|
20508
|
+
function parseCachedModels(value3) {
|
|
20509
|
+
if (!Array.isArray(value3))
|
|
20510
|
+
return;
|
|
20511
|
+
const models = [];
|
|
20512
|
+
const ids = new Set;
|
|
20513
|
+
for (const item of value3) {
|
|
20514
|
+
if (!isRecord(item))
|
|
20515
|
+
return;
|
|
20516
|
+
const { id: id2, name, contextWindow, efforts } = item;
|
|
20517
|
+
if (typeof id2 !== "string" || id2.length === 0 || id2.length > 256 || /[^\x21-\x7E]/.test(id2) || ids.has(id2) || typeof name !== "string" || name.trim().length === 0 || name.length > 256 || typeof contextWindow !== "number" || !Number.isSafeInteger(contextWindow) || contextWindow <= 0 || !Array.isArray(efforts) || efforts.some((effort) => typeof effort !== "string" || !MERIDIAN_EFFORTS.includes(effort))) {
|
|
20518
|
+
return;
|
|
20519
|
+
}
|
|
20520
|
+
ids.add(id2);
|
|
20521
|
+
models.push({ id: id2, name, contextWindow, efforts });
|
|
20522
|
+
}
|
|
20523
|
+
return models;
|
|
20524
|
+
}
|
|
20525
|
+
function serializeCatalogCache(discovered, now3) {
|
|
20526
|
+
const providers = {};
|
|
20527
|
+
for (const entry of discovered) {
|
|
20528
|
+
if (typeof entry.baseURL !== "string" || entry.models.length === 0)
|
|
20529
|
+
continue;
|
|
20530
|
+
providers[entry.providerID] = { baseURL: entry.baseURL, fetchedAt: now3, models: entry.models };
|
|
20531
|
+
}
|
|
20532
|
+
return `${JSON.stringify({ version: CATALOG_CACHE_VERSION, providers }, null, 2)}
|
|
20533
|
+
`;
|
|
20534
|
+
}
|
|
20535
|
+
function removeCatalogCache(remove4 = (path) => rmSync(path, { force: true })) {
|
|
20536
|
+
try {
|
|
20537
|
+
remove4(catalogCachePath());
|
|
20538
|
+
} catch {}
|
|
20539
|
+
}
|
|
20540
|
+
function readCatalogCache(now3, read = (path) => readFileSync2(path, "utf-8")) {
|
|
20541
|
+
try {
|
|
20542
|
+
return parseCatalogCache(JSON.parse(read(catalogCachePath())), now3);
|
|
20543
|
+
} catch {
|
|
20544
|
+
return new Map;
|
|
20545
|
+
}
|
|
20546
|
+
}
|
|
20547
|
+
function writeCatalogCache(discovered, now3, write = (path, contents) => {
|
|
20548
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
20549
|
+
writeFileSync(temporary, contents, { encoding: "utf-8", mode: 384 });
|
|
20550
|
+
renameSync(temporary, path);
|
|
20551
|
+
}) {
|
|
20552
|
+
try {
|
|
20553
|
+
const contents = serializeCatalogCache(discovered, now3);
|
|
20554
|
+
if (contents.includes('"providers": {}'))
|
|
20555
|
+
return;
|
|
20556
|
+
write(catalogCachePath(), contents);
|
|
20557
|
+
} catch {}
|
|
20558
|
+
}
|
|
20559
|
+
function resolveCatalogModels(catalog, discovered, cached3) {
|
|
20560
|
+
if (discovered.length > 0)
|
|
20561
|
+
return [...discovered];
|
|
20562
|
+
const seeded = [];
|
|
20563
|
+
for (const providerID of MERIDIAN_PROVIDERS) {
|
|
20564
|
+
const entry = cached3.get(providerID);
|
|
20565
|
+
if (!entry)
|
|
20566
|
+
continue;
|
|
20567
|
+
if (!catalog.provider.get(providerID))
|
|
20568
|
+
continue;
|
|
20569
|
+
seeded.push({ providerID, baseURL: entry.baseURL, models: [...entry.models] });
|
|
20570
|
+
}
|
|
20571
|
+
return seeded;
|
|
20468
20572
|
}
|
|
20469
20573
|
function applyMeridianModels(catalog, providerID, models) {
|
|
20470
20574
|
for (const model of models) {
|
|
@@ -20527,19 +20631,31 @@ var MeridianV2Plugin = define({
|
|
|
20527
20631
|
const modelDiscoveryController = new AbortController;
|
|
20528
20632
|
let discoveredModels = [];
|
|
20529
20633
|
let discoveryStarted = false;
|
|
20634
|
+
const cachedModels = readCatalogCache(Date.now());
|
|
20530
20635
|
registered.push(await context3.catalog.transform((catalog) => {
|
|
20531
|
-
for (const
|
|
20532
|
-
applyMeridianModels(catalog,
|
|
20636
|
+
for (const entry of resolveCatalogModels(catalog, discoveredModels, cachedModels)) {
|
|
20637
|
+
applyMeridianModels(catalog, entry.providerID, entry.models);
|
|
20533
20638
|
}
|
|
20534
20639
|
}));
|
|
20535
20640
|
const discoverModels = async () => {
|
|
20536
20641
|
if (discoveryStarted || modelDiscoveryController.signal.aborted)
|
|
20537
20642
|
return false;
|
|
20538
|
-
const
|
|
20539
|
-
if (
|
|
20643
|
+
const loaded = await loadMeridianModels(context3.catalog, modelDiscoveryController.signal).catch(() => ({ configured: [], discovered: [] }));
|
|
20644
|
+
if (modelDiscoveryController.signal.aborted)
|
|
20645
|
+
return false;
|
|
20646
|
+
if (loaded.configured.length === 0) {
|
|
20647
|
+
if (cachedModels.size > 0) {
|
|
20648
|
+
cachedModels.clear();
|
|
20649
|
+
removeCatalogCache();
|
|
20650
|
+
await context3.catalog.reload();
|
|
20651
|
+
}
|
|
20652
|
+
return false;
|
|
20653
|
+
}
|
|
20654
|
+
if (loaded.discovered.length === 0)
|
|
20540
20655
|
return false;
|
|
20541
20656
|
discoveryStarted = true;
|
|
20542
|
-
discoveredModels =
|
|
20657
|
+
discoveredModels = loaded.discovered;
|
|
20658
|
+
writeCatalogCache(loaded.discovered, Date.now());
|
|
20543
20659
|
await context3.catalog.reload();
|
|
20544
20660
|
return true;
|
|
20545
20661
|
};
|
|
@@ -20667,8 +20783,14 @@ var MeridianV2Plugin = define({
|
|
|
20667
20783
|
});
|
|
20668
20784
|
var meridian_v2_default = MeridianV2Plugin;
|
|
20669
20785
|
export {
|
|
20786
|
+
writeCatalogCache,
|
|
20670
20787
|
shouldDetachFromParentSession,
|
|
20788
|
+
serializeCatalogCache,
|
|
20789
|
+
resolveCatalogModels,
|
|
20790
|
+
removeCatalogCache,
|
|
20791
|
+
readCatalogCache,
|
|
20671
20792
|
parseMeridianModels,
|
|
20793
|
+
parseCatalogCache,
|
|
20672
20794
|
meridianModelsURL,
|
|
20673
20795
|
loadMeridianModels,
|
|
20674
20796
|
isRootV2Session,
|
|
@@ -20677,6 +20799,7 @@ export {
|
|
|
20677
20799
|
fetchMeridianModels,
|
|
20678
20800
|
fallbackAgentTraits,
|
|
20679
20801
|
meridian_v2_default as default,
|
|
20802
|
+
catalogCachePath,
|
|
20680
20803
|
applyMeridianV2Headers,
|
|
20681
20804
|
applyMeridianModels,
|
|
20682
20805
|
SUPPORTED_OPENCODE_V2_VERSION,
|
package/dist/proxy/errors.d.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Error classification for SDK errors.
|
|
3
3
|
* Maps raw error messages to structured HTTP error responses.
|
|
4
4
|
*/
|
|
5
|
+
import type { AbortCauseSnapshot } from "./requestAbort";
|
|
5
6
|
export interface ClassifiedError {
|
|
6
7
|
status: number;
|
|
7
8
|
type: string;
|
|
@@ -167,6 +168,58 @@ export declare function canRecoverCapturedToolUses(input: {
|
|
|
167
168
|
capturedToolUses: number;
|
|
168
169
|
abortIsOurs: boolean;
|
|
169
170
|
}): boolean;
|
|
171
|
+
/**
|
|
172
|
+
* Per-block completeness record for a tool_use the client received on the
|
|
173
|
+
* wire (uncaptured-recovery tracker). Populated only by real forwarding;
|
|
174
|
+
* `naturalStop` is set exclusively when the block's own content_block_stop
|
|
175
|
+
* was enqueued — a synthetic flush closure never counts, because a dangling
|
|
176
|
+
* block's arguments may be incomplete.
|
|
177
|
+
*/
|
|
178
|
+
export interface StreamedToolBlockRecord {
|
|
179
|
+
id: string;
|
|
180
|
+
name: string;
|
|
181
|
+
/** Accumulated input_json_delta partials (plus any inline start input). */
|
|
182
|
+
json: string;
|
|
183
|
+
/** True when the block carried an inline input object at start (no deltas). */
|
|
184
|
+
startedInputObject: boolean;
|
|
185
|
+
forwardedStart: boolean;
|
|
186
|
+
naturalStop: boolean;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Is a streamed-but-uncaptured tool call complete and executable?
|
|
190
|
+
* Pure function — no I/O.
|
|
191
|
+
*/
|
|
192
|
+
export declare function isStreamedToolBlockComplete(record: StreamedToolBlockRecord): boolean;
|
|
193
|
+
/**
|
|
194
|
+
* Can a failed passthrough turn whose tool_use blocks fully streamed but
|
|
195
|
+
* were NEVER captured by the PreToolUse hook still be delivered as a
|
|
196
|
+
* tool-use response?
|
|
197
|
+
*
|
|
198
|
+
* This is the 2026-09-10 0a95wd-tusk incident shape: an abort landing
|
|
199
|
+
* between stream completion and tool dispatch makes the CLI yield
|
|
200
|
+
* `max_turns_reached` WITHOUT running the hook, so captures are empty even
|
|
201
|
+
* though every streamed block is complete and names a declared client tool.
|
|
202
|
+
* This is a materially different trust basis from
|
|
203
|
+
* `canRecoverCapturedToolUses` (which requires the hook to have seen the
|
|
204
|
+
* calls) and is therefore a separate predicate, not a relaxed count.
|
|
205
|
+
*
|
|
206
|
+
* Callers must further verify: the attempted maxTurns was 1, the kill switch
|
|
207
|
+
* is enabled, no cancellation of any kind fired, no forced-single/duplicate/
|
|
208
|
+
* early-stop state exists, and the envelope is still open. Every streamed
|
|
209
|
+
* block must pass `isStreamedToolBlockComplete`.
|
|
210
|
+
*/
|
|
211
|
+
export declare function canRecoverUncapturedToolUses(input: {
|
|
212
|
+
reason: SdkTermination["reason"];
|
|
213
|
+
passthrough: boolean;
|
|
214
|
+
capturedToolUses: number;
|
|
215
|
+
streamedToolUses: number;
|
|
216
|
+
droppedToolUseIds: number;
|
|
217
|
+
sawDuplicateToolUse: boolean;
|
|
218
|
+
forceSingleToolUse: boolean;
|
|
219
|
+
earlyStopFired: boolean;
|
|
220
|
+
uncapturedRecoveryEnabled: boolean;
|
|
221
|
+
attemptedMaxTurns: number | undefined;
|
|
222
|
+
}): boolean;
|
|
170
223
|
export declare function extractSdkTermination(errMsg: string): SdkTermination;
|
|
171
224
|
/**
|
|
172
225
|
* Render an SdkTermination plus request context as a single greppable log line.
|
|
@@ -182,5 +235,7 @@ export declare function formatSdkTermination(t: SdkTermination, ctx: {
|
|
|
182
235
|
isResume?: boolean;
|
|
183
236
|
hasDeferredTools?: boolean;
|
|
184
237
|
sdkSessionId?: string;
|
|
238
|
+
/** Abort-cause snapshot: which Meridian-linked producer fired, if any. */
|
|
239
|
+
abort?: AbortCauseSnapshot;
|
|
185
240
|
}): string;
|
|
186
241
|
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/proxy/errors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;CAChB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAc1D;AAoKD,2EAA2E;AAC3E,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,OAAO,CAEpF;AAqFD;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,eAAe,CA0L7E;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAM3D;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,aAAa,GAAG,iBAAiB,CAAA;AAEtE;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAahG;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAI3E;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAGxD;AAuBD;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,SAAS,IAAI,MAAM,CAEhG;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAE5E;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAGjE;AAED;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,WAAW,GAAG,cAAc,GAAG,SAAS,GAAG,eAAe,GAAG,kBAAkB,GAAG,SAAS,CAAA;IACnG,sDAAsD;IACtD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,wDAAwD;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,uEAAuE;IACvE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,wDAAwD;IACxD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;wCAEoC;IACpC,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAuBD;;;;;;GAMG;AACH;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,EAAE;IAChD,MAAM,EAAE,cAAc,CAAC,QAAQ,CAAC,CAAA;IAChC,WAAW,EAAE,OAAO,CAAA;IACpB,gBAAgB,EAAE,MAAM,CAAA;IACxB,WAAW,EAAE,OAAO,CAAA;CACrB,GAAG,OAAO,CAYV;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc,CAiEpE;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,CAAC,EAAE,cAAc,EACjB,GAAG,EAAE;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/proxy/errors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AAExD,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;CAChB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAc1D;AAoKD,2EAA2E;AAC3E,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,OAAO,CAEpF;AAqFD;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,eAAe,CA0L7E;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAM3D;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,aAAa,GAAG,iBAAiB,CAAA;AAEtE;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAahG;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAI3E;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAGxD;AAuBD;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,SAAS,IAAI,MAAM,CAEhG;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAE5E;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAGjE;AAED;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,WAAW,GAAG,cAAc,GAAG,SAAS,GAAG,eAAe,GAAG,kBAAkB,GAAG,SAAS,CAAA;IACnG,sDAAsD;IACtD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,wDAAwD;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,uEAAuE;IACvE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,wDAAwD;IACxD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;wCAEoC;IACpC,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAuBD;;;;;;GAMG;AACH;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,EAAE;IAChD,MAAM,EAAE,cAAc,CAAC,QAAQ,CAAC,CAAA;IAChC,WAAW,EAAE,OAAO,CAAA;IACpB,gBAAgB,EAAE,MAAM,CAAA;IACxB,WAAW,EAAE,OAAO,CAAA;CACrB,GAAG,OAAO,CAYV;AAED;;;;;;GAMG;AACH,MAAM,WAAW,uBAAuB;IACtC,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,2EAA2E;IAC3E,IAAI,EAAE,MAAM,CAAA;IACZ,+EAA+E;IAC/E,kBAAkB,EAAE,OAAO,CAAA;IAC3B,cAAc,EAAE,OAAO,CAAA;IACvB,WAAW,EAAE,OAAO,CAAA;CACrB;AAED;;;GAGG;AACH,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,uBAAuB,GAC9B,OAAO,CAaT;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE;IAClD,MAAM,EAAE,cAAc,CAAC,QAAQ,CAAC,CAAA;IAChC,WAAW,EAAE,OAAO,CAAA;IACpB,gBAAgB,EAAE,MAAM,CAAA;IACxB,gBAAgB,EAAE,MAAM,CAAA;IACxB,iBAAiB,EAAE,MAAM,CAAA;IACzB,mBAAmB,EAAE,OAAO,CAAA;IAC5B,kBAAkB,EAAE,OAAO,CAAA;IAC3B,cAAc,EAAE,OAAO,CAAA;IACvB,yBAAyB,EAAE,OAAO,CAAA;IAClC,iBAAiB,EAAE,MAAM,GAAG,SAAS,CAAA;CACtC,GAAG,OAAO,CAeV;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc,CAiEpE;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,CAAC,EAAE,cAAc,EACjB,GAAG,EAAE;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,0EAA0E;IAC1E,KAAK,CAAC,EAAE,kBAAkB,CAAA;CAC3B,GACA,MAAM,CAkBR"}
|
|
@@ -1,7 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request-scoped abort link with an abort-cause registry.
|
|
3
|
+
*
|
|
4
|
+
* Meridian aborts a request's SDK query from several producers (client
|
|
5
|
+
* disconnect, cancelled response body, session turn watchdog, subtree
|
|
6
|
+
* cancellation, process shutdown, the proxy's own passthrough single-step
|
|
7
|
+
* abort). When a capped turn then dies with an opaque SDK termination, the
|
|
8
|
+
* operator needs to know WHICH producer fired — or that none did, which is
|
|
9
|
+
* the discriminating marker for the uncaptured-tool-turn incident
|
|
10
|
+
* (2026-09-10 0a95wd-tusk): an externally-initiated CLI abort masqueraded as
|
|
11
|
+
* "Reached maximum number of turns (1)".
|
|
12
|
+
*
|
|
13
|
+
* The registry latches the FIRST classified cause. `abortSnapshot` is the
|
|
14
|
+
* single read side used by diagnostics. `cause: "none"` means no
|
|
15
|
+
* Meridian-linked abort was observed — it is NOT proof that no abort occurred
|
|
16
|
+
* inside the CLI subprocess; only that nothing Meridian controls aborted.
|
|
17
|
+
*/
|
|
18
|
+
export type RequestAbortCause = "client_abort" | "stream_cancel" | "session_watchdog" | "process_shutdown" | "subtree_cancel" | "passthrough_single_step" | "unknown_abort";
|
|
19
|
+
export declare const REQUEST_ABORT_CAUSES: readonly RequestAbortCause[];
|
|
20
|
+
export interface AbortCauseSnapshot {
|
|
21
|
+
/** First classified cause; "none" when the observed signal never aborted. */
|
|
22
|
+
cause: RequestAbortCause | "none";
|
|
23
|
+
/** Whether the linked controller's signal is aborted. */
|
|
24
|
+
aborted: boolean;
|
|
25
|
+
/** Monotonic ms from link creation to abort; undefined when not aborted. */
|
|
26
|
+
elapsedMs?: number;
|
|
27
|
+
}
|
|
1
28
|
export interface RequestAbortLink {
|
|
2
29
|
controller: AbortController;
|
|
3
30
|
abort: (reason?: unknown) => void;
|
|
4
31
|
detach: () => void;
|
|
32
|
+
/**
|
|
33
|
+
* Latch the classified cause of an impending abort. First call wins; later
|
|
34
|
+
* labels are ignored so the earliest producer owns the diagnosis. Callers
|
|
35
|
+
* should label BEFORE invoking abort, and may label a cause that arrives
|
|
36
|
+
* with an already-aborted signal.
|
|
37
|
+
*/
|
|
38
|
+
setCause: (cause: RequestAbortCause) => void;
|
|
39
|
+
/** Read-side snapshot for diagnostics and telemetry. */
|
|
40
|
+
abortSnapshot: () => AbortCauseSnapshot;
|
|
5
41
|
}
|
|
6
42
|
/** Forward an HTTP request abort into the SDK query lifecycle. */
|
|
7
43
|
export declare function linkRequestAbort(signal: AbortSignal): RequestAbortLink;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"requestAbort.d.ts","sourceRoot":"","sources":["../../src/proxy/requestAbort.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,eAAe,CAAA;IAC3B,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;IACjC,MAAM,EAAE,MAAM,IAAI,CAAA;
|
|
1
|
+
{"version":3,"file":"requestAbort.d.ts","sourceRoot":"","sources":["../../src/proxy/requestAbort.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,iBAAiB,GACzB,cAAc,GACd,eAAe,GACf,kBAAkB,GAClB,kBAAkB,GAClB,gBAAgB,GAChB,yBAAyB,GACzB,eAAe,CAAA;AAEnB,eAAO,MAAM,oBAAoB,EAAE,SAAS,iBAAiB,EAQnD,CAAA;AAEV,MAAM,WAAW,kBAAkB;IACjC,6EAA6E;IAC7E,KAAK,EAAE,iBAAiB,GAAG,MAAM,CAAA;IACjC,yDAAyD;IACzD,OAAO,EAAE,OAAO,CAAA;IAChB,4EAA4E;IAC5E,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,eAAe,CAAA;IAC3B,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;IACjC,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB;;;;;OAKG;IACH,QAAQ,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAA;IAC5C,wDAAwD;IACxD,aAAa,EAAE,MAAM,kBAAkB,CAAA;CACxC;AAED,kEAAkE;AAClE,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,WAAW,GAAG,gBAAgB,CAgDtE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/proxy/server.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AACtE,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,CAAA;AAGvD,YAAY,EACV,SAAS,EACT,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACb,WAAW,GACZ,MAAM,aAAa,CAAA;AAKpB,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AA4EnG,OAAO,EACL,kBAAkB,EAElB,WAAW,EACX,oBAAoB,EAMpB,KAAK,aAAa,EAGnB,MAAM,mBAAmB,CAAA;AAI1B,OAAO,EAKL,iBAAiB,EACjB,mBAAmB,EAKpB,MAAM,iBAAiB,CAAA;AA0CxB,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,oBAAoB,EAAE,CAAA;AAChE,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,CAAA;AACjD,YAAY,EAAE,aAAa,EAAE,CAAA;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/proxy/server.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AACtE,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,CAAA;AAGvD,YAAY,EACV,SAAS,EACT,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACb,WAAW,GACZ,MAAM,aAAa,CAAA;AAKpB,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AA4EnG,OAAO,EACL,kBAAkB,EAElB,WAAW,EACX,oBAAoB,EAMpB,KAAK,aAAa,EAGnB,MAAM,mBAAmB,CAAA;AAI1B,OAAO,EAKL,iBAAiB,EACjB,mBAAmB,EAKpB,MAAM,iBAAiB,CAAA;AA0CxB,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,oBAAoB,EAAE,CAAA;AAChE,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,CAAA;AACjD,YAAY,EAAE,aAAa,EAAE,CAAA;AA+Y7B,wBAAgB,iBAAiB,CAAC,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM,GAAG,WAAW,CAsmPhF;AAWD,wBAAgB,gCAAgC,IAAI,IAAI,CAavD;AAED,wBAAsB,gBAAgB,CAAC,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CA8NhG"}
|
package/dist/server.js
CHANGED
package/package.json
CHANGED
package/plugin/meridian-v2.ts
CHANGED
|
@@ -18,11 +18,14 @@
|
|
|
18
18
|
import * as Plugin from "@opencode-ai/plugin/promise/plugin"
|
|
19
19
|
import { Model } from "@opencode-ai/schema/model"
|
|
20
20
|
import type { CatalogDraft } from "@opencode-ai/plugin/promise/catalog"
|
|
21
|
+
import { readFileSync, renameSync, rmSync, writeFileSync } from "node:fs"
|
|
22
|
+
import { join } from "node:path"
|
|
21
23
|
import {
|
|
22
24
|
PRIORITY_ATTESTATION_HEADER,
|
|
23
25
|
createPriorityAttestation,
|
|
24
26
|
deleteHeader,
|
|
25
27
|
getHeader,
|
|
28
|
+
meridianConfigDirectory,
|
|
26
29
|
setHeader,
|
|
27
30
|
type MutableHeaders,
|
|
28
31
|
} from "./priority-attestation"
|
|
@@ -36,6 +39,25 @@ const PARENT_SESSION_ONE_SHOTS = new Set(["title", "summary"])
|
|
|
36
39
|
const ATTACHED_COMPACTION_AGENT = "compaction"
|
|
37
40
|
const MODEL_DISCOVERY_TIMEOUT_MS = 3_000
|
|
38
41
|
const PROVIDER_READY_POLL_MS = 25
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Catalog cache, for the cold-start gap (#1008).
|
|
45
|
+
*
|
|
46
|
+
* Discovery cannot begin until OpenCode has finished assembling the catalog, so
|
|
47
|
+
* the first request against a freshly started server used to see only
|
|
48
|
+
* OpenCode's built-in models.dev entries and rejected a Meridian-only variant
|
|
49
|
+
* with `provider.no-route`. Awaiting the catalog inside `setup` deadlocks the
|
|
50
|
+
* server, so the seed has to come from somewhere that is not the catalog: a
|
|
51
|
+
* plain file, read synchronously before the first transform runs.
|
|
52
|
+
*
|
|
53
|
+
* The entry records the base URL it was discovered from and is only applied to a
|
|
54
|
+
* provider still pointed at that URL, so repointing OpenCode at a different
|
|
55
|
+
* Meridian cannot apply another one's models.
|
|
56
|
+
*/
|
|
57
|
+
const CATALOG_CACHE_FILE = "opencode-v2-catalog.json"
|
|
58
|
+
const CATALOG_CACHE_VERSION = 1
|
|
59
|
+
/** Bounds how stale a seed can be if discovery never succeeds again. */
|
|
60
|
+
const CATALOG_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1_000
|
|
39
61
|
// The plugin tree does not import from src/, so this mirrors VALID_EFFORTS in
|
|
40
62
|
// src/proxy/effort.ts. A test asserts the two stay identical. Filtering in this
|
|
41
63
|
// order also gives every model its variants low -> max regardless of the order
|
|
@@ -152,6 +174,8 @@ export interface MeridianModel {
|
|
|
152
174
|
|
|
153
175
|
export interface MeridianProviderModels {
|
|
154
176
|
providerID: string
|
|
177
|
+
/** The URL these models came from, so a cached seed is never applied to another. */
|
|
178
|
+
baseURL?: string
|
|
155
179
|
models: MeridianModel[]
|
|
156
180
|
}
|
|
157
181
|
|
|
@@ -260,11 +284,17 @@ function isLegacyMeridianBaseURL(baseURL: unknown): boolean {
|
|
|
260
284
|
}
|
|
261
285
|
}
|
|
262
286
|
|
|
287
|
+
export interface MeridianCatalogLoad {
|
|
288
|
+
/** Providers whose base URL still looks like Meridian, reachable or not. */
|
|
289
|
+
readonly configured: string[]
|
|
290
|
+
readonly discovered: MeridianProviderModels[]
|
|
291
|
+
}
|
|
292
|
+
|
|
263
293
|
export async function loadMeridianModels(
|
|
264
294
|
catalog: MeridianCatalogClient,
|
|
265
295
|
signal: AbortSignal,
|
|
266
296
|
fetcher: ModelFetcher = globalThis.fetch,
|
|
267
|
-
): Promise<
|
|
297
|
+
): Promise<MeridianCatalogLoad> {
|
|
268
298
|
const deadline = Date.now() + MODEL_DISCOVERY_TIMEOUT_MS
|
|
269
299
|
while (!signal.aborted) {
|
|
270
300
|
const providers = await Promise.all([...MERIDIAN_PROVIDERS].map(async (providerID) => {
|
|
@@ -281,14 +311,176 @@ export async function loadMeridianModels(
|
|
|
281
311
|
if (configured.length > 0) {
|
|
282
312
|
const discovered = await Promise.all(configured.map(async ({ providerID, baseURL }) => {
|
|
283
313
|
const models = await fetchMeridianModels(baseURL, signal, fetcher)
|
|
284
|
-
return models ? { providerID, models } : undefined
|
|
314
|
+
return models ? { providerID, baseURL: typeof baseURL === "string" ? baseURL : undefined, models } : undefined
|
|
285
315
|
}))
|
|
286
|
-
return
|
|
316
|
+
return {
|
|
317
|
+
configured: configured.map(provider => provider.providerID),
|
|
318
|
+
discovered: discovered.flatMap(result => result ? [result] : []),
|
|
319
|
+
}
|
|
287
320
|
}
|
|
288
|
-
if (Date.now() >= deadline) return []
|
|
321
|
+
if (Date.now() >= deadline) return { configured: [], discovered: [] }
|
|
289
322
|
await new Promise<void>((resolve) => setTimeout(resolve, PROVIDER_READY_POLL_MS))
|
|
290
323
|
}
|
|
291
|
-
return []
|
|
324
|
+
return { configured: [], discovered: [] }
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export interface CachedProviderCatalog {
|
|
328
|
+
readonly baseURL: string
|
|
329
|
+
readonly models: readonly MeridianModel[]
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export function catalogCachePath(): string {
|
|
333
|
+
return join(meridianConfigDirectory(), CATALOG_CACHE_FILE)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Validate a cache document as strictly as a discovery response.
|
|
338
|
+
*
|
|
339
|
+
* A corrupt or hand-edited file must seed nothing rather than write junk into
|
|
340
|
+
* the catalog, so every failure returns an empty map.
|
|
341
|
+
*/
|
|
342
|
+
export function parseCatalogCache(value: unknown, now: number): Map<string, CachedProviderCatalog> {
|
|
343
|
+
const entries = new Map<string, CachedProviderCatalog>()
|
|
344
|
+
if (!isRecord(value) || value.version !== CATALOG_CACHE_VERSION) return entries
|
|
345
|
+
if (!isRecord(value.providers)) return entries
|
|
346
|
+
for (const [providerID, entry] of Object.entries(value.providers)) {
|
|
347
|
+
if (!MERIDIAN_PROVIDERS.has(providerID) || !isRecord(entry)) continue
|
|
348
|
+
const { baseURL, fetchedAt } = entry
|
|
349
|
+
if (typeof baseURL !== "string" || meridianModelsURL(baseURL) === undefined) continue
|
|
350
|
+
if (typeof fetchedAt !== "number" || !Number.isSafeInteger(fetchedAt) || fetchedAt <= 0) continue
|
|
351
|
+
if (fetchedAt > now || now - fetchedAt > CATALOG_CACHE_TTL_MS) continue
|
|
352
|
+
const models = parseCachedModels(entry.models)
|
|
353
|
+
if (!models || models.length === 0) continue
|
|
354
|
+
entries.set(providerID, { baseURL, models })
|
|
355
|
+
}
|
|
356
|
+
return entries
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function parseCachedModels(value: unknown): MeridianModel[] | undefined {
|
|
360
|
+
if (!Array.isArray(value)) return undefined
|
|
361
|
+
const models: MeridianModel[] = []
|
|
362
|
+
const ids = new Set<string>()
|
|
363
|
+
for (const item of value) {
|
|
364
|
+
if (!isRecord(item)) return undefined
|
|
365
|
+
const { id, name, contextWindow, efforts } = item
|
|
366
|
+
if (
|
|
367
|
+
typeof id !== "string"
|
|
368
|
+
|| id.length === 0
|
|
369
|
+
|| id.length > 256
|
|
370
|
+
|| /[^\x21-\x7E]/.test(id)
|
|
371
|
+
|| ids.has(id)
|
|
372
|
+
|| typeof name !== "string"
|
|
373
|
+
|| name.trim().length === 0
|
|
374
|
+
|| name.length > 256
|
|
375
|
+
|| typeof contextWindow !== "number"
|
|
376
|
+
|| !Number.isSafeInteger(contextWindow)
|
|
377
|
+
|| contextWindow <= 0
|
|
378
|
+
|| !Array.isArray(efforts)
|
|
379
|
+
|| efforts.some(effort => typeof effort !== "string" || !MERIDIAN_EFFORTS.includes(effort as never))
|
|
380
|
+
) {
|
|
381
|
+
return undefined
|
|
382
|
+
}
|
|
383
|
+
ids.add(id)
|
|
384
|
+
models.push({ id, name, contextWindow, efforts: efforts as string[] })
|
|
385
|
+
}
|
|
386
|
+
return models
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export function serializeCatalogCache(
|
|
390
|
+
discovered: readonly MeridianProviderModels[],
|
|
391
|
+
now: number,
|
|
392
|
+
): string {
|
|
393
|
+
const providers: Record<string, unknown> = {}
|
|
394
|
+
for (const entry of discovered) {
|
|
395
|
+
if (typeof entry.baseURL !== "string" || entry.models.length === 0) continue
|
|
396
|
+
providers[entry.providerID] = { baseURL: entry.baseURL, fetchedAt: now, models: entry.models }
|
|
397
|
+
}
|
|
398
|
+
return `${JSON.stringify({ version: CATALOG_CACHE_VERSION, providers }, null, 2)}\n`
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Drop the seed. Called when no Meridian-shaped provider is configured any
|
|
403
|
+
* more, so a later cold start cannot describe a provider this catalog no longer
|
|
404
|
+
* points at. Best effort, like the write.
|
|
405
|
+
*/
|
|
406
|
+
export function removeCatalogCache(remove: (path: string) => void = path => rmSync(path, { force: true })): void {
|
|
407
|
+
try {
|
|
408
|
+
remove(catalogCachePath())
|
|
409
|
+
} catch {
|
|
410
|
+
// Ignored on purpose: a stale seed is corrected by the next discovery.
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** Read the seed. Any failure — missing, unreadable, corrupt — seeds nothing. */
|
|
415
|
+
export function readCatalogCache(
|
|
416
|
+
now: number,
|
|
417
|
+
read: (path: string) => string = path => readFileSync(path, "utf-8"),
|
|
418
|
+
): Map<string, CachedProviderCatalog> {
|
|
419
|
+
try {
|
|
420
|
+
return parseCatalogCache(JSON.parse(read(catalogCachePath())), now)
|
|
421
|
+
} catch {
|
|
422
|
+
return new Map()
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Persist the seed for the next cold start. Best effort: a cache we cannot write
|
|
428
|
+
* costs a deferred catalog on the next start, which is the old behaviour, so it
|
|
429
|
+
* must never interrupt a working session.
|
|
430
|
+
*/
|
|
431
|
+
export function writeCatalogCache(
|
|
432
|
+
discovered: readonly MeridianProviderModels[],
|
|
433
|
+
now: number,
|
|
434
|
+
write: (path: string, contents: string) => void = (path, contents) => {
|
|
435
|
+
// Rename onto the target so a concurrent reader never sees a partial file.
|
|
436
|
+
const temporary = `${path}.${process.pid}.tmp`
|
|
437
|
+
writeFileSync(temporary, contents, { encoding: "utf-8", mode: 0o600 })
|
|
438
|
+
renameSync(temporary, path)
|
|
439
|
+
},
|
|
440
|
+
): void {
|
|
441
|
+
try {
|
|
442
|
+
const contents = serializeCatalogCache(discovered, now)
|
|
443
|
+
if (contents.includes('"providers": {}')) return
|
|
444
|
+
write(catalogCachePath(), contents)
|
|
445
|
+
} catch {
|
|
446
|
+
// Ignored on purpose: see above.
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* The only thing choosing a seed needs from the draft: whether a provider is
|
|
452
|
+
* still in the catalog. Narrower than `CatalogDraft` on purpose, so the decision
|
|
453
|
+
* stays unit-testable without standing up a whole branded provider record.
|
|
454
|
+
*/
|
|
455
|
+
export interface CatalogProviderProbe {
|
|
456
|
+
readonly provider: { get(providerID: string): unknown }
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Choose what the transform should write: live discovery when it has landed,
|
|
461
|
+
* otherwise the cached seed.
|
|
462
|
+
*
|
|
463
|
+
* The seed cannot be validated against the provider's configured URL here. A
|
|
464
|
+
* draft `Provider.Info` exposes only `id`, `name`, `activation`, `package`,
|
|
465
|
+
* `integrationID` and `headers` — verified on beta-18866, where the whole
|
|
466
|
+
* record contains no URL at all. So the seed is applied optimistically to any
|
|
467
|
+
* Meridian provider that still exists in the catalog, and `discoverModels`
|
|
468
|
+
* corrects or drops it once it can read the real URL.
|
|
469
|
+
*/
|
|
470
|
+
export function resolveCatalogModels(
|
|
471
|
+
catalog: CatalogProviderProbe,
|
|
472
|
+
discovered: readonly MeridianProviderModels[],
|
|
473
|
+
cached: ReadonlyMap<string, CachedProviderCatalog>,
|
|
474
|
+
): MeridianProviderModels[] {
|
|
475
|
+
if (discovered.length > 0) return [...discovered]
|
|
476
|
+
const seeded: MeridianProviderModels[] = []
|
|
477
|
+
for (const providerID of MERIDIAN_PROVIDERS) {
|
|
478
|
+
const entry = cached.get(providerID)
|
|
479
|
+
if (!entry) continue
|
|
480
|
+
if (!catalog.provider.get(providerID)) continue
|
|
481
|
+
seeded.push({ providerID, baseURL: entry.baseURL, models: [...entry.models] })
|
|
482
|
+
}
|
|
483
|
+
return seeded
|
|
292
484
|
}
|
|
293
485
|
|
|
294
486
|
/**
|
|
@@ -391,19 +583,41 @@ const MeridianV2Plugin = Plugin.define({
|
|
|
391
583
|
const modelDiscoveryController = new AbortController()
|
|
392
584
|
let discoveredModels: MeridianProviderModels[] = []
|
|
393
585
|
let discoveryStarted = false
|
|
586
|
+
// Read synchronously, before the first transform can run. Awaiting the
|
|
587
|
+
// catalog here would deadlock the server, which is why the seed is a file
|
|
588
|
+
// and not a catalog read (#1008).
|
|
589
|
+
const cachedModels = readCatalogCache(Date.now())
|
|
394
590
|
|
|
395
591
|
registered.push(await context.catalog.transform((catalog) => {
|
|
396
|
-
for (const
|
|
397
|
-
applyMeridianModels(catalog,
|
|
592
|
+
for (const entry of resolveCatalogModels(catalog, discoveredModels, cachedModels)) {
|
|
593
|
+
applyMeridianModels(catalog, entry.providerID, entry.models)
|
|
398
594
|
}
|
|
399
595
|
}))
|
|
400
596
|
|
|
401
597
|
const discoverModels = async () => {
|
|
402
598
|
if (discoveryStarted || modelDiscoveryController.signal.aborted) return false
|
|
403
|
-
const
|
|
404
|
-
|
|
599
|
+
const loaded = await loadMeridianModels(context.catalog, modelDiscoveryController.signal)
|
|
600
|
+
.catch((): MeridianCatalogLoad => ({ configured: [], discovered: [] }))
|
|
601
|
+
if (modelDiscoveryController.signal.aborted) return false
|
|
602
|
+
if (loaded.configured.length === 0) {
|
|
603
|
+
// Nothing here points at Meridian any more — the user repointed the
|
|
604
|
+
// provider. A seed from a previous run would describe a different
|
|
605
|
+
// endpoint, so drop it and rebuild the catalog without it.
|
|
606
|
+
if (cachedModels.size > 0) {
|
|
607
|
+
cachedModels.clear()
|
|
608
|
+
removeCatalogCache()
|
|
609
|
+
await context.catalog.reload()
|
|
610
|
+
}
|
|
611
|
+
return false
|
|
612
|
+
}
|
|
613
|
+
// Configured but unreachable: keep the seed. It is the last thing Meridian
|
|
614
|
+
// actually served, which beats OpenCode's models.dev entries.
|
|
615
|
+
if (loaded.discovered.length === 0) return false
|
|
405
616
|
discoveryStarted = true
|
|
406
|
-
discoveredModels =
|
|
617
|
+
discoveredModels = loaded.discovered
|
|
618
|
+
// Seed the next cold start before the reload, so a crash mid-reload still
|
|
619
|
+
// leaves the catalog available to the following process.
|
|
620
|
+
writeCatalogCache(loaded.discovered, Date.now())
|
|
407
621
|
await context.catalog.reload()
|
|
408
622
|
return true
|
|
409
623
|
}
|
|
@@ -32,10 +32,20 @@ export interface PriorityAttestationSignInput {
|
|
|
32
32
|
readonly issuedAt: number
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Meridian's own configuration directory, as the plugin sees it.
|
|
37
|
+
*
|
|
38
|
+
* Exported because the V2 plugin also keeps its catalog cache here: the plugin
|
|
39
|
+
* tree must not import from src/, so this is the one place the resolution lives.
|
|
40
|
+
*/
|
|
41
|
+
export function meridianConfigDirectory(): string {
|
|
36
42
|
return process.env.MERIDIAN_CONFIG_DIR ?? join(homedir(), ".config", "meridian")
|
|
37
43
|
}
|
|
38
44
|
|
|
45
|
+
function configDirectory(): string {
|
|
46
|
+
return meridianConfigDirectory()
|
|
47
|
+
}
|
|
48
|
+
|
|
39
49
|
export function priorityAttestationKeyPath(): string {
|
|
40
50
|
return join(configDirectory(), PRIORITY_ATTESTATION_KEY_FILE)
|
|
41
51
|
}
|