newmark-agent 0.5.4 → 0.5.7
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/conversation-utility-host.bundle.cjs +71 -26
- package/dist/core/agent.js +48 -24
- package/dist/core/agentKernelRunner.js +12 -1
- package/dist/core/autoRouter.d.ts +1 -0
- package/dist/core/autoRouter.js +9 -0
- package/dist/core/conversationKernel.d.ts +12 -0
- package/dist/core/conversationKernel.js +32 -1
- package/dist/core/electronUtilityRuntimePool.js +7 -2
- package/dist/core/types.d.ts +21 -0
- package/dist/core/wslAgentRuntimePool.js +6 -2
- package/dist/main.js +34 -0
- package/dist/preload.js +1 -0
- package/dist/providers/provider-events.d.ts +6 -0
- package/dist/providers/provider-events.js +15 -4
- package/dist/server.js +21 -0
- package/dist/ui/index.html +2345 -197
- package/dist/ui/lucide-sprite.svg +4 -0
- package/dist/wsl-agent-host.bundle.cjs +71 -26
- package/package.json +8 -3
|
@@ -328670,7 +328670,7 @@ function providerStreamTimeoutError(timeoutMs) {
|
|
|
328670
328670
|
error.message = `Stream read timeout after ${timeoutMs}ms`;
|
|
328671
328671
|
return error;
|
|
328672
328672
|
}
|
|
328673
|
-
async function readProviderStreamChunk(reader, signal, timeoutMs =
|
|
328673
|
+
async function readProviderStreamChunk(reader, signal, timeoutMs = 0) {
|
|
328674
328674
|
if (signal.aborted) throw providerAbortError(signal);
|
|
328675
328675
|
let timer;
|
|
328676
328676
|
let onAbort;
|
|
@@ -328678,9 +328678,9 @@ async function readProviderStreamChunk(reader, signal, timeoutMs = 3e4) {
|
|
|
328678
328678
|
onAbort = () => reject(providerAbortError(signal));
|
|
328679
328679
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
328680
328680
|
});
|
|
328681
|
-
const timeoutPromise = new Promise((_3, reject) => {
|
|
328681
|
+
const timeoutPromise = timeoutMs > 0 ? new Promise((_3, reject) => {
|
|
328682
328682
|
timer = setTimeout(() => reject(providerStreamTimeoutError(timeoutMs)), timeoutMs);
|
|
328683
|
-
});
|
|
328683
|
+
}) : new Promise(() => void 0);
|
|
328684
328684
|
try {
|
|
328685
328685
|
return await Promise.race([reader.read(), abortPromise, timeoutPromise]);
|
|
328686
328686
|
} catch (error) {
|
|
@@ -340857,7 +340857,13 @@ async function runAgentKernel(agent) {
|
|
|
340857
340857
|
agent.recordWorkStatus("Final visual fallback used: local mini OCR plus conservative text correction.");
|
|
340858
340858
|
}
|
|
340859
340859
|
if (modelBeforeKernelRun && modelBeforeKernelRun !== agent.model && !tokens.some((t3) => t3.text?.includes("[Model fallback]"))) {
|
|
340860
|
-
|
|
340860
|
+
const notice = `[Model fallback] ${modelBeforeKernelRun} unavailable; switched to ${agent.model}.`;
|
|
340861
|
+
tokens.unshift({ type: "text", text: notice });
|
|
340862
|
+
agent.emitWorkEvent({
|
|
340863
|
+
type: "status",
|
|
340864
|
+
content: notice,
|
|
340865
|
+
fallback: { from: modelBeforeKernelRun, to: agent.model, providerId: agent.activeDeployment()?.providerId }
|
|
340866
|
+
});
|
|
340861
340867
|
}
|
|
340862
340868
|
let emptyResponseRetries = 0;
|
|
340863
340869
|
while (providerTurnIsEmpty(lastTurn) && emptyResponseRetries < 2) {
|
|
@@ -340878,6 +340884,11 @@ async function runAgentKernel(agent) {
|
|
|
340878
340884
|
const notice = routeTransitionNotice(agent, previous);
|
|
340879
340885
|
tokens.push({ type: "text", text: notice });
|
|
340880
340886
|
agent.recordWorkStatus(notice);
|
|
340887
|
+
agent.emitWorkEvent({
|
|
340888
|
+
type: "status",
|
|
340889
|
+
content: notice,
|
|
340890
|
+
fallback: { from: previous, to: agent.model, providerId: agent.activeDeployment()?.providerId }
|
|
340891
|
+
});
|
|
340881
340892
|
kernel2.state.model = toKernelModel(agent);
|
|
340882
340893
|
const fallbackToolSurface = refreshToolSurface(true);
|
|
340883
340894
|
kernel2.state.systemPrompt = [agent.buildSystemPrompt(), fallbackToolSurface.systemPromptNotice].filter(Boolean).join("\n\n");
|
|
@@ -343302,14 +343313,16 @@ var AutoRouter = class {
|
|
|
343302
343313
|
retryDelayMs
|
|
343303
343314
|
});
|
|
343304
343315
|
}
|
|
343316
|
+
if (failure.allowModelFallback === false) return attempts.slice(0, remainingAttempts);
|
|
343305
343317
|
const selection = decision.requestedSelection;
|
|
343306
343318
|
if (selection.kind === "fixed") return attempts.slice(0, remainingAttempts);
|
|
343307
343319
|
const scope = selection.kind === "auto" ? selection.scope : { kind: "provider", providerId: current.providerId };
|
|
343308
343320
|
const subset = selection.kind === "auto" ? selection.subset : void 0;
|
|
343309
343321
|
const currentGroup = current.logicalModelGroupId;
|
|
343322
|
+
const currentProviderId = current.providerId;
|
|
343310
343323
|
const now2 = this.now();
|
|
343311
343324
|
const attemptedDeployments = decision.attempts.map((attempt) => attempt.deployment);
|
|
343312
|
-
const eligible = candidates.filter((candidate) => candidate.enabled && inScope(candidate.deployment, scope) && inSubset(candidate.deployment, subset) && !sameDeployment(candidate.deployment, current) && !attemptedDeployments.some((attempted) => sameDeployment(candidate.deployment, attempted)) && validationEligible(candidate, now2).length === 0 && this.passedInitialHardFilters(decision, candidate) && this.circuitState(candidate.deployment, now2, false) !== "open");
|
|
343325
|
+
const eligible = candidates.filter((candidate) => candidate.enabled && candidate.deployment.providerId === currentProviderId && inScope(candidate.deployment, scope) && inSubset(candidate.deployment, subset) && !sameDeployment(candidate.deployment, current) && !attemptedDeployments.some((attempted) => sameDeployment(candidate.deployment, attempted)) && validationEligible(candidate, now2).length === 0 && this.passedInitialHardFilters(decision, candidate) && this.circuitState(candidate.deployment, now2, false) !== "open");
|
|
343313
343326
|
const equivalent = currentGroup ? eligible.find((candidate) => candidate.deployment.logicalModelGroupId === currentGroup && !candidate.fallbackOnly) : void 0;
|
|
343314
343327
|
const fallback = eligible.find((candidate) => candidate.fallbackOnly);
|
|
343315
343328
|
const rankedAlternates = decision.rankedCandidates.map((ranked) => eligible.find((candidate) => sameDeployment(candidate.deployment, ranked.deployment))).filter((candidate) => !!candidate && candidate !== equivalent && candidate !== fallback && !candidate.fallbackOnly);
|
|
@@ -346747,7 +346760,8 @@ ${String(event.toolArgs || "")}`;
|
|
|
346747
346760
|
sequence,
|
|
346748
346761
|
status: input.status,
|
|
346749
346762
|
guide: !isToolEvent && input.guide ? this.normalizeGuideReceipt(input.guide) : void 0,
|
|
346750
|
-
displayImage: isToolEvent ? this.hydrateDisplayImage(input.displayImage) : void 0
|
|
346763
|
+
displayImage: isToolEvent ? this.hydrateDisplayImage(input.displayImage) : void 0,
|
|
346764
|
+
fallback: input.fallback
|
|
346751
346765
|
};
|
|
346752
346766
|
if (activeRun && this.isPersistablePublicWorkEvent(event)) {
|
|
346753
346767
|
activeRun.sequence = Number(sequence || activeRun.sequence + 1);
|
|
@@ -349666,9 +349680,15 @@ ${summary}`, segment, "local-summarize", true);
|
|
|
349666
349680
|
}
|
|
349667
349681
|
compressionBuildBlockStart(messages) {
|
|
349668
349682
|
const activeRunId = this.currentWorkRunId();
|
|
349669
|
-
if (
|
|
349670
|
-
|
|
349671
|
-
|
|
349683
|
+
if (activeRunId) {
|
|
349684
|
+
const index = messages.findIndex((message) => String(message.run_id || message.runId || "") === activeRunId);
|
|
349685
|
+
if (index >= 0) return index;
|
|
349686
|
+
}
|
|
349687
|
+
let lastRunBoundary = -1;
|
|
349688
|
+
for (let index = 0; index < messages.length; index += 1) {
|
|
349689
|
+
if (String(messages[index]?.run_id || messages[index]?.runId || "")) lastRunBoundary = index + 1;
|
|
349690
|
+
}
|
|
349691
|
+
return lastRunBoundary >= 0 ? lastRunBoundary : messages.length;
|
|
349672
349692
|
}
|
|
349673
349693
|
recentContextSuffix(messages, maxMessages, tokenBudget) {
|
|
349674
349694
|
if (!messages.length) return [];
|
|
@@ -350597,17 +350617,12 @@ ${msg.content}
|
|
|
350597
350617
|
currentAttempt.sideEffectBoundary = this.routeSideEffectCommitted;
|
|
350598
350618
|
if (this.routeAttemptStartedAt > 0) currentAttempt.durationMs = Math.max(0, Date.now() - this.routeAttemptStartedAt);
|
|
350599
350619
|
}
|
|
350600
|
-
if (!fallbackEnabled) {
|
|
350601
|
-
this.lastRouteDecision.finalStatus = "failed";
|
|
350602
|
-
this.routeAttemptStartedAt = 0;
|
|
350603
|
-
this.persistRouteDecision(this.lastRouteDecision);
|
|
350604
|
-
return null;
|
|
350605
|
-
}
|
|
350606
350620
|
if (!this.pendingAutoAttempts.length) {
|
|
350607
350621
|
this.pendingAutoAttempts = this.autoRouter.planAttempts(this.lastRouteDecision, this.autoRouteCandidates(), {
|
|
350608
350622
|
error: failure,
|
|
350609
350623
|
streamCommitted: this.routeStreamCommitted,
|
|
350610
|
-
sideEffectCommitted: this.routeSideEffectCommitted
|
|
350624
|
+
sideEffectCommitted: this.routeSideEffectCommitted,
|
|
350625
|
+
allowModelFallback: fallbackEnabled
|
|
350611
350626
|
});
|
|
350612
350627
|
}
|
|
350613
350628
|
const next2 = this.pendingAutoAttempts.shift();
|
|
@@ -350642,7 +350657,6 @@ ${msg.content}
|
|
|
350642
350657
|
if (!next?.name) return null;
|
|
350643
350658
|
this.model = next.name;
|
|
350644
350659
|
this.fixedDeployment = this.deploymentRef(next);
|
|
350645
|
-
if (next.provider) this.config.set("models", "auto_switch_anchor_provider", next.provider);
|
|
350646
350660
|
return current;
|
|
350647
350661
|
}
|
|
350648
350662
|
isLlmErrorText(text) {
|
|
@@ -350650,12 +350664,10 @@ ${msg.content}
|
|
|
350650
350664
|
}
|
|
350651
350665
|
scopedSwitchModels(currentModelName) {
|
|
350652
350666
|
const all = this.config.allModels();
|
|
350653
|
-
|
|
350654
|
-
const
|
|
350655
|
-
|
|
350656
|
-
|
|
350657
|
-
const provider = this.config.findProvider(providerId);
|
|
350658
|
-
return all.filter((m2) => m2.provider_id === (provider?.id || providerId));
|
|
350667
|
+
const current = currentModelName === "auto" ? this.activeModelConfig() : this.config.findModel(currentModelName);
|
|
350668
|
+
const providerId = current?.provider_id || this.fixedDeployment?.providerId || (currentModelName !== "auto" ? this.activeDeployment()?.providerId : void 0);
|
|
350669
|
+
if (!providerId) return [];
|
|
350670
|
+
return all.filter((m2) => m2.provider_id === providerId);
|
|
350659
350671
|
}
|
|
350660
350672
|
async validateModels(selectedNames, options = {}) {
|
|
350661
350673
|
if (this.modelValidationPromise) return this.modelValidationPromise;
|
|
@@ -352037,7 +352049,10 @@ ${items.map((item) => this.formatAutomation(item)).join("\n")}`;
|
|
|
352037
352049
|
if (!sa) return "[Subagent] Not found.";
|
|
352038
352050
|
const requestedModel = this.normalizeSubagentModelSelection(sa.model && sa.model !== "default" ? sa.model : this.modelSelectionValue() || this.model);
|
|
352039
352051
|
const requestedDeployment = parseDeploymentSelectionValue2(requestedModel);
|
|
352040
|
-
|
|
352052
|
+
let assignedModel = requestedModel === "auto" ? this.activeModelConfig() : requestedDeployment ? this.config.findDeployment(requestedDeployment) : this.config.findModel(requestedModel);
|
|
352053
|
+
if (!assignedModel && !requestedDeployment && requestedModel !== "auto") {
|
|
352054
|
+
assignedModel = this.activeModelConfig();
|
|
352055
|
+
}
|
|
352041
352056
|
const model = assignedModel?.name || (requestedModel === "auto" ? this.activeModelName() : requestedModel);
|
|
352042
352057
|
const activeModel = this.activeModelConfig();
|
|
352043
352058
|
const activeProvider = this.engineModel();
|
|
@@ -353466,6 +353481,32 @@ var ConversationKernel = class {
|
|
|
353466
353481
|
followUp: queued?.followUp.slice() || []
|
|
353467
353482
|
};
|
|
353468
353483
|
}
|
|
353484
|
+
/**
|
|
353485
|
+
* dev-0.5.6: 排队 followUp 消息 drain 时转成普通用户消息载荷。
|
|
353486
|
+
*
|
|
353487
|
+
* 入队时(enqueueSameSession)为去重/编辑保留了 clientMessageId,并把
|
|
353488
|
+
* runId 固定为入队时运行的 runId;若原样传给 process,写入 chatMessages
|
|
353489
|
+
* 的用户消息会携带旧 runId + clientMessageId,渲染端(PC renderTranscript /
|
|
353490
|
+
* 移动端 projectRemoteConversationItems)会把「clientMessageId + runId 命中
|
|
353491
|
+
* workRuns」的消息误判为 guide 消息而跳过气泡,导致排队自动发送的消息不
|
|
353492
|
+
* 显示在用户输入时间线。清除这两个字段后 process 走普通用户消息分支
|
|
353493
|
+
* (chatMessages.push,mode 取 visibleMode,runId 取当前 run)。
|
|
353494
|
+
*/
|
|
353495
|
+
drainQueuedFollowUpMessage(message) {
|
|
353496
|
+
if (typeof message === "string") return message;
|
|
353497
|
+
const text = String(message.text || "");
|
|
353498
|
+
const images = message.images;
|
|
353499
|
+
const attachments = message.attachments;
|
|
353500
|
+
const visible = message.visibleUserInput;
|
|
353501
|
+
const visibleMode = message.visibleMode;
|
|
353502
|
+
return {
|
|
353503
|
+
text,
|
|
353504
|
+
...images?.length ? { images } : {},
|
|
353505
|
+
...attachments?.length ? { attachments } : {},
|
|
353506
|
+
...visible ? { visibleUserInput: visible } : {},
|
|
353507
|
+
...visibleMode ? { visibleMode } : {}
|
|
353508
|
+
};
|
|
353509
|
+
}
|
|
353469
353510
|
queueItems(target) {
|
|
353470
353511
|
const runtime = this.findRuntime(target);
|
|
353471
353512
|
if (!runtime) return [];
|
|
@@ -354207,7 +354248,7 @@ ${batchText}`,
|
|
|
354207
354248
|
};
|
|
354208
354249
|
lastTokens = await this.runSingle(runtime, batchMessage, "steer");
|
|
354209
354250
|
} else {
|
|
354210
|
-
lastTokens = await this.runSingle(runtime, next.message, next.queueMode);
|
|
354251
|
+
lastTokens = await this.runSingle(runtime, this.drainQueuedFollowUpMessage(next.message), next.queueMode);
|
|
354211
354252
|
}
|
|
354212
354253
|
}
|
|
354213
354254
|
const rootMessage = runtime.runner.subagents.readRootInbox()[0];
|
|
@@ -354666,7 +354707,11 @@ ${text}`;
|
|
|
354666
354707
|
type: "queue_update",
|
|
354667
354708
|
content: "Conversation queue updated.",
|
|
354668
354709
|
conversationId: runtime.id,
|
|
354669
|
-
queue: this.queued(runtime.target)
|
|
354710
|
+
queue: this.queued(runtime.target),
|
|
354711
|
+
// Structured rows with stable kernel ids so every consumer (PC UI and
|
|
354712
|
+
// the paired mobile client) can render/edit/delete the same items.
|
|
354713
|
+
queueItems: this.queueItems(runtime.target),
|
|
354714
|
+
queuePaused: runtime.queuePaused === true
|
|
354670
354715
|
});
|
|
354671
354716
|
}
|
|
354672
354717
|
clearQueued(runtime) {
|
package/dist/core/agent.js
CHANGED
|
@@ -2298,6 +2298,7 @@ class Agent {
|
|
|
2298
2298
|
status: input.status,
|
|
2299
2299
|
guide: !isToolEvent && input.guide ? this.normalizeGuideReceipt(input.guide) : undefined,
|
|
2300
2300
|
displayImage: isToolEvent ? this.hydrateDisplayImage(input.displayImage) : undefined,
|
|
2301
|
+
fallback: input.fallback,
|
|
2301
2302
|
};
|
|
2302
2303
|
if (activeRun && this.isPersistablePublicWorkEvent(event)) {
|
|
2303
2304
|
activeRun.sequence = Number(sequence || activeRun.sequence + 1);
|
|
@@ -5587,10 +5588,22 @@ class Agent {
|
|
|
5587
5588
|
}
|
|
5588
5589
|
compressionBuildBlockStart(messages) {
|
|
5589
5590
|
const activeRunId = this.currentWorkRunId();
|
|
5590
|
-
if (
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5591
|
+
if (activeRunId) {
|
|
5592
|
+
const index = messages.findIndex(message => String(message.run_id || message.runId || '') === activeRunId);
|
|
5593
|
+
if (index >= 0)
|
|
5594
|
+
return index;
|
|
5595
|
+
}
|
|
5596
|
+
// dev-0.5.6: 空闲对话(无活动 run)时不得把全部历史算进当前 Build Block,
|
|
5597
|
+
// 否则上下文显示窗口的长期历史恒为 0。回退语义:
|
|
5598
|
+
// - 存在带 run_id 的消息:boundary 取最后一个 run 的起点之后(该 run 及其
|
|
5599
|
+
// 之前的历史属于长期历史,其后无归属的消息属于当前未命名区块);
|
|
5600
|
+
// - 完全没有 run_id:全部历史都是长期历史(不存在当前 Build Block)。
|
|
5601
|
+
let lastRunBoundary = -1;
|
|
5602
|
+
for (let index = 0; index < messages.length; index += 1) {
|
|
5603
|
+
if (String(messages[index]?.run_id || messages[index]?.runId || ''))
|
|
5604
|
+
lastRunBoundary = index + 1;
|
|
5605
|
+
}
|
|
5606
|
+
return lastRunBoundary >= 0 ? lastRunBoundary : messages.length;
|
|
5594
5607
|
}
|
|
5595
5608
|
recentContextSuffix(messages, maxMessages, tokenBudget) {
|
|
5596
5609
|
if (!messages.length)
|
|
@@ -6651,17 +6664,12 @@ class Agent {
|
|
|
6651
6664
|
if (this.routeAttemptStartedAt > 0)
|
|
6652
6665
|
currentAttempt.durationMs = Math.max(0, Date.now() - this.routeAttemptStartedAt);
|
|
6653
6666
|
}
|
|
6654
|
-
if (!fallbackEnabled) {
|
|
6655
|
-
this.lastRouteDecision.finalStatus = 'failed';
|
|
6656
|
-
this.routeAttemptStartedAt = 0;
|
|
6657
|
-
this.persistRouteDecision(this.lastRouteDecision);
|
|
6658
|
-
return null;
|
|
6659
|
-
}
|
|
6660
6667
|
if (!this.pendingAutoAttempts.length) {
|
|
6661
6668
|
this.pendingAutoAttempts = this.autoRouter.planAttempts(this.lastRouteDecision, this.autoRouteCandidates(), {
|
|
6662
6669
|
error: failure,
|
|
6663
6670
|
streamCommitted: this.routeStreamCommitted,
|
|
6664
6671
|
sideEffectCommitted: this.routeSideEffectCommitted,
|
|
6672
|
+
allowModelFallback: fallbackEnabled,
|
|
6665
6673
|
});
|
|
6666
6674
|
}
|
|
6667
6675
|
const next = this.pendingAutoAttempts.shift();
|
|
@@ -6702,8 +6710,11 @@ class Agent {
|
|
|
6702
6710
|
return null;
|
|
6703
6711
|
this.model = next.name;
|
|
6704
6712
|
this.fixedDeployment = this.deploymentRef(next);
|
|
6705
|
-
|
|
6706
|
-
|
|
6713
|
+
// Do NOT rewrite auto_switch_anchor_provider here. The anchor describes
|
|
6714
|
+
// the user's Auto-routing scope; a fixed-model recovery must never
|
|
6715
|
+
// repoint it (a single transient failure would otherwise silently move
|
|
6716
|
+
// every future Auto route — and every future fallback pool — to the
|
|
6717
|
+
// recovery provider, which is the cross-provider leak users observed).
|
|
6707
6718
|
return current;
|
|
6708
6719
|
}
|
|
6709
6720
|
isLlmErrorText(text) {
|
|
@@ -6711,18 +6722,23 @@ class Agent {
|
|
|
6711
6722
|
}
|
|
6712
6723
|
scopedSwitchModels(currentModelName) {
|
|
6713
6724
|
const all = this.config.allModels();
|
|
6714
|
-
|
|
6715
|
-
|
|
6716
|
-
|
|
6717
|
-
|
|
6718
|
-
|
|
6719
|
-
|
|
6720
|
-
|
|
6721
|
-
|
|
6725
|
+
// Fixed-model recovery is always provider-local. A same-named model on a
|
|
6726
|
+
// different provider is a different deployment and must never receive the
|
|
6727
|
+
// failed provider's credentials or become an implicit fallback.
|
|
6728
|
+
// Resolution order is strictly deployment-first: the pinned deployment,
|
|
6729
|
+
// then the exact active deployment. The global auto anchor is deliberately
|
|
6730
|
+
// NOT consulted here — it belongs to Auto routing only and can point at a
|
|
6731
|
+
// different provider (for example after a previous cross-provider switch),
|
|
6732
|
+
// which is exactly the same-name cross-provider leak this guard exists to
|
|
6733
|
+
// prevent. When no provider can be established unambiguously, return an
|
|
6734
|
+
// empty pool: failing the switch is safe, crossing providers is not.
|
|
6735
|
+
const current = currentModelName === 'auto' ? this.activeModelConfig() : this.config.findModel(currentModelName);
|
|
6736
|
+
const providerId = current?.provider_id
|
|
6737
|
+
|| this.fixedDeployment?.providerId
|
|
6738
|
+
|| (currentModelName !== 'auto' ? this.activeDeployment()?.providerId : undefined);
|
|
6722
6739
|
if (!providerId)
|
|
6723
|
-
return
|
|
6724
|
-
|
|
6725
|
-
return all.filter(m => m.provider_id === (provider?.id || providerId));
|
|
6740
|
+
return [];
|
|
6741
|
+
return all.filter(m => m.provider_id === providerId);
|
|
6726
6742
|
}
|
|
6727
6743
|
async validateModels(selectedNames, options = {}) {
|
|
6728
6744
|
if (this.modelValidationPromise)
|
|
@@ -8224,9 +8240,17 @@ class Agent {
|
|
|
8224
8240
|
return '[Subagent] Not found.';
|
|
8225
8241
|
const requestedModel = this.normalizeSubagentModelSelection(sa.model && sa.model !== 'default' ? sa.model : this.modelSelectionValue() || this.model);
|
|
8226
8242
|
const requestedDeployment = parseDeploymentSelectionValue(requestedModel);
|
|
8227
|
-
|
|
8243
|
+
let assignedModel = requestedModel === 'auto'
|
|
8228
8244
|
? this.activeModelConfig()
|
|
8229
8245
|
: (requestedDeployment ? this.config.findDeployment(requestedDeployment) : this.config.findModel(requestedModel));
|
|
8246
|
+
// Imported presets (and stale records) may reference a bare model name
|
|
8247
|
+
// that no longer exists in the configured catalog. Resolving by name must
|
|
8248
|
+
// never guess a provider; instead the peer inherits the parent deployment
|
|
8249
|
+
// so the job can still run. Qualified deployment values keep failing
|
|
8250
|
+
// closed — they are exact references, not guesses.
|
|
8251
|
+
if (!assignedModel && !requestedDeployment && requestedModel !== 'auto') {
|
|
8252
|
+
assignedModel = this.activeModelConfig();
|
|
8253
|
+
}
|
|
8230
8254
|
const model = assignedModel?.name || (requestedModel === 'auto' ? this.activeModelName() : requestedModel);
|
|
8231
8255
|
const activeModel = this.activeModelConfig();
|
|
8232
8256
|
const activeProvider = this.engineModel();
|
|
@@ -440,7 +440,13 @@ async function runAgentKernel(agent) {
|
|
|
440
440
|
agent.recordWorkStatus('Final visual fallback used: local mini OCR plus conservative text correction.');
|
|
441
441
|
}
|
|
442
442
|
if (modelBeforeKernelRun && modelBeforeKernelRun !== agent.model && !tokens.some(t => t.text?.includes('[Model fallback]'))) {
|
|
443
|
-
|
|
443
|
+
const notice = `[Model fallback] ${modelBeforeKernelRun} unavailable; switched to ${agent.model}.`;
|
|
444
|
+
tokens.unshift({ type: 'text', text: notice });
|
|
445
|
+
agent.emitWorkEvent({
|
|
446
|
+
type: 'status',
|
|
447
|
+
content: notice,
|
|
448
|
+
fallback: { from: modelBeforeKernelRun, to: agent.model, providerId: agent.activeDeployment()?.providerId },
|
|
449
|
+
});
|
|
444
450
|
}
|
|
445
451
|
let emptyResponseRetries = 0;
|
|
446
452
|
while (providerTurnIsEmpty(lastTurn) && emptyResponseRetries < 2) {
|
|
@@ -462,6 +468,11 @@ async function runAgentKernel(agent) {
|
|
|
462
468
|
const notice = routeTransitionNotice(agent, previous);
|
|
463
469
|
tokens.push({ type: 'text', text: notice });
|
|
464
470
|
agent.recordWorkStatus(notice);
|
|
471
|
+
agent.emitWorkEvent({
|
|
472
|
+
type: 'status',
|
|
473
|
+
content: notice,
|
|
474
|
+
fallback: { from: previous, to: agent.model, providerId: agent.activeDeployment()?.providerId },
|
|
475
|
+
});
|
|
465
476
|
kernel.state.model = toKernelModel(agent);
|
|
466
477
|
const fallbackToolSurface = refreshToolSurface(true);
|
|
467
478
|
kernel.state.systemPrompt = [agent.buildSystemPrompt(), fallbackToolSurface.systemPromptNotice].filter(Boolean).join('\n\n');
|
|
@@ -153,6 +153,7 @@ export declare class AutoRouter {
|
|
|
153
153
|
error: RouteFailure;
|
|
154
154
|
streamCommitted: boolean;
|
|
155
155
|
sideEffectCommitted: boolean;
|
|
156
|
+
allowModelFallback?: boolean;
|
|
156
157
|
}): PlannedRouteAttempt[];
|
|
157
158
|
recordEndpointFailure(deployment: DeploymentRef, failure: RouteFailureType | RouteFailure): void;
|
|
158
159
|
recordEndpointSuccess(deployment: DeploymentRef, latencyMs?: number, throughput?: number): void;
|
package/dist/core/autoRouter.js
CHANGED
|
@@ -290,15 +290,24 @@ class AutoRouter {
|
|
|
290
290
|
retryDelayMs,
|
|
291
291
|
});
|
|
292
292
|
}
|
|
293
|
+
if (failure.allowModelFallback === false)
|
|
294
|
+
return attempts.slice(0, remainingAttempts);
|
|
293
295
|
const selection = decision.requestedSelection;
|
|
294
296
|
if (selection.kind === 'fixed')
|
|
295
297
|
return attempts.slice(0, remainingAttempts);
|
|
296
298
|
const scope = selection.kind === 'auto' ? selection.scope : { kind: 'provider', providerId: current.providerId };
|
|
297
299
|
const subset = selection.kind === 'auto' ? selection.subset : undefined;
|
|
298
300
|
const currentGroup = current.logicalModelGroupId;
|
|
301
|
+
const currentProviderId = current.providerId;
|
|
299
302
|
const now = this.now();
|
|
300
303
|
const attemptedDeployments = decision.attempts.map(attempt => attempt.deployment);
|
|
304
|
+
// Fallback is a recovery operation, not a new global routing decision.
|
|
305
|
+
// It must never cross the provider boundary, even when the original Auto
|
|
306
|
+
// selection has global scope. This prevents equal model ids from silently
|
|
307
|
+
// switching credentials/endpoints (for example provider A/model X to
|
|
308
|
+
// provider B/model X).
|
|
301
309
|
const eligible = candidates.filter(candidate => candidate.enabled
|
|
310
|
+
&& candidate.deployment.providerId === currentProviderId
|
|
302
311
|
&& inScope(candidate.deployment, scope)
|
|
303
312
|
&& inSubset(candidate.deployment, subset)
|
|
304
313
|
&& !sameDeployment(candidate.deployment, current)
|
|
@@ -194,6 +194,18 @@ export declare class ConversationKernel {
|
|
|
194
194
|
steering: string[];
|
|
195
195
|
followUp: string[];
|
|
196
196
|
};
|
|
197
|
+
/**
|
|
198
|
+
* dev-0.5.6: 排队 followUp 消息 drain 时转成普通用户消息载荷。
|
|
199
|
+
*
|
|
200
|
+
* 入队时(enqueueSameSession)为去重/编辑保留了 clientMessageId,并把
|
|
201
|
+
* runId 固定为入队时运行的 runId;若原样传给 process,写入 chatMessages
|
|
202
|
+
* 的用户消息会携带旧 runId + clientMessageId,渲染端(PC renderTranscript /
|
|
203
|
+
* 移动端 projectRemoteConversationItems)会把「clientMessageId + runId 命中
|
|
204
|
+
* workRuns」的消息误判为 guide 消息而跳过气泡,导致排队自动发送的消息不
|
|
205
|
+
* 显示在用户输入时间线。清除这两个字段后 process 走普通用户消息分支
|
|
206
|
+
* (chatMessages.push,mode 取 visibleMode,runId 取当前 run)。
|
|
207
|
+
*/
|
|
208
|
+
private drainQueuedFollowUpMessage;
|
|
197
209
|
queueItems(target: ConversationTargetInput): ConversationQueueItemSnapshot[];
|
|
198
210
|
enqueueNext(target: ConversationTargetInput, input: {
|
|
199
211
|
id: string;
|
|
@@ -60,6 +60,33 @@ class ConversationKernel {
|
|
|
60
60
|
followUp: queued?.followUp.slice() || [],
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* dev-0.5.6: 排队 followUp 消息 drain 时转成普通用户消息载荷。
|
|
65
|
+
*
|
|
66
|
+
* 入队时(enqueueSameSession)为去重/编辑保留了 clientMessageId,并把
|
|
67
|
+
* runId 固定为入队时运行的 runId;若原样传给 process,写入 chatMessages
|
|
68
|
+
* 的用户消息会携带旧 runId + clientMessageId,渲染端(PC renderTranscript /
|
|
69
|
+
* 移动端 projectRemoteConversationItems)会把「clientMessageId + runId 命中
|
|
70
|
+
* workRuns」的消息误判为 guide 消息而跳过气泡,导致排队自动发送的消息不
|
|
71
|
+
* 显示在用户输入时间线。清除这两个字段后 process 走普通用户消息分支
|
|
72
|
+
* (chatMessages.push,mode 取 visibleMode,runId 取当前 run)。
|
|
73
|
+
*/
|
|
74
|
+
drainQueuedFollowUpMessage(message) {
|
|
75
|
+
if (typeof message === 'string')
|
|
76
|
+
return message;
|
|
77
|
+
const text = String(message.text || '');
|
|
78
|
+
const images = message.images;
|
|
79
|
+
const attachments = message.attachments;
|
|
80
|
+
const visible = message.visibleUserInput;
|
|
81
|
+
const visibleMode = message.visibleMode;
|
|
82
|
+
return {
|
|
83
|
+
text,
|
|
84
|
+
...(images?.length ? { images } : {}),
|
|
85
|
+
...(attachments?.length ? { attachments } : {}),
|
|
86
|
+
...(visible ? { visibleUserInput: visible } : {}),
|
|
87
|
+
...(visibleMode ? { visibleMode } : {}),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
63
90
|
queueItems(target) {
|
|
64
91
|
const runtime = this.findRuntime(target);
|
|
65
92
|
if (!runtime)
|
|
@@ -888,7 +915,7 @@ class ConversationKernel {
|
|
|
888
915
|
lastTokens = await this.runSingle(runtime, batchMessage, 'steer');
|
|
889
916
|
}
|
|
890
917
|
else {
|
|
891
|
-
lastTokens = await this.runSingle(runtime, next.message, next.queueMode);
|
|
918
|
+
lastTokens = await this.runSingle(runtime, this.drainQueuedFollowUpMessage(next.message), next.queueMode);
|
|
892
919
|
}
|
|
893
920
|
}
|
|
894
921
|
const rootMessage = runtime.runner.subagents.readRootInbox()[0];
|
|
@@ -1414,6 +1441,10 @@ class ConversationKernel {
|
|
|
1414
1441
|
content: 'Conversation queue updated.',
|
|
1415
1442
|
conversationId: runtime.id,
|
|
1416
1443
|
queue: this.queued(runtime.target),
|
|
1444
|
+
// Structured rows with stable kernel ids so every consumer (PC UI and
|
|
1445
|
+
// the paired mobile client) can render/edit/delete the same items.
|
|
1446
|
+
queueItems: this.queueItems(runtime.target),
|
|
1447
|
+
queuePaused: runtime.queuePaused === true,
|
|
1417
1448
|
});
|
|
1418
1449
|
}
|
|
1419
1450
|
clearQueued(runtime) {
|
|
@@ -601,8 +601,13 @@ class ElectronUtilityRuntimePool {
|
|
|
601
601
|
return this.accessSequence;
|
|
602
602
|
}
|
|
603
603
|
maxResidentRuntimes() {
|
|
604
|
-
|
|
605
|
-
|
|
604
|
+
// The pool hosts one utility runtime per active conversation target.
|
|
605
|
+
// A small default (2) silently blocked users from running more than two
|
|
606
|
+
// conversations at once with "utility runtime pool capacity reached".
|
|
607
|
+
// Idle runtimes are still evicted on LRU after the idle TTL, so a higher
|
|
608
|
+
// default bounds memory by activity, not by an arbitrary conversation cap.
|
|
609
|
+
const configured = Number(this.options.maxResidentRuntimes ?? 8);
|
|
610
|
+
return Number.isFinite(configured) ? Math.max(1, Math.floor(configured)) : 8;
|
|
606
611
|
}
|
|
607
612
|
async serializeCapacity(operation) {
|
|
608
613
|
const previous = this.capacityTail;
|
package/dist/core/types.d.ts
CHANGED
|
@@ -112,6 +112,17 @@ export interface AgentWorkEvent {
|
|
|
112
112
|
steering: string[];
|
|
113
113
|
followUp: string[];
|
|
114
114
|
};
|
|
115
|
+
/** Structured queue rows with stable kernel ids (mobile/PC queue unification export). */
|
|
116
|
+
queueItems?: Array<{
|
|
117
|
+
id: string;
|
|
118
|
+
text: string;
|
|
119
|
+
queueMode: string;
|
|
120
|
+
requestedMode?: string;
|
|
121
|
+
goalObjective?: string;
|
|
122
|
+
runId?: string;
|
|
123
|
+
createdAt: string;
|
|
124
|
+
}>;
|
|
125
|
+
queuePaused?: boolean;
|
|
115
126
|
workspaceId?: string;
|
|
116
127
|
workspaceKey?: string;
|
|
117
128
|
runtimeKey?: string;
|
|
@@ -124,6 +135,16 @@ export interface AgentWorkEvent {
|
|
|
124
135
|
status?: GuideReceiptStatus | ConversationWorkRunStatus | 'stopping' | 'force_restarting';
|
|
125
136
|
guide?: GuideReceipt;
|
|
126
137
|
displayImage?: DisplayImageAttachment;
|
|
138
|
+
/**
|
|
139
|
+
* 结构化模型回退信号:from 为回退前的模型名,to 为实际使用的模型名。
|
|
140
|
+
* 前端据此把输入框下方的模型选择区同步为实际生效的模型,而不是隐藏的
|
|
141
|
+
* 参数回退。
|
|
142
|
+
*/
|
|
143
|
+
fallback?: {
|
|
144
|
+
from: string;
|
|
145
|
+
to: string;
|
|
146
|
+
providerId?: string;
|
|
147
|
+
};
|
|
127
148
|
}
|
|
128
149
|
export interface ConversationWorkRun {
|
|
129
150
|
runId: string;
|
|
@@ -569,8 +569,12 @@ class WslAgentRuntimePool {
|
|
|
569
569
|
return this.accessSequence;
|
|
570
570
|
}
|
|
571
571
|
maxResidentRuntimes() {
|
|
572
|
-
|
|
573
|
-
|
|
572
|
+
// Same rationale as the Electron utility pool: one runtime per active
|
|
573
|
+
// conversation target. The previous default of 2 blocked parallel
|
|
574
|
+
// conversations with a capacity error; idle LRU eviction still bounds
|
|
575
|
+
// resident memory.
|
|
576
|
+
const configured = Number(this.options.maxResidentRuntimes ?? 8);
|
|
577
|
+
return Number.isFinite(configured) ? Math.max(1, Math.floor(configured)) : 8;
|
|
574
578
|
}
|
|
575
579
|
async serializeCapacity(operation) {
|
|
576
580
|
const previous = this.capacityTail;
|
package/dist/main.js
CHANGED
|
@@ -3020,6 +3020,10 @@ else {
|
|
|
3020
3020
|
activePromptLeases.set(promptLeaseKey, (activePromptLeases.get(promptLeaseKey) || 0) + 1);
|
|
3021
3021
|
activePromptWorkspaces.set(promptLeaseWorkspaceKey, (activePromptWorkspaces.get(promptLeaseWorkspaceKey) || 0) + 1);
|
|
3022
3022
|
const targetConversation = target.conversationId;
|
|
3023
|
+
// 发送命令时锁定输入框选择的模型:接受命令后的整个运行过程都以该
|
|
3024
|
+
// 模型为准(唯一例外是显式的不可用回退,且回退会以结构化事件同步
|
|
3025
|
+
// 到前端输入框下方的选择区,而不是隐藏的参数回退)。
|
|
3026
|
+
const requestedModel = agent.model;
|
|
3023
3027
|
const options = {
|
|
3024
3028
|
mode: agent.mode,
|
|
3025
3029
|
model: agent.ensureUsableModelSelection(),
|
|
@@ -3027,6 +3031,21 @@ else {
|
|
|
3027
3031
|
inputMode: agent.inputMode,
|
|
3028
3032
|
engine: agent.engine,
|
|
3029
3033
|
};
|
|
3034
|
+
if (options.model && requestedModel && options.model !== requestedModel) {
|
|
3035
|
+
const usableConfig = agent.activeModelConfig();
|
|
3036
|
+
broadcastAgentWorkEvent({
|
|
3037
|
+
id: `model-fallback-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
|
3038
|
+
conversationId: target.conversationId,
|
|
3039
|
+
type: 'status',
|
|
3040
|
+
content: `[Model fallback] ${requestedModel} unavailable; switched to ${options.model}.`,
|
|
3041
|
+
mode: agent.modeName(),
|
|
3042
|
+
model: options.model,
|
|
3043
|
+
timestamp: new Date().toISOString(),
|
|
3044
|
+
workspaceId: target.workspaceId,
|
|
3045
|
+
workspaceKey: target.workspaceKey,
|
|
3046
|
+
fallback: { from: requestedModel, to: options.model, providerId: usableConfig?.provider_id || agent.activeDeployment()?.providerId },
|
|
3047
|
+
});
|
|
3048
|
+
}
|
|
3030
3049
|
const queueMode = agent.inputMode === 'guide' ? 'steer' : 'followUp';
|
|
3031
3050
|
let result;
|
|
3032
3051
|
if (wslBackendEnabled()) {
|
|
@@ -5206,6 +5225,21 @@ else {
|
|
|
5206
5225
|
// make an ordinary minimize click disappear from the taskbar.
|
|
5207
5226
|
win?.minimize();
|
|
5208
5227
|
});
|
|
5228
|
+
electron_1.ipcMain.handle('glass:captureBackdrop', async (event, requestedSize) => {
|
|
5229
|
+
const image = await event.sender.capturePage();
|
|
5230
|
+
const sourceSize = image.getSize();
|
|
5231
|
+
const width = Math.max(1, Math.min(sourceSize.width, Math.round(Number(requestedSize?.width) || sourceSize.width)));
|
|
5232
|
+
const height = Math.max(1, Math.min(sourceSize.height, Math.round(Number(requestedSize?.height) || sourceSize.height)));
|
|
5233
|
+
const resized = sourceSize.width === width && sourceSize.height === height
|
|
5234
|
+
? image
|
|
5235
|
+
: image.resize({ width, height, quality: 'good' });
|
|
5236
|
+
return {
|
|
5237
|
+
bytes: resized.toJPEG(82),
|
|
5238
|
+
mimeType: 'image/jpeg',
|
|
5239
|
+
width,
|
|
5240
|
+
height,
|
|
5241
|
+
};
|
|
5242
|
+
});
|
|
5209
5243
|
electron_1.ipcMain.handle('app:maximize', () => {
|
|
5210
5244
|
const win = electron_1.BrowserWindow.getFocusedWindow() || mainWindow;
|
|
5211
5245
|
if (win?.isMaximized())
|
package/dist/preload.js
CHANGED
|
@@ -6,6 +6,7 @@ contextBridge.exposeInMainWorld('api', {
|
|
|
6
6
|
startupWaitForBackend: () => ipcRenderer.invoke('startup:waitForBackend'),
|
|
7
7
|
startupUiReady: (payload) => ipcRenderer.invoke('startup:uiReady', payload),
|
|
8
8
|
startupUiFailed: (payload) => ipcRenderer.invoke('startup:uiFailed', payload),
|
|
9
|
+
captureLiquidBackdrop: (size) => ipcRenderer.invoke('glass:captureBackdrop', size),
|
|
9
10
|
onStartupStatus: (callback) => {
|
|
10
11
|
ipcRenderer.on('startup:status', (_event, payload) => callback(payload));
|
|
11
12
|
},
|
|
@@ -17,6 +17,12 @@ export declare function providerStreamTimeoutError(timeoutMs: number): Error;
|
|
|
17
17
|
* Read one SSE chunk with both user cancellation and an inactivity deadline.
|
|
18
18
|
* Cancelling the reader is important: rejecting the race alone leaves the
|
|
19
19
|
* provider socket alive and lets later requests accumulate behind it.
|
|
20
|
+
*
|
|
21
|
+
* timeoutMs defaults to 0 (no stream idle deadline), matching the request-
|
|
22
|
+
* level DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS = 0 and the Android client's
|
|
23
|
+
* readTimeout(0) / SSE_IDLE_TIMEOUT_MS = 0L. A caller that still wants an
|
|
24
|
+
* inactivity cap passes an explicit positive value (the recovery verify
|
|
25
|
+
* passes 50ms to prove reader cancellation).
|
|
20
26
|
*/
|
|
21
27
|
export declare function readProviderStreamChunk(reader: ReadableStreamDefaultReader<Uint8Array>, signal: AbortSignal, timeoutMs?: number): Promise<ReadableStreamReadResult<Uint8Array>>;
|
|
22
28
|
export declare function parseProviderSse(raw: string): Array<{
|