newmark-agent 0.5.4 → 0.5.8

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.
@@ -1016,7 +1016,13 @@ class Agent {
1016
1016
  beginRouteAttempt() {
1017
1017
  this.routeAttemptStartedAt = Date.now();
1018
1018
  }
1019
- async waitForPlannedRouteRetry() {
1019
+ async waitForPlannedRouteRetry(explicitDelayMs) {
1020
+ if (explicitDelayMs !== undefined) {
1021
+ if (explicitDelayMs <= 0)
1022
+ return;
1023
+ await new Promise(resolve => setTimeout(resolve, explicitDelayMs));
1024
+ return;
1025
+ }
1020
1026
  const waitBudgetMs = Math.max(0, Math.min(15_000, this.lastRouteDecision?.retryBudgetMs ?? 5_000));
1021
1027
  const delay = Math.max(0, Math.min(waitBudgetMs, this.lastRouteRetryDelayMs));
1022
1028
  this.lastRouteRetryDelayMs = 0;
@@ -2298,6 +2304,7 @@ class Agent {
2298
2304
  status: input.status,
2299
2305
  guide: !isToolEvent && input.guide ? this.normalizeGuideReceipt(input.guide) : undefined,
2300
2306
  displayImage: isToolEvent ? this.hydrateDisplayImage(input.displayImage) : undefined,
2307
+ fallback: input.fallback,
2301
2308
  };
2302
2309
  if (activeRun && this.isPersistablePublicWorkEvent(event)) {
2303
2310
  activeRun.sequence = Number(sequence || activeRun.sequence + 1);
@@ -5587,10 +5594,22 @@ class Agent {
5587
5594
  }
5588
5595
  compressionBuildBlockStart(messages) {
5589
5596
  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;
5597
+ if (activeRunId) {
5598
+ const index = messages.findIndex(message => String(message.run_id || message.runId || '') === activeRunId);
5599
+ if (index >= 0)
5600
+ return index;
5601
+ }
5602
+ // dev-0.5.6: 空闲对话(无活动 run)时不得把全部历史算进当前 Build Block,
5603
+ // 否则上下文显示窗口的长期历史恒为 0。回退语义:
5604
+ // - 存在带 run_id 的消息:boundary 取最后一个 run 的起点之后(该 run 及其
5605
+ // 之前的历史属于长期历史,其后无归属的消息属于当前未命名区块);
5606
+ // - 完全没有 run_id:全部历史都是长期历史(不存在当前 Build Block)。
5607
+ let lastRunBoundary = -1;
5608
+ for (let index = 0; index < messages.length; index += 1) {
5609
+ if (String(messages[index]?.run_id || messages[index]?.runId || ''))
5610
+ lastRunBoundary = index + 1;
5611
+ }
5612
+ return lastRunBoundary >= 0 ? lastRunBoundary : messages.length;
5594
5613
  }
5595
5614
  recentContextSuffix(messages, maxMessages, tokenBudget) {
5596
5615
  if (!messages.length)
@@ -6651,17 +6670,12 @@ class Agent {
6651
6670
  if (this.routeAttemptStartedAt > 0)
6652
6671
  currentAttempt.durationMs = Math.max(0, Date.now() - this.routeAttemptStartedAt);
6653
6672
  }
6654
- if (!fallbackEnabled) {
6655
- this.lastRouteDecision.finalStatus = 'failed';
6656
- this.routeAttemptStartedAt = 0;
6657
- this.persistRouteDecision(this.lastRouteDecision);
6658
- return null;
6659
- }
6660
6673
  if (!this.pendingAutoAttempts.length) {
6661
6674
  this.pendingAutoAttempts = this.autoRouter.planAttempts(this.lastRouteDecision, this.autoRouteCandidates(), {
6662
6675
  error: failure,
6663
6676
  streamCommitted: this.routeStreamCommitted,
6664
6677
  sideEffectCommitted: this.routeSideEffectCommitted,
6678
+ allowModelFallback: fallbackEnabled,
6665
6679
  });
6666
6680
  }
6667
6681
  const next = this.pendingAutoAttempts.shift();
@@ -6702,8 +6716,11 @@ class Agent {
6702
6716
  return null;
6703
6717
  this.model = next.name;
6704
6718
  this.fixedDeployment = this.deploymentRef(next);
6705
- if (next.provider)
6706
- this.config.set('models', 'auto_switch_anchor_provider', next.provider);
6719
+ // Do NOT rewrite auto_switch_anchor_provider here. The anchor describes
6720
+ // the user's Auto-routing scope; a fixed-model recovery must never
6721
+ // repoint it (a single transient failure would otherwise silently move
6722
+ // every future Auto route — and every future fallback pool — to the
6723
+ // recovery provider, which is the cross-provider leak users observed).
6707
6724
  return current;
6708
6725
  }
6709
6726
  isLlmErrorText(text) {
@@ -6711,18 +6728,23 @@ class Agent {
6711
6728
  }
6712
6729
  scopedSwitchModels(currentModelName) {
6713
6730
  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
- '';
6731
+ // Fixed-model recovery is always provider-local. A same-named model on a
6732
+ // different provider is a different deployment and must never receive the
6733
+ // failed provider's credentials or become an implicit fallback.
6734
+ // Resolution order is strictly deployment-first: the pinned deployment,
6735
+ // then the exact active deployment. The global auto anchor is deliberately
6736
+ // NOT consulted here — it belongs to Auto routing only and can point at a
6737
+ // different provider (for example after a previous cross-provider switch),
6738
+ // which is exactly the same-name cross-provider leak this guard exists to
6739
+ // prevent. When no provider can be established unambiguously, return an
6740
+ // empty pool: failing the switch is safe, crossing providers is not.
6741
+ const current = currentModelName === 'auto' ? this.activeModelConfig() : this.config.findModel(currentModelName);
6742
+ const providerId = current?.provider_id
6743
+ || this.fixedDeployment?.providerId
6744
+ || (currentModelName !== 'auto' ? this.activeDeployment()?.providerId : undefined);
6722
6745
  if (!providerId)
6723
- return all;
6724
- const provider = this.config.findProvider(providerId);
6725
- return all.filter(m => m.provider_id === (provider?.id || providerId));
6746
+ return [];
6747
+ return all.filter(m => m.provider_id === providerId);
6726
6748
  }
6727
6749
  async validateModels(selectedNames, options = {}) {
6728
6750
  if (this.modelValidationPromise)
@@ -8224,9 +8246,17 @@ class Agent {
8224
8246
  return '[Subagent] Not found.';
8225
8247
  const requestedModel = this.normalizeSubagentModelSelection(sa.model && sa.model !== 'default' ? sa.model : this.modelSelectionValue() || this.model);
8226
8248
  const requestedDeployment = parseDeploymentSelectionValue(requestedModel);
8227
- const assignedModel = requestedModel === 'auto'
8249
+ let assignedModel = requestedModel === 'auto'
8228
8250
  ? this.activeModelConfig()
8229
8251
  : (requestedDeployment ? this.config.findDeployment(requestedDeployment) : this.config.findModel(requestedModel));
8252
+ // Imported presets (and stale records) may reference a bare model name
8253
+ // that no longer exists in the configured catalog. Resolving by name must
8254
+ // never guess a provider; instead the peer inherits the parent deployment
8255
+ // so the job can still run. Qualified deployment values keep failing
8256
+ // closed — they are exact references, not guesses.
8257
+ if (!assignedModel && !requestedDeployment && requestedModel !== 'auto') {
8258
+ assignedModel = this.activeModelConfig();
8259
+ }
8230
8260
  const model = assignedModel?.name || (requestedModel === 'auto' ? this.activeModelName() : requestedModel);
8231
8261
  const activeModel = this.activeModelConfig();
8232
8262
  const activeProvider = this.engineModel();
@@ -46,6 +46,7 @@ const toolPolicy_1 = require("./toolPolicy");
46
46
  const performanceDiagnostics_1 = require("./performanceDiagnostics");
47
47
  const agentKernelDiagnostics_1 = require("./agentKernelDiagnostics");
48
48
  const toolchain_1 = require("../toolchain");
49
+ const emptyResponseRetry_1 = require("./emptyResponseRetry");
49
50
  const publicStreamFilters = new WeakMap();
50
51
  const brokerOnlyAssistantBuffers = new WeakMap();
51
52
  const BROKER_PREFACE_BUFFER_CHARS = 96;
@@ -206,7 +207,7 @@ function kernelTurnFailed(agent, turn) {
206
207
  return turn.stopReason === 'error' || agent.isLlmErrorText(turn.text);
207
208
  }
208
209
  function providerTurnIsEmpty(turn) {
209
- return /provider returned an empty response/i.test(`${turn.errorMessage}\n${turn.text}`);
210
+ return !turn.activity && /provider returned an empty response/i.test(`${turn.errorMessage}\n${turn.text}`);
210
211
  }
211
212
  function removeTrailingFailedAssistant(agent, messages) {
212
213
  const last = messages[messages.length - 1];
@@ -216,6 +217,14 @@ function removeTrailingFailedAssistant(agent, messages) {
216
217
  if (last.stopReason === 'error' || agent.isLlmErrorText(text))
217
218
  messages.pop();
218
219
  }
220
+ function removeTrailingThoughtOnlyAssistant(messages) {
221
+ const last = messages[messages.length - 1];
222
+ if (last?.role !== 'assistant')
223
+ return;
224
+ const hasToolCall = last.content.some(content => content.type === 'toolCall');
225
+ if (!KernelMessageText(last).trim() && !hasToolCall)
226
+ messages.pop();
227
+ }
219
228
  function normalizePublicProviderError(error, secrets = []) {
220
229
  let raw = '';
221
230
  if (error instanceof Error) {
@@ -369,8 +378,22 @@ async function runAgentKernel(agent) {
369
378
  const tokens = [];
370
379
  const runOnce = async (promptMessages, appendPromptToAgentHistory) => {
371
380
  let lastAssistant = null;
381
+ let observedActivity = false;
382
+ let observedThought = false;
372
383
  const unsubscribe = kernel.subscribe(async (event) => {
373
384
  await handleKernelEvent(agent, event, tokens);
385
+ if (event.type === 'message_update') {
386
+ const delta = event.assistantMessageEvent;
387
+ const deltaText = typeof delta.delta === 'string'
388
+ ? delta.delta
389
+ : '';
390
+ const thoughtDelta = delta.type === 'thinking_delta' && !!deltaText.trim();
391
+ observedThought = observedThought || thoughtDelta;
392
+ observedActivity = observedActivity ||
393
+ thoughtDelta ||
394
+ (delta.type === 'text_delta' && !!deltaText.trim()) ||
395
+ (delta.type === 'toolcall_end');
396
+ }
374
397
  if (event.type === 'message_end' && event.message.role === 'assistant') {
375
398
  lastAssistant = event.message;
376
399
  }
@@ -390,11 +413,14 @@ async function runAgentKernel(agent) {
390
413
  const text = assistant ? KernelMessageText(assistant) : '';
391
414
  const hasToolCall = !!assistant?.content?.some(content => content.type === 'toolCall');
392
415
  const emptyResponse = !assistant
393
- || (!text.trim() && !hasToolCall && String(assistant?.stopReason || '') !== 'aborted');
416
+ || (!text.trim() && !hasToolCall && !observedActivity && String(assistant?.stopReason || '') !== 'aborted');
394
417
  return {
395
418
  text: emptyResponse ? '[Error] Provider returned an empty response.' : text,
396
419
  stopReason: String(assistant?.stopReason || ''),
397
420
  errorMessage: String(assistant?.errorMessage || (emptyResponse ? 'Provider returned an empty response.' : '')),
421
+ activity: observedActivity || !!text.trim() || hasToolCall,
422
+ thoughtOnly: observedThought && !text.trim() && !hasToolCall
423
+ && !['error', 'aborted'].includes(String(assistant?.stopReason || '')),
398
424
  };
399
425
  }
400
426
  finally {
@@ -440,16 +466,31 @@ async function runAgentKernel(agent) {
440
466
  agent.recordWorkStatus('Final visual fallback used: local mini OCR plus conservative text correction.');
441
467
  }
442
468
  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}.` });
469
+ const notice = `[Model fallback] ${modelBeforeKernelRun} unavailable; switched to ${agent.model}.`;
470
+ tokens.unshift({ type: 'text', text: notice });
471
+ agent.emitWorkEvent({
472
+ type: 'status',
473
+ content: notice,
474
+ fallback: { from: modelBeforeKernelRun, to: agent.model, providerId: agent.activeDeployment()?.providerId },
475
+ });
444
476
  }
445
- let emptyResponseRetries = 0;
446
- while (providerTurnIsEmpty(lastTurn) && emptyResponseRetries < 2) {
477
+ let consecutiveEmptyResponses = 0;
478
+ for (;;) {
479
+ const emptyResponseState = (0, emptyResponseRetry_1.observeEmptyResponseOutcome)(consecutiveEmptyResponses, providerTurnIsEmpty(lastTurn));
480
+ consecutiveEmptyResponses = emptyResponseState.consecutiveEmptyResponses;
481
+ if (lastTurn.thoughtOnly) {
482
+ removeTrailingThoughtOnlyAssistant(kernel.state.messages);
483
+ lastTurn = await runWithCompressionResume([], false);
484
+ continue;
485
+ }
486
+ if (!emptyResponseState.retry)
487
+ break;
447
488
  removeTrailingFailedAssistant(agent, kernel.state.messages);
448
- emptyResponseRetries += 1;
449
- const notice = `[Model retry] Provider returned an empty response; retrying the same deployment (${emptyResponseRetries}/2).`;
489
+ const retryNumber = consecutiveEmptyResponses;
490
+ const notice = `[Model retry] Provider returned an empty response; retrying the same deployment (${retryNumber}/${emptyResponseRetry_1.MAX_EMPTY_RESPONSE_RETRIES}) after ${(0, emptyResponseRetry_1.emptyResponseRetryDelayMs)(consecutiveEmptyResponses)}ms.`;
450
491
  tokens.push({ type: 'text', text: notice });
451
492
  agent.recordWorkStatus(notice);
452
- await agent.waitForPlannedRouteRetry();
493
+ await agent.waitForPlannedRouteRetry((0, emptyResponseRetry_1.emptyResponseRetryDelayMs)(consecutiveEmptyResponses));
453
494
  lastTurn = await runWithCompressionResume([], false);
454
495
  }
455
496
  let routeRetries = 0;
@@ -462,6 +503,11 @@ async function runAgentKernel(agent) {
462
503
  const notice = routeTransitionNotice(agent, previous);
463
504
  tokens.push({ type: 'text', text: notice });
464
505
  agent.recordWorkStatus(notice);
506
+ agent.emitWorkEvent({
507
+ type: 'status',
508
+ content: notice,
509
+ fallback: { from: previous, to: agent.model, providerId: agent.activeDeployment()?.providerId },
510
+ });
465
511
  kernel.state.model = toKernelModel(agent);
466
512
  const fallbackToolSurface = refreshToolSurface(true);
467
513
  kernel.state.systemPrompt = [agent.buildSystemPrompt(), fallbackToolSurface.systemPromptNotice].filter(Boolean).join('\n\n');
@@ -656,7 +702,7 @@ async function runAgentKernel(agent) {
656
702
  }
657
703
  if (textStarted)
658
704
  finalContent.push({ type: 'text', text });
659
- if (!finalContent.length) {
705
+ if (!finalContent.length && !thinking.trim()) {
660
706
  text = '[Error] Provider returned an empty response.';
661
707
  finalContent.push({ type: 'text', text });
662
708
  }
@@ -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;
@@ -0,0 +1,16 @@
1
+ export declare const EMPTY_RESPONSE_RETRY_DELAYS_MS: readonly [200, 800, 2000, 10000, 60000];
2
+ /**
3
+ * A retry is scheduled only after an explicit provider empty-response
4
+ * failure. The initial failed request is not called a retry, so five retries
5
+ * means six consecutive explicit failures before termination.
6
+ */
7
+ export declare const MAX_EMPTY_RESPONSE_RETRIES: 5;
8
+ export declare const MAX_CONSECUTIVE_EMPTY_RESPONSES: number;
9
+ export declare function emptyResponseRetryDelayMs(consecutiveEmptyResponses: number): number;
10
+ export interface EmptyResponseRetryState {
11
+ consecutiveEmptyResponses: number;
12
+ retry: boolean;
13
+ terminate: boolean;
14
+ }
15
+ export declare function observeEmptyResponseOutcome(consecutiveEmptyResponses: number, emptyResponse: boolean): EmptyResponseRetryState;
16
+ //# sourceMappingURL=emptyResponseRetry.d.ts.map
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MAX_CONSECUTIVE_EMPTY_RESPONSES = exports.MAX_EMPTY_RESPONSE_RETRIES = exports.EMPTY_RESPONSE_RETRY_DELAYS_MS = void 0;
4
+ exports.emptyResponseRetryDelayMs = emptyResponseRetryDelayMs;
5
+ exports.observeEmptyResponseOutcome = observeEmptyResponseOutcome;
6
+ exports.EMPTY_RESPONSE_RETRY_DELAYS_MS = [200, 800, 2_000, 10_000, 60_000];
7
+ /**
8
+ * A retry is scheduled only after an explicit provider empty-response
9
+ * failure. The initial failed request is not called a retry, so five retries
10
+ * means six consecutive explicit failures before termination.
11
+ */
12
+ exports.MAX_EMPTY_RESPONSE_RETRIES = exports.EMPTY_RESPONSE_RETRY_DELAYS_MS.length;
13
+ exports.MAX_CONSECUTIVE_EMPTY_RESPONSES = exports.MAX_EMPTY_RESPONSE_RETRIES + 1;
14
+ function emptyResponseRetryDelayMs(consecutiveEmptyResponses) {
15
+ return exports.EMPTY_RESPONSE_RETRY_DELAYS_MS[Math.max(0, consecutiveEmptyResponses - 1)] ?? 0;
16
+ }
17
+ function observeEmptyResponseOutcome(consecutiveEmptyResponses, emptyResponse) {
18
+ const nextCount = emptyResponse ? consecutiveEmptyResponses + 1 : 0;
19
+ return {
20
+ consecutiveEmptyResponses: nextCount,
21
+ retry: emptyResponse && nextCount <= exports.MAX_EMPTY_RESPONSE_RETRIES,
22
+ terminate: emptyResponse && nextCount > exports.MAX_EMPTY_RESPONSE_RETRIES,
23
+ };
24
+ }
25
+ //# sourceMappingURL=emptyResponseRetry.js.map
@@ -0,0 +1,24 @@
1
+ export declare const TERMINAL_HISTORY_LIMIT: number;
2
+ export interface TerminalOutputBufferOptions {
3
+ flushIntervalMs?: number;
4
+ historyLimit?: number;
5
+ }
6
+ export declare class TerminalOutputBuffer {
7
+ private readonly send;
8
+ private readonly sessions;
9
+ private timer;
10
+ private readonly flushIntervalMs;
11
+ private readonly historyLimit;
12
+ constructor(send: (sessionId: string, text: string) => void, options?: TerminalOutputBufferOptions);
13
+ push(sessionId: string, text: string): void;
14
+ flush(sessionId: string): void;
15
+ flushAll(): void;
16
+ close(sessionId: string): string;
17
+ history(sessionId: string): string;
18
+ pendingChunkCount(): number;
19
+ hasScheduledFlush(): boolean;
20
+ private bound;
21
+ private schedule;
22
+ private clearTimerWhenIdle;
23
+ }
24
+ //# sourceMappingURL=terminalOutputBuffer.d.ts.map
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TerminalOutputBuffer = exports.TERMINAL_HISTORY_LIMIT = void 0;
4
+ exports.TERMINAL_HISTORY_LIMIT = 256 * 1024;
5
+ class TerminalOutputBuffer {
6
+ send;
7
+ sessions = new Map();
8
+ timer = null;
9
+ flushIntervalMs;
10
+ historyLimit;
11
+ constructor(send, options = {}) {
12
+ this.send = send;
13
+ this.flushIntervalMs = Math.max(1, options.flushIntervalMs ?? 20);
14
+ this.historyLimit = Math.max(1, options.historyLimit ?? exports.TERMINAL_HISTORY_LIMIT);
15
+ }
16
+ push(sessionId, text) {
17
+ if (!text)
18
+ return;
19
+ let state = this.sessions.get(sessionId);
20
+ if (!state) {
21
+ state = { chunks: [], length: 0, history: '' };
22
+ this.sessions.set(sessionId, state);
23
+ }
24
+ state.chunks.push(text);
25
+ state.length += text.length;
26
+ this.schedule();
27
+ }
28
+ flush(sessionId) {
29
+ const state = this.sessions.get(sessionId);
30
+ if (!state?.length)
31
+ return;
32
+ const text = state.chunks.length === 1 ? state.chunks[0] : state.chunks.join('');
33
+ state.chunks = [];
34
+ state.length = 0;
35
+ state.history = this.bound(`${state.history}${text}`);
36
+ this.send(sessionId, text);
37
+ this.clearTimerWhenIdle();
38
+ }
39
+ flushAll() {
40
+ for (const sessionId of this.sessions.keys())
41
+ this.flush(sessionId);
42
+ this.clearTimerWhenIdle(true);
43
+ }
44
+ close(sessionId) {
45
+ this.flush(sessionId);
46
+ const history = this.sessions.get(sessionId)?.history ?? '';
47
+ this.sessions.delete(sessionId);
48
+ this.clearTimerWhenIdle();
49
+ return history;
50
+ }
51
+ history(sessionId) {
52
+ const state = this.sessions.get(sessionId);
53
+ if (!state)
54
+ return '';
55
+ if (!state.length)
56
+ return state.history;
57
+ const pending = state.chunks.length === 1 ? state.chunks[0] : state.chunks.join('');
58
+ return this.bound(`${state.history}${pending}`);
59
+ }
60
+ pendingChunkCount() {
61
+ let count = 0;
62
+ for (const state of this.sessions.values())
63
+ count += state.chunks.length;
64
+ return count;
65
+ }
66
+ hasScheduledFlush() {
67
+ return this.timer !== null;
68
+ }
69
+ bound(text) {
70
+ return text.length > this.historyLimit ? text.slice(-this.historyLimit) : text;
71
+ }
72
+ schedule() {
73
+ if (this.timer)
74
+ return;
75
+ this.timer = setTimeout(() => {
76
+ this.timer = null;
77
+ this.flushAll();
78
+ }, this.flushIntervalMs);
79
+ this.timer.unref?.();
80
+ }
81
+ clearTimerWhenIdle(force = false) {
82
+ if (!this.timer)
83
+ return;
84
+ const hasPending = !force && Array.from(this.sessions.values()).some(state => state.length > 0);
85
+ if (hasPending)
86
+ return;
87
+ clearTimeout(this.timer);
88
+ this.timer = null;
89
+ }
90
+ }
91
+ exports.TerminalOutputBuffer = TerminalOutputBuffer;
92
+ //# sourceMappingURL=terminalOutputBuffer.js.map
@@ -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;
@@ -96,7 +96,7 @@ export declare class LLMProvider {
96
96
  * `provider_adapters_v2` context flag. Request serialization and SSE
97
97
  * normalization are delegated to the shared provider adapters while the
98
98
  * transport orchestration (loopback node-http, fetch -> node-http fallback,
99
- * 120s/30s timeouts) and the 4xx Chat -> Responses downgrade stay here.
99
+ * cancellation-only streaming and the 4xx Chat -> Responses downgrade) stay here.
100
100
  * The emitted request body and StreamToken stream are byte-equivalent to
101
101
  * the legacy inlined path.
102
102
  */
@@ -106,9 +106,9 @@ export declare class LLMProvider {
106
106
  private shouldDowngradeToResponses;
107
107
  /**
108
108
  * Loopback-aware transport injected into adapter `execute`. Streaming
109
- * requests retain the fetch-to-node fallback for transport failures, while
110
- * a local deadline is returned directly so one request cannot become a
111
- * second Windows fallback request.
109
+ * requests retain the fetch-to-node fallback for transport failures. They
110
+ * have no response deadline; only caller cancellation or a concrete
111
+ * transport/provider failure may end the request.
112
112
  */
113
113
  private buildProviderAdapterTransport;
114
114
  private toTransportResponse;