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.
- package/dist/conversation-utility-host.bundle.cjs +75356 -26589
- package/dist/core/agent.d.ts +43 -18
- package/dist/core/agent.js +321 -69
- package/dist/core/browserUse.d.ts +7 -0
- package/dist/core/browserUse.js +24 -7
- package/dist/core/conversationKernel.d.ts +4 -0
- package/dist/core/conversationKernel.js +65 -3
- package/dist/core/displayImages.d.ts +10 -0
- package/dist/core/displayImages.js +31 -8
- package/dist/core/electronBrowserUseHost.d.ts +4 -0
- package/dist/core/electronBrowserUseHost.js +32 -6
- package/dist/core/electronUtilityAgentClient.d.ts +1 -0
- package/dist/core/electronUtilityAgentClient.js +70 -2
- package/dist/core/runtimeDiagnostics.d.ts +18 -0
- package/dist/core/runtimeDiagnostics.js +89 -0
- package/dist/core/runtimeLifecycle.d.ts +7 -1
- package/dist/core/runtimeLifecycle.js +29 -0
- package/dist/core/searchMcpPool.d.ts +80 -0
- package/dist/core/searchMcpPool.js +419 -0
- package/dist/main.js +72 -1
- package/dist/server.js +35 -0
- package/dist/tools/index.d.ts +17 -1
- package/dist/tools/index.js +203 -64
- package/dist/tui/src/data.js +2 -2
- package/dist/ui/index.html +208 -26
- package/dist/wsl-agent-host.bundle.cjs +75333 -26566
- package/package.json +11 -2
|
@@ -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;
|
package/dist/core/browserUse.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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(() =>
|
|
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
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
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(
|
|
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
|
|
73
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|