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.
@@ -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 = 3e4) {
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
- tokens.unshift({ type: "text", text: `[Model fallback] ${modelBeforeKernelRun} unavailable; switched to ${agent.model}.` });
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 (!activeRunId) return 0;
349670
- const index = messages.findIndex((message) => String(message.run_id || message.runId || "") === activeRunId);
349671
- return index >= 0 ? index : 0;
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
- if (this.config.autoSwitchScope() !== "provider") return all;
350654
- const current = currentModelName === "auto" ? void 0 : this.config.findModel(currentModelName);
350655
- const providerId = current?.provider_id || this.config.autoSwitchAnchorProvider() || this.config.findModel(this.config.getStr("models", "default_model"))?.provider_id || all[0]?.provider_id || "";
350656
- if (!providerId) return all;
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
- const assignedModel = requestedModel === "auto" ? this.activeModelConfig() : requestedDeployment ? this.config.findDeployment(requestedDeployment) : this.config.findModel(requestedModel);
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) {
@@ -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 (!activeRunId)
5591
- return 0;
5592
- const index = messages.findIndex(message => String(message.run_id || message.runId || '') === activeRunId);
5593
- return index >= 0 ? index : 0;
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
- if (next.provider)
6706
- this.config.set('models', 'auto_switch_anchor_provider', next.provider);
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
- if (this.config.autoSwitchScope() !== 'provider')
6715
- return all;
6716
- const current = currentModelName === 'auto' ? undefined : this.config.findModel(currentModelName);
6717
- const providerId = current?.provider_id ||
6718
- this.config.autoSwitchAnchorProvider() ||
6719
- this.config.findModel(this.config.getStr('models', 'default_model'))?.provider_id ||
6720
- all[0]?.provider_id ||
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 all;
6724
- const provider = this.config.findProvider(providerId);
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
- const assignedModel = requestedModel === 'auto'
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
- tokens.unshift({ type: 'text', text: `[Model fallback] ${modelBeforeKernelRun} unavailable; switched to ${agent.model}.` });
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;
@@ -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
- const configured = Number(this.options.maxResidentRuntimes ?? 2);
605
- return Number.isFinite(configured) ? Math.max(1, Math.floor(configured)) : 2;
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;
@@ -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
- const configured = Number(this.options.maxResidentRuntimes ?? 2);
573
- return Number.isFinite(configured) ? Math.max(1, Math.floor(configured)) : 2;
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<{