newmark-agent 0.5.11 → 0.5.13

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.
@@ -18,6 +18,13 @@ export declare function isPublicBrowserUseAttribute(input: string): boolean;
18
18
  export interface BrowserUseScope {
19
19
  owner: string;
20
20
  runtimeKey: string;
21
+ /**
22
+ * Selects the user-visible right-sidebar browser surface. `false` keeps the
23
+ * same Browser-Use protocol on a host-owned background page that is never
24
+ * attached to the renderer. Omission is normalized to `true` for backwards
25
+ * compatibility.
26
+ */
27
+ visible?: boolean;
21
28
  }
22
29
  export interface BrowserUseRequest extends BrowserUseScope {
23
30
  action: BrowserUseAction;
@@ -46,7 +46,17 @@ function hasControlCharacter(text) {
46
46
  return false;
47
47
  }
48
48
  function scopeKey(scope) {
49
- return `${scope.runtimeKey}\u0000${scope.owner}`;
49
+ return `${scope.runtimeKey}\u0000${scope.owner}\u0000${browserUseVisible(scope.visible) ? 'visible' : 'background'}`;
50
+ }
51
+ function browserUseVisible(value) {
52
+ return typeof value === 'boolean' ? value : true;
53
+ }
54
+ function bindBrowserUseVisible(value) {
55
+ if (value === undefined)
56
+ return true;
57
+ if (typeof value !== 'boolean')
58
+ throw new TypeError('Browser-Use visible must be a boolean when provided.');
59
+ return value;
50
60
  }
51
61
  function abortReason(signal) {
52
62
  return signal.reason instanceof Error
@@ -154,6 +164,7 @@ function bindBrowserUseRequest(input, context) {
154
164
  const request = {
155
165
  owner: runtimeKey && actorId ? `browser-use:${runtimeKey}:actor:${actorId}` : '',
156
166
  runtimeKey,
167
+ visible: bindBrowserUseVisible(raw.visible),
157
168
  action: String(raw.action || '').trim().toLowerCase(),
158
169
  };
159
170
  if (raw.actionId !== undefined || raw.action_id !== undefined)
@@ -263,10 +274,11 @@ class BrowserUseEngine {
263
274
  const rawAction = String(input?.action || '').trim().toLowerCase();
264
275
  const action = (ACTIONS.has(rawAction) ? rawAction : 'observe');
265
276
  const actionId = cleanScopePart(input?.actionId) || `browser-use-${this.id()}`;
266
- const normalized = { ...input, owner, runtimeKey, action, actionId };
277
+ const visible = browserUseVisible(input?.visible);
278
+ const normalized = { ...input, owner, runtimeKey, visible, action, actionId };
267
279
  if (!owner || !runtimeKey || !ACTIONS.has(rawAction))
268
280
  return await this.runNow(normalized, signal);
269
- const session = this.ensureSession({ owner, runtimeKey });
281
+ const session = this.ensureSession({ owner, runtimeKey, visible });
270
282
  const cached = session.receipts.get(actionId);
271
283
  if (cached)
272
284
  return cached;
@@ -293,17 +305,18 @@ class BrowserUseEngine {
293
305
  throwIfBrowserUseAborted(signal);
294
306
  const owner = cleanScopePart(input?.owner);
295
307
  const runtimeKey = cleanScopePart(input?.runtimeKey);
308
+ const visible = browserUseVisible(input?.visible);
296
309
  const rawAction = String(input?.action || '').trim().toLowerCase();
297
310
  const action = (ACTIONS.has(rawAction) ? rawAction : 'observe');
298
311
  const actionId = cleanScopePart(input?.actionId) || `browser-use-${this.id()}`;
299
312
  const startedAt = this.now();
300
313
  if (!owner || !runtimeKey) {
301
- return this.standaloneFailure({ owner, runtimeKey, action, actionId, startedAt }, 'invalid_scope', 'Browser-Use requires a non-empty owner and runtimeKey.');
314
+ return this.standaloneFailure({ owner, runtimeKey, visible, action, actionId, startedAt }, 'invalid_scope', 'Browser-Use requires a non-empty owner and runtimeKey.');
302
315
  }
303
316
  if (!ACTIONS.has(rawAction)) {
304
- return this.standaloneFailure({ owner, runtimeKey, action, actionId, startedAt }, 'invalid_request', `Unsupported Browser-Use action: ${rawAction || '(missing)'}`);
317
+ return this.standaloneFailure({ owner, runtimeKey, visible, action, actionId, startedAt }, 'invalid_request', `Unsupported Browser-Use action: ${rawAction || '(missing)'}`);
305
318
  }
306
- const scope = { owner, runtimeKey };
319
+ const scope = { owner, runtimeKey, visible };
307
320
  const session = this.ensureSession(scope);
308
321
  const cached = session.receipts.get(actionId);
309
322
  if (cached)
@@ -572,7 +585,10 @@ class BrowserUse {
572
585
  throwIfBrowserUseAborted(signal);
573
586
  if (bridged.ok && bridged.data && typeof bridged.data === 'object') {
574
587
  const receipt = bridged.data;
575
- if (receipt.action && receipt.actionId && receipt.owner === request.owner && receipt.runtimeKey === request.runtimeKey)
588
+ if (receipt.action && receipt.actionId
589
+ && receipt.owner === request.owner
590
+ && receipt.runtimeKey === request.runtimeKey
591
+ && browserUseVisible(receipt.visible) === browserUseVisible(request.visible))
576
592
  return receipt;
577
593
  }
578
594
  return this.unavailable(request, bridged.error ? 'backend_error' : 'backend_unavailable', bridged.error || 'Browser-Use backend is not connected. Start Newmark Desktop to use the built-in browser.');
@@ -585,6 +601,7 @@ class BrowserUse {
585
601
  actionId: cleanScopePart(request?.actionId) || `browser-use-${(0, crypto_1.randomUUID)()}`,
586
602
  owner: cleanScopePart(request?.owner),
587
603
  runtimeKey: cleanScopePart(request?.runtimeKey),
604
+ visible: browserUseVisible(request?.visible),
588
605
  sequence: 0,
589
606
  pageGeneration: Number.isInteger(request?.pageGeneration) ? Number(request.pageGeneration) : 0,
590
607
  startedAt: now,
@@ -285,6 +285,10 @@ export declare class ConversationKernel {
285
285
  private settleCooperativeStop;
286
286
  private stopAutomaticContinuationAfterError;
287
287
  private run;
288
+ private automaticMessage;
289
+ private currentAssistantFingerprint;
290
+ private rememberAutomaticAssistantFingerprint;
291
+ private repeatedAutomaticAssistant;
288
292
  /**
289
293
  * Apply a model selection recorded while a Build block was running. The
290
294
  * in-flight block never switches mid-block; the switch takes effect the next
@@ -85,6 +85,11 @@ class ConversationKernel {
85
85
  ...(attachments?.length ? { attachments } : {}),
86
86
  ...(visible ? { visibleUserInput: visible } : {}),
87
87
  ...(visibleMode ? { visibleMode } : {}),
88
+ // A normal user follow-up must never inherit the identity metadata that
89
+ // was only needed while it lived in the runtime queue. In particular,
90
+ // do not let a stale/forwarded hiddenUserInput flag classify it as an
91
+ // Agent-generated continuation when Agent.process persists the turn.
92
+ hiddenUserInput: message.hiddenUserInput === true && !message.clientMessageId,
88
93
  };
89
94
  }
90
95
  queueItems(target) {
@@ -718,7 +723,7 @@ class ConversationKernel {
718
723
  this.deferOutstandingGuides(runtime);
719
724
  const checkpointed = this.checkpoint(runtime.target).checkpointed;
720
725
  runtime.stopCheckpointed = checkpointed;
721
- runtime.runner.abortActiveKernelRun();
726
+ runtime.runner.abortActiveKernelRun('user_stop');
722
727
  runtime.runner.emitWorkEvent({
723
728
  type: 'status',
724
729
  content: 'Stop requested. Saving progress and interrupting this conversation.',
@@ -874,6 +879,7 @@ class ConversationKernel {
874
879
  async run(runtime, message, options) {
875
880
  this.applyOptions(runtime.runner, options);
876
881
  let lastTokens = await this.runSingle(runtime, message);
882
+ this.rememberAutomaticAssistantFingerprint(runtime, message);
877
883
  if (runtime.stopRequestedRunId === runtime.runId) {
878
884
  this.mirrorHostIfTargetActive(runtime);
879
885
  return this.result(runtime, lastTokens);
@@ -904,6 +910,8 @@ class ConversationKernel {
904
910
  }
905
911
  if (batchGuides.length === 1) {
906
912
  lastTokens = await this.runSingle(runtime, next.message, next.queueMode);
913
+ if (this.repeatedAutomaticAssistant(runtime, next.message, next.queueMode))
914
+ break;
907
915
  continue;
908
916
  }
909
917
  const batchText = batchGuides.map((guide, index) => `Guide ${index + 1}: ${guide.text}`).join('\n');
@@ -913,9 +921,14 @@ class ConversationKernel {
913
921
  batchGuides,
914
922
  };
915
923
  lastTokens = await this.runSingle(runtime, batchMessage, 'steer');
924
+ if (this.repeatedAutomaticAssistant(runtime, batchMessage, 'steer'))
925
+ break;
916
926
  }
917
927
  else {
918
- lastTokens = await this.runSingle(runtime, this.drainQueuedFollowUpMessage(next.message), next.queueMode);
928
+ const drained = this.drainQueuedFollowUpMessage(next.message);
929
+ lastTokens = await this.runSingle(runtime, drained, next.queueMode);
930
+ if (this.repeatedAutomaticAssistant(runtime, drained, next.queueMode))
931
+ break;
919
932
  }
920
933
  }
921
934
  const rootMessage = runtime.runner.subagents.readRootInbox()[0];
@@ -956,6 +969,41 @@ class ConversationKernel {
956
969
  this.mirrorHostIfTargetActive(runtime);
957
970
  return this.result(runtime, lastTokens);
958
971
  }
972
+ automaticMessage(message, queueMode) {
973
+ return queueMode === 'steer'
974
+ || (typeof message !== 'string' && (message.hiddenUserInput === true || message.goalContinuation === true || !!message.batchGuides?.length));
975
+ }
976
+ currentAssistantFingerprint(runtime) {
977
+ const messages = runtime.runner.chatMessages || [];
978
+ const last = messages[messages.length - 1];
979
+ if (!last || last.role !== 'assistant')
980
+ return '';
981
+ return String(last.content || '').trim().replace(/\s+/g, ' ').toLowerCase().slice(0, 4000);
982
+ }
983
+ rememberAutomaticAssistantFingerprint(runtime, message, queueMode) {
984
+ if (this.automaticMessage(message, queueMode))
985
+ runtime.lastAutomaticAssistantFingerprint = this.currentAssistantFingerprint(runtime) || undefined;
986
+ }
987
+ repeatedAutomaticAssistant(runtime, message, queueMode) {
988
+ if (!this.automaticMessage(message, queueMode))
989
+ return false;
990
+ const fingerprint = this.currentAssistantFingerprint(runtime);
991
+ if (!fingerprint)
992
+ return false;
993
+ if (runtime.lastAutomaticAssistantFingerprint === fingerprint) {
994
+ runtime.pendingNextTurn = runtime.pendingNextTurn.filter(item => {
995
+ const automatic = item.queueMode === 'steer' || (typeof item.message !== 'string' && item.message.hiddenUserInput === true);
996
+ if (automatic)
997
+ runtime.runner.consumeConversationContinuation({ content: typeof item.message === 'string' ? item.message : item.message.text, queueMode: item.queueMode, clientMessageId: typeof item.message === 'string' ? undefined : item.message.clientMessageId });
998
+ return !automatic;
999
+ });
1000
+ runtime.runner.recordWorkStatus('Automatic continuation stopped after a repeated assistant response.');
1001
+ this.emitQueueUpdate(runtime);
1002
+ return true;
1003
+ }
1004
+ runtime.lastAutomaticAssistantFingerprint = fingerprint;
1005
+ return false;
1006
+ }
959
1007
  /**
960
1008
  * Apply a model selection recorded while a Build block was running. The
961
1009
  * in-flight block never switches mid-block; the switch takes effect the next
@@ -999,7 +1047,20 @@ class ConversationKernel {
999
1047
  tokens = await Promise.race([
1000
1048
  runtime.runner.process(message),
1001
1049
  new Promise((_, reject) => {
1002
- timeout = setTimeout(() => reject(new Error(`Process timeout (${Math.round(timeoutMs / 1000)}s)`)), timeoutMs);
1050
+ timeout = setTimeout(() => {
1051
+ // A timeout must cancel the provider/kernel operation as well as
1052
+ // reject the caller. Previously only the Promise.race rejected,
1053
+ // leaving the worker running in the background and making the
1054
+ // UI observe a later unexplained interruption.
1055
+ runtime.runner.abortActiveKernelRun(`process_timeout_${timeoutMs}ms`);
1056
+ runtime.runner.emitWorkEvent({
1057
+ type: 'error',
1058
+ content: `Process timeout (${Math.round(timeoutMs / 1000)}s); the run was aborted to prevent a stale worker from continuing.`,
1059
+ status: 'error',
1060
+ runId: runtime.runId,
1061
+ });
1062
+ reject(new Error(`Process timeout (${Math.round(timeoutMs / 1000)}s); run aborted and checkpointed`));
1063
+ }, timeoutMs);
1003
1064
  }),
1004
1065
  ]);
1005
1066
  }
@@ -1067,6 +1128,7 @@ class ConversationKernel {
1067
1128
  guideEnvelopes: new Map(),
1068
1129
  goalContinuationTimer: undefined,
1069
1130
  pendingContinuationRunId: undefined,
1131
+ lastAutomaticAssistantFingerprint: undefined,
1070
1132
  };
1071
1133
  runner.setGoalContinuationGate(() => {
1072
1134
  this.queueState(runtime);
@@ -1,4 +1,14 @@
1
1
  import type { DisplayImageAttachment } from './types';
2
+ export interface WorkspaceImageObservation {
3
+ path: string;
4
+ name: string;
5
+ byteLength: number;
6
+ dataUrl: string;
7
+ mimeType: DisplayImageAttachment['mimeType'];
8
+ width: number;
9
+ height: number;
10
+ }
11
+ export declare function readWorkspaceImageForVision(workspacePath: string, requestedPath: string): WorkspaceImageObservation;
2
12
  export declare function persistWorkspaceDisplayImage(rootPath: string, workspacePath: string, requestedPath: string, caption?: string, createdAt?: string): DisplayImageAttachment;
3
13
  export declare function hydrateDisplayImage(rootPath: string, input: unknown): DisplayImageAttachment | undefined;
4
14
  export declare function durableDisplayImage(input: DisplayImageAttachment | undefined): DisplayImageAttachment | undefined;
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.readWorkspaceImageForVision = readWorkspaceImageForVision;
36
37
  exports.persistWorkspaceDisplayImage = persistWorkspaceDisplayImage;
37
38
  exports.hydrateDisplayImage = hydrateDisplayImage;
38
39
  exports.durableDisplayImage = durableDisplayImage;
@@ -78,6 +79,27 @@ function decodeFile(filePath) {
78
79
  throw new Error('Display image extension does not match its decoded content.');
79
80
  return { bytes, dataUrl, mimeType, width: decoded.width, height: decoded.height };
80
81
  }
82
+ function readWorkspaceImageForVision(workspacePath, requestedPath) {
83
+ const workspace = fs.realpathSync(path.resolve(workspacePath));
84
+ const candidate = path.resolve(workspace, String(requestedPath || '').trim());
85
+ if (!inside(workspace, candidate))
86
+ throw new Error('Workspace images must stay inside the active workspace.');
87
+ if (!fs.existsSync(candidate) || !fs.statSync(candidate).isFile())
88
+ throw new Error(`Workspace image not found: ${requestedPath}`);
89
+ const realCandidate = fs.realpathSync(candidate);
90
+ if (!inside(workspace, realCandidate))
91
+ throw new Error('Workspace images must stay inside the active workspace.');
92
+ const decoded = decodeFile(realCandidate);
93
+ return {
94
+ path: path.relative(workspace, realCandidate).split(path.sep).join('/'),
95
+ name: path.basename(realCandidate),
96
+ byteLength: decoded.bytes.length,
97
+ dataUrl: decoded.dataUrl,
98
+ mimeType: decoded.mimeType,
99
+ width: decoded.width,
100
+ height: decoded.height,
101
+ };
102
+ }
81
103
  function writeAsset(filePath, bytes, sha256) {
82
104
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
83
105
  if (fs.existsSync(filePath)) {
@@ -105,14 +127,15 @@ function writeAsset(filePath, bytes, sha256) {
105
127
  }
106
128
  }
107
129
  function persistWorkspaceDisplayImage(rootPath, workspacePath, requestedPath, caption = '', createdAt = new Date().toISOString()) {
108
- const workspace = fs.realpathSync(path.resolve(workspacePath));
109
- const candidate = path.resolve(workspace, String(requestedPath || '').trim());
110
- if (!fs.existsSync(candidate) || !fs.statSync(candidate).isFile())
111
- throw new Error(`Display image not found: ${requestedPath}`);
112
- const realCandidate = fs.realpathSync(candidate);
113
- if (!inside(workspace, realCandidate))
114
- throw new Error('Display images must stay inside the active workspace.');
115
- const decoded = decodeFile(realCandidate);
130
+ const source = readWorkspaceImageForVision(workspacePath, requestedPath);
131
+ const realCandidate = path.resolve(fs.realpathSync(path.resolve(workspacePath)), ...source.path.split('/'));
132
+ const decoded = {
133
+ bytes: Buffer.from(source.dataUrl.slice(source.dataUrl.indexOf(',') + 1), 'base64'),
134
+ dataUrl: source.dataUrl,
135
+ mimeType: source.mimeType,
136
+ width: source.width,
137
+ height: source.height,
138
+ };
116
139
  const sha256 = crypto.createHash('sha256').update(decoded.bytes).digest('hex');
117
140
  writeAsset(absoluteAssetPath(rootPath, sha256, decoded.mimeType), decoded.bytes, sha256);
118
141
  return {
@@ -5,6 +5,7 @@ export interface ElectronBrowserUseHostOptions {
5
5
  resolveContents(scope: BrowserUseScope, boundContentsId?: number): Promise<WebContents>;
6
6
  openExternal?(url: string): void | Promise<void>;
7
7
  guardSettleMs?: number;
8
+ releaseContents?(scope: BrowserUseScope, contents: WebContents): void;
8
9
  }
9
10
  /**
10
11
  * Electron-owned Browser-Use page host. All model-independent DOM programs live in
@@ -25,6 +26,9 @@ export declare class ElectronBrowserUseHost {
25
26
  private page;
26
27
  private activeEffects;
27
28
  private isRuntimeBound;
29
+ private bindingKey;
30
+ private bindingMatchesScope;
31
+ private releaseBinding;
28
32
  private installDownloadGuard;
29
33
  }
30
34
  //# sourceMappingURL=electronBrowserUseHost.d.ts.map
@@ -59,22 +59,31 @@ class ElectronBrowserUseHost {
59
59
  });
60
60
  }
61
61
  async resolve(scope) {
62
- const boundId = this.runtimeBindings.get(scope.runtimeKey);
62
+ const bindingKey = this.bindingKey(scope);
63
+ const boundId = this.runtimeBindings.get(bindingKey);
63
64
  const contents = await this.options.resolveContents(scope, boundId);
64
65
  if (contents.isDestroyed())
65
66
  throw new Error('Built-in browser page is unavailable.');
66
67
  this.attach(contents);
67
- this.runtimeBindings.set(scope.runtimeKey, contents.id);
68
+ this.runtimeBindings.set(bindingKey, contents.id);
68
69
  return this.page(contents);
69
70
  }
70
71
  clear(scope) {
71
- if (scope)
72
- this.runtimeBindings.delete(scope.runtimeKey);
73
- else
72
+ if (!scope) {
73
+ for (const [bindingKey, contentsId] of this.runtimeBindings)
74
+ this.releaseBinding(bindingKey, contentsId);
74
75
  this.runtimeBindings.clear();
76
+ return;
77
+ }
78
+ for (const [bindingKey, contentsId] of this.runtimeBindings) {
79
+ if (!this.bindingMatchesScope(bindingKey, scope))
80
+ continue;
81
+ this.releaseBinding(bindingKey, contentsId, scope);
82
+ this.runtimeBindings.delete(bindingKey);
83
+ }
75
84
  }
76
85
  dispose() {
77
- this.runtimeBindings.clear();
86
+ this.clear();
78
87
  for (const [browserSession, handler] of this.downloadHandlers) {
79
88
  browserSession.removeListener('will-download', handler);
80
89
  }
@@ -193,6 +202,23 @@ class ElectronBrowserUseHost {
193
202
  }
194
203
  return false;
195
204
  }
205
+ bindingKey(scope) {
206
+ return `${scope.runtimeKey}\u0000${scope.visible === false ? 'background' : 'visible'}`;
207
+ }
208
+ bindingMatchesScope(bindingKey, scope) {
209
+ const prefix = `${scope.runtimeKey}\u0000`;
210
+ return bindingKey.startsWith(prefix)
211
+ && (scope.visible === undefined || bindingKey === this.bindingKey({ ...scope, visible: scope.visible !== false }));
212
+ }
213
+ releaseBinding(bindingKey, contentsId, scope) {
214
+ const contents = this.pages.get(contentsId)?.contents;
215
+ if (!contents || contents.isDestroyed())
216
+ return;
217
+ const separator = bindingKey.lastIndexOf('\u0000');
218
+ const runtimeKey = separator >= 0 ? bindingKey.slice(0, separator) : bindingKey;
219
+ const visible = separator < 0 || bindingKey.slice(separator + 1) !== 'background';
220
+ this.options.releaseContents?.({ owner: scope?.owner || '', runtimeKey, visible }, contents);
221
+ }
196
222
  installDownloadGuard(browserSession) {
197
223
  if (this.downloadHandlers.has(browserSession))
198
224
  return;
@@ -73,6 +73,7 @@ export declare class ElectronUtilityAgentClient {
73
73
  private childRootIdentity;
74
74
  private sequence;
75
75
  private lastError;
76
+ private expectedExit;
76
77
  private restartQuarantine;
77
78
  constructor(root: string, hostScript: string, target: NormalizedConversationTarget, options?: ElectronUtilityAgentClientOptions);
78
79
  subscribe(listener: (event: AgentWorkEvent) => void): () => void;
@@ -49,6 +49,8 @@ const fs = __importStar(require("fs"));
49
49
  const path = __importStar(require("path"));
50
50
  const electron_1 = require("electron");
51
51
  const child_process_1 = require("child_process");
52
+ const runtimeDiagnostics_1 = require("./runtimeDiagnostics");
53
+ const runtimeLifecycle_1 = require("./runtimeLifecycle");
52
54
  const conversationTarget_1 = require("./conversationTarget");
53
55
  // PowerShell startup can be slow on a cold or busy Windows host even though
54
56
  // the precompiled Toolhelp helper itself is healthy.
@@ -882,6 +884,7 @@ class ElectronUtilityAgentClient {
882
884
  childRootIdentity = null;
883
885
  sequence = 0;
884
886
  lastError = '';
887
+ expectedExit = false;
885
888
  // A failed force-stop means an old descendant may still own target-scoped
886
889
  // resources. This is intentionally sticky for the lifetime of this client:
887
890
  // only rebuilding the Electron main-process runtime pool may clear it.
@@ -955,6 +958,11 @@ class ElectronUtilityAgentClient {
955
958
  const generation = ++this.childGeneration;
956
959
  this.readyGeneration = 0;
957
960
  this.lastError = '';
961
+ this.expectedExit = false;
962
+ (0, runtimeDiagnostics_1.appendRuntimeDiagnostic)(this.root, {
963
+ event: 'utility_runtime_started', runtimeKey: this.target.runtimeKey,
964
+ generation, pid: Number(child.pid || 0),
965
+ });
958
966
  child.on('message', message => this.handleMessage(child, generation, message));
959
967
  child.on('error', (_type, _location, report) => {
960
968
  if (this.child === child)
@@ -1078,6 +1086,7 @@ class ElectronUtilityAgentClient {
1078
1086
  const child = this.child;
1079
1087
  if (!child)
1080
1088
  return;
1089
+ this.expectedExit = true;
1081
1090
  if (!this.restartQuarantine && this.readyGeneration === this.childGeneration) {
1082
1091
  try {
1083
1092
  await this.request('shutdown', undefined, 2_000);
@@ -1337,7 +1346,13 @@ class ElectronUtilityAgentClient {
1337
1346
  return;
1338
1347
  let result;
1339
1348
  const controller = new AbortController();
1340
- this.hostToolRuns.set(request.requestId, { generation, controller });
1349
+ const run = { generation, controller, tool: String(request.tool), startedAt: Date.now(), stage: 'received' };
1350
+ this.hostToolRuns.set(request.requestId, run);
1351
+ (0, runtimeDiagnostics_1.appendRuntimeDiagnostic)(this.root, {
1352
+ event: 'utility_host_rpc_started', runtimeKey: this.target.runtimeKey,
1353
+ generation, pid: Number(child.pid || 0), requestId: request.requestId,
1354
+ tool: request.tool, stage: run.stage,
1355
+ });
1341
1356
  const allowed = new Set(['browser_control', 'browser_use', 'screen_capture', 'computer_use', 'automation', 'terminal_takeover']);
1342
1357
  if (!allowed.has(request.tool)) {
1343
1358
  result = { requestId: request.requestId, ok: false, error: `Electron host tool is not allowed: ${String(request.tool)}` };
@@ -1350,13 +1365,26 @@ class ElectronUtilityAgentClient {
1350
1365
  }
1351
1366
  else {
1352
1367
  try {
1368
+ const active = this.hostToolRuns.get(request.requestId);
1369
+ if (active)
1370
+ active.stage = 'running';
1353
1371
  result = { requestId: request.requestId, ok: true, result: await this.hostToolHandler(request, controller.signal) };
1354
1372
  }
1355
1373
  catch (error) {
1356
1374
  result = { requestId: request.requestId, ok: false, error: error instanceof Error ? error.message : String(error) };
1357
1375
  }
1358
1376
  }
1377
+ const active = this.hostToolRuns.get(request.requestId);
1378
+ if (active)
1379
+ active.stage = 'responding';
1359
1380
  this.hostToolRuns.delete(request.requestId);
1381
+ (0, runtimeDiagnostics_1.appendRuntimeDiagnostic)(this.root, {
1382
+ event: result.ok ? 'utility_host_rpc_completed' : 'utility_host_rpc_failed',
1383
+ level: result.ok ? 'info' : 'warn', runtimeKey: this.target.runtimeKey,
1384
+ generation, pid: Number(child.pid || 0), requestId: request.requestId,
1385
+ tool: request.tool, stage: active?.stage || 'responding',
1386
+ durationMs: Date.now() - run.startedAt, error: result.ok ? '' : result.error,
1387
+ });
1360
1388
  if (controller.signal.aborted
1361
1389
  || this.restartQuarantine
1362
1390
  || this.invalidGenerations.has(generation)
@@ -1377,8 +1405,48 @@ class ElectronUtilityAgentClient {
1377
1405
  handleExit(child, code) {
1378
1406
  if (this.child !== child)
1379
1407
  return;
1380
- const error = new Error(`Electron utility runtime exited (${code}): ${this.lastError || 'no stderr'}`);
1408
+ const expected = this.expectedExit || !!this.restartQuarantine;
1409
+ const lastHostRun = [...this.hostToolRuns.entries()]
1410
+ .filter(([, run]) => run.generation === this.childGeneration)
1411
+ .sort((left, right) => right[1].startedAt - left[1].startedAt)[0];
1412
+ const hostSuffix = lastHostRun
1413
+ ? `; last host RPC ${lastHostRun[1].tool}/${lastHostRun[1].stage} (${Date.now() - lastHostRun[1].startedAt} ms)`
1414
+ : '';
1415
+ const error = new Error(`Electron utility runtime exited (${code}): ${this.lastError || 'no stderr'}${hostSuffix}`);
1416
+ (0, runtimeDiagnostics_1.appendRuntimeDiagnostic)(this.root, {
1417
+ event: expected ? 'utility_runtime_stopped' : 'utility_runtime_unexpected_exit',
1418
+ level: expected ? 'info' : 'error', runtimeKey: this.target.runtimeKey,
1419
+ generation: this.childGeneration, pid: Number(child.pid || 0), exitCode: code,
1420
+ expected, requestId: lastHostRun?.[0], tool: lastHostRun?.[1].tool,
1421
+ stage: lastHostRun?.[1].stage, durationMs: lastHostRun ? Date.now() - lastHostRun[1].startedAt : 0,
1422
+ error: this.lastError || (expected ? '' : 'no stderr'),
1423
+ });
1424
+ (0, runtimeLifecycle_1.markRuntimeLifecycleExitedByPid)(this.root, 'utility', Number(child.pid || 0), {
1425
+ unexpected: !expected, exitCode: code, error: this.lastError || '',
1426
+ });
1427
+ // Surface unexpected worker death as a target-scoped terminal error. A
1428
+ // silent child exit used to leave the renderer with a generic interrupted
1429
+ // state and no explanation of whether the provider, runtime, or process
1430
+ // supervisor was responsible.
1431
+ if (!expected && !this.restartQuarantine) {
1432
+ const event = {
1433
+ id: `utility-runtime-exit-${process.pid}-${Date.now()}`,
1434
+ conversationId: this.target.conversationId,
1435
+ type: 'error',
1436
+ content: error.message,
1437
+ mode: 'build',
1438
+ model: '',
1439
+ timestamp: new Date().toISOString(),
1440
+ workspaceId: this.target.workspaceId,
1441
+ workspaceKey: this.target.workspaceKey,
1442
+ runtimeKey: this.target.runtimeKey,
1443
+ status: 'error',
1444
+ };
1445
+ for (const listener of this.listeners)
1446
+ listener(event);
1447
+ }
1381
1448
  this.detachChild(child, error);
1449
+ this.expectedExit = false;
1382
1450
  }
1383
1451
  detachChild(child, error) {
1384
1452
  if (this.child !== child)
@@ -0,0 +1,18 @@
1
+ export type RuntimeDiagnosticLevel = 'info' | 'warn' | 'error';
2
+ export interface RuntimeDiagnosticEvent {
3
+ event: string;
4
+ level?: RuntimeDiagnosticLevel;
5
+ runtimeKey?: string;
6
+ generation?: number;
7
+ pid?: number;
8
+ requestId?: string;
9
+ tool?: string;
10
+ stage?: string;
11
+ durationMs?: number;
12
+ exitCode?: number | null;
13
+ expected?: boolean;
14
+ error?: string;
15
+ }
16
+ /** Append one bounded, path-free JSON event. Diagnostics must never break runtime work. */
17
+ export declare function appendRuntimeDiagnostic(root: string, input: RuntimeDiagnosticEvent): void;
18
+ //# sourceMappingURL=runtimeDiagnostics.d.ts.map
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.appendRuntimeDiagnostic = appendRuntimeDiagnostic;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const crypto_1 = require("crypto");
40
+ const MAX_LOG_BYTES = 2 * 1024 * 1024;
41
+ function boundedText(value, limit = 800) {
42
+ return String(value || '')
43
+ .replace(/(?:[A-Za-z]:[\\/]|\\\\)[^\s"']+/g, '[local-path]')
44
+ .replace(/\b(?:sk|ghp|github_pat)-?[A-Za-z0-9_.-]{8,}\b/g, '[redacted]')
45
+ .replace(/(Authorization\s*:\s*Bearer\s+)[^\s,;]+/ig, '$1[redacted]')
46
+ .slice(-limit);
47
+ }
48
+ function runtimeCorrelation(runtimeKey) {
49
+ return runtimeKey
50
+ ? (0, crypto_1.createHash)('sha256').update(runtimeKey).digest('hex').slice(0, 16)
51
+ : '';
52
+ }
53
+ /** Append one bounded, path-free JSON event. Diagnostics must never break runtime work. */
54
+ function appendRuntimeDiagnostic(root, input) {
55
+ try {
56
+ const directory = path.join(root, '.newmark-runtime');
57
+ const file = path.join(directory, 'runtime-events.jsonl');
58
+ fs.mkdirSync(directory, { recursive: true });
59
+ try {
60
+ if (fs.statSync(file).size > MAX_LOG_BYTES) {
61
+ const previous = `${file}.1`;
62
+ try {
63
+ fs.unlinkSync(previous);
64
+ }
65
+ catch { }
66
+ fs.renameSync(file, previous);
67
+ }
68
+ }
69
+ catch { }
70
+ const record = {
71
+ at: new Date().toISOString(),
72
+ event: boundedText(input.event, 100),
73
+ level: input.level || 'info',
74
+ runtime: runtimeCorrelation(String(input.runtimeKey || '')),
75
+ generation: Math.max(0, Math.floor(Number(input.generation) || 0)),
76
+ pid: Math.max(0, Math.floor(Number(input.pid) || 0)),
77
+ requestId: boundedText(input.requestId, 160),
78
+ tool: boundedText(input.tool, 80),
79
+ stage: boundedText(input.stage, 80),
80
+ durationMs: Math.max(0, Math.floor(Number(input.durationMs) || 0)),
81
+ exitCode: input.exitCode === null ? null : Number.isFinite(Number(input.exitCode)) ? Number(input.exitCode) : undefined,
82
+ expected: input.expected,
83
+ error: boundedText(input.error),
84
+ };
85
+ fs.appendFileSync(file, `${JSON.stringify(record)}\n`, 'utf-8');
86
+ }
87
+ catch { }
88
+ }
89
+ //# sourceMappingURL=runtimeDiagnostics.js.map
@@ -6,7 +6,7 @@ export interface RuntimeLifecycleState {
6
6
  startedAt: string;
7
7
  previousOwnerAlive: boolean;
8
8
  unexpectedExit: boolean;
9
- active: true;
9
+ active: boolean;
10
10
  }
11
11
  export declare function isRuntimeProcessAlive(pid: number): boolean;
12
12
  /** Prepare crash-recovery markers asynchronously after the startup shell is visible. */
@@ -21,5 +21,11 @@ export declare function prepareRuntimeLifecycle(root: string, role?: RuntimeLife
21
21
  export declare function beginRuntimeLifecycle(root: string, role?: RuntimeLifecycleRole): RuntimeLifecycleState;
22
22
  /** Mark only this owner clean; a hard kill leaves the active marker intact. */
23
23
  export declare function markRuntimeLifecycleClean(root: string, role?: RuntimeLifecycleRole): void;
24
+ /** Parent-side fallback for a worker that died before it could clean its own marker. */
25
+ export declare function markRuntimeLifecycleExitedByPid(root: string, role: RuntimeLifecycleRole, pid: number, input?: {
26
+ unexpected: boolean;
27
+ exitCode?: number | null;
28
+ error?: string;
29
+ }): void;
24
30
  export declare function runtimeLifecycleState(root: string, role?: RuntimeLifecycleRole): RuntimeLifecycleState | null;
25
31
  //# sourceMappingURL=runtimeLifecycle.d.ts.map