newmark-agent 0.5.3 → 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.
@@ -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<{
@@ -46,8 +46,14 @@ function providerStreamTimeoutError(timeoutMs) {
46
46
  * Read one SSE chunk with both user cancellation and an inactivity deadline.
47
47
  * Cancelling the reader is important: rejecting the race alone leaves the
48
48
  * provider socket alive and lets later requests accumulate behind it.
49
+ *
50
+ * timeoutMs defaults to 0 (no stream idle deadline), matching the request-
51
+ * level DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS = 0 and the Android client's
52
+ * readTimeout(0) / SSE_IDLE_TIMEOUT_MS = 0L. A caller that still wants an
53
+ * inactivity cap passes an explicit positive value (the recovery verify
54
+ * passes 50ms to prove reader cancellation).
49
55
  */
50
- async function readProviderStreamChunk(reader, signal, timeoutMs = 30_000) {
56
+ async function readProviderStreamChunk(reader, signal, timeoutMs = 0) {
51
57
  if (signal.aborted)
52
58
  throw providerAbortError(signal);
53
59
  let timer;
@@ -56,9 +62,14 @@ async function readProviderStreamChunk(reader, signal, timeoutMs = 30_000) {
56
62
  onAbort = () => reject(providerAbortError(signal));
57
63
  signal.addEventListener('abort', onAbort, { once: true });
58
64
  });
59
- const timeoutPromise = new Promise((_, reject) => {
60
- timer = setTimeout(() => reject(providerStreamTimeoutError(timeoutMs)), timeoutMs);
61
- });
65
+ // timeoutMs <= 0 means no inactivity deadline (unlimited). setTimeout with
66
+ // 0 would fire on the next tick, so we only arm the timer for positive
67
+ // values and race a never-settling promise otherwise.
68
+ const timeoutPromise = timeoutMs > 0
69
+ ? new Promise((_, reject) => {
70
+ timer = setTimeout(() => reject(providerStreamTimeoutError(timeoutMs)), timeoutMs);
71
+ })
72
+ : new Promise(() => undefined);
62
73
  try {
63
74
  return await Promise.race([reader.read(), abortPromise, timeoutPromise]);
64
75
  }
package/dist/server.js CHANGED
@@ -1298,6 +1298,27 @@ async function handleApi(req, res, body) {
1298
1298
  mobileJson(res, await hostedConversationUiAction({ workspaceId, conversationId }, action, String(params.value || ''), params));
1299
1299
  return;
1300
1300
  }
1301
+ case '/api/queue-action': {
1302
+ // Desktop-renderer queue mutations. The mobile endpoint requires a
1303
+ // pairing token; the renderer shares the same process and is trusted,
1304
+ // so it gets a dedicated unauthenticated route that reuses the same
1305
+ // GUI runtime-pool queueAction path (update/delete/reorder/guide).
1306
+ const params = JSON.parse(body || '{}');
1307
+ const workspaceId = String(params.workspaceId || '');
1308
+ const conversationId = String(params.conversationId || '');
1309
+ const action = String(params.action || '');
1310
+ const allowed = new Set(['queue_enqueue', 'queue_update', 'queue_delete', 'queue_reorder', 'queue_toggle_pause', 'queue_guide']);
1311
+ if (!workspaceId || !conversationId || !allowed.has(action)) {
1312
+ jsonResponse(res, { error: 'workspaceId, conversationId, and a valid queue action are required' }, 400);
1313
+ return;
1314
+ }
1315
+ if (!hostedConversationUiAction) {
1316
+ jsonResponse(res, { error: 'This action requires the GUI-hosted runtime pool' }, 409);
1317
+ return;
1318
+ }
1319
+ jsonResponse(res, await hostedConversationUiAction({ workspaceId, conversationId }, action, String(params.value || ''), params));
1320
+ return;
1321
+ }
1301
1322
  case '/api/mobile/conversation-rename': {
1302
1323
  const params = JSON.parse(body || '{}');
1303
1324
  const workspaceId = String(params.workspaceId || '');
@@ -2,6 +2,7 @@ import { ConfigManager } from '../core/config';
2
2
  import { NewmarkToolDefinition, NewmarkToolResult } from '../core/compat';
3
3
  import { SshManager } from '../core/ssh';
4
4
  import { WorkspaceManager } from '../core/workspace';
5
+ import { LocalOcrResult } from '../core/localOcr';
5
6
  export interface ToolExecutionContext {
6
7
  mode?: string;
7
8
  workspacePath?: string;
@@ -30,6 +31,8 @@ export declare class ToolExecutor {
30
31
  private hostProfile;
31
32
  constructor(root: string, config: ConfigManager, ssh?: SshManager | undefined, workspace?: WorkspaceManager | undefined);
32
33
  webSearch(query: string): Promise<string>;
34
+ /** OCR entry point for the runtime's final visual fallback. */
35
+ finalVisualFallbackOcr(dataUrl: string, signal?: AbortSignal): Promise<LocalOcrResult>;
33
36
  setHostProfile(profile: ToolHostProfile): void;
34
37
  definitions(mode?: string): unknown[];
35
38
  canonicalDefinitions(mode?: string): NewmarkToolDefinition[];
@@ -234,6 +234,10 @@ class ToolExecutor {
234
234
  async webSearch(query) {
235
235
  return this.wsearch(query);
236
236
  }
237
+ /** OCR entry point for the runtime's final visual fallback. */
238
+ async finalVisualFallbackOcr(dataUrl, signal) {
239
+ return await this.localOcr.recognizeDataUrl(dataUrl, signal, 'sparse-ui');
240
+ }
237
241
  setHostProfile(profile) {
238
242
  this.hostProfile = { ...profile };
239
243
  }