newmark-agent 0.3.8 → 0.3.11

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.
Files changed (36) hide show
  1. package/dist/cli-help.d.ts +2 -0
  2. package/dist/cli-help.js +22 -0
  3. package/dist/context/domain/types.d.ts +1 -1
  4. package/dist/conversation-utility-host.bundle.cjs +2060 -774
  5. package/dist/conversation-utility-host.js +4 -1
  6. package/dist/core/agent.d.ts +23 -1
  7. package/dist/core/agent.js +517 -37
  8. package/dist/core/agentKernelRunner.js +20 -3
  9. package/dist/core/browserControl.d.ts +8 -0
  10. package/dist/core/browserUsePageAdapter.d.ts +3 -0
  11. package/dist/core/browserUsePageAdapter.js +19 -2
  12. package/dist/core/compressionHistoryArchive.d.ts +28 -0
  13. package/dist/core/compressionHistoryArchive.js +131 -0
  14. package/dist/core/computerUseSession.d.ts +44 -0
  15. package/dist/core/computerUseSession.js +105 -0
  16. package/dist/core/config.js +1 -0
  17. package/dist/core/conversationKernel.d.ts +3 -1
  18. package/dist/core/conversationKernel.js +76 -7
  19. package/dist/core/electronBrowserUseHost.js +16 -0
  20. package/dist/core/electronUtilityAgentClient.js +84 -2
  21. package/dist/core/electronUtilityRuntimePool.js +6 -7
  22. package/dist/core/runtimeLifecycle.d.ts +23 -0
  23. package/dist/core/runtimeLifecycle.js +146 -0
  24. package/dist/core/types.d.ts +3 -0
  25. package/dist/core/utilityHostToolRouter.d.ts +7 -0
  26. package/dist/core/utilityHostToolRouter.js +25 -33
  27. package/dist/core/wslAgentRuntimePool.js +9 -10
  28. package/dist/launcher.js +13 -1
  29. package/dist/main.js +147 -18
  30. package/dist/preload.js +4 -2
  31. package/dist/tools/index.js +42 -52
  32. package/dist/tools/nativeTools.js +1 -1
  33. package/dist/ui/index.html +296 -40
  34. package/dist/wsl-agent-host.bundle.cjs +2063 -776
  35. package/dist/wsl-agent-host.js +4 -0
  36. package/package.json +3 -3
package/dist/main.js CHANGED
@@ -70,8 +70,10 @@ const wslAgentRuntimePool_1 = require("./core/wslAgentRuntimePool");
70
70
  const utilityHostToolRouter_1 = require("./core/utilityHostToolRouter");
71
71
  const startupPrewarm_1 = require("./core/startupPrewarm");
72
72
  const runtimeShutdown_1 = require("./core/runtimeShutdown");
73
+ const runtimeLifecycle_1 = require("./core/runtimeLifecycle");
73
74
  const compat_1 = require("./core/compat");
74
75
  const mcpManager_1 = require("./core/mcpManager");
76
+ const cli_help_1 = require("./cli-help");
75
77
  const APP_NAME = 'Newmark Agent';
76
78
  const APP_ID = 'ai.newmark.agent';
77
79
  const CONFIG_RELOAD_RUNTIME_TIMEOUT_MS = 6_000;
@@ -115,7 +117,27 @@ let _forceQuit = false;
115
117
  let forcedExitTimer = null;
116
118
  let electronBrowserUseHost = null;
117
119
  let browserUseEngine = null;
120
+ // A single renderer can host several hidden conversation-bound guests. The
121
+ // legacy host map is retained for focused-window discovery, while this map is
122
+ // the authoritative Browser-Use/right-sidebar binding.
118
123
  const browserGuestContentsByHost = new Map();
124
+ const browserGuestBindingsByRuntime = new Map();
125
+ function browserGuestRuntimeKey(target) {
126
+ return (0, conversationTarget_1.normalizeConversationTarget)(target).runtimeKey;
127
+ }
128
+ function browserGuestBindingFor(runtimeKey) {
129
+ const binding = browserGuestBindingsByRuntime.get(String(runtimeKey || ''));
130
+ if (!binding)
131
+ return null;
132
+ const guest = electron_1.webContents.fromId(binding.guestId);
133
+ if (!guest || guest.isDestroyed() || guest.getType() !== 'webview' || guest.hostWebContents?.id !== binding.hostId) {
134
+ browserGuestBindingsByRuntime.delete(String(runtimeKey || ''));
135
+ if (browserGuestContentsByHost.get(binding.hostId) === binding.guestId)
136
+ browserGuestContentsByHost.delete(binding.hostId);
137
+ return null;
138
+ }
139
+ return binding;
140
+ }
119
141
  let workspaceSwitchGeneration = 0;
120
142
  let workspaceSelectionCoordinator = null;
121
143
  function defaultTerminalShell() {
@@ -736,7 +758,15 @@ async function waitForWebContentsLoad(contents, timeoutMs = 15000) {
736
758
  contents.once('did-fail-load', finish);
737
759
  });
738
760
  }
739
- function registeredBrowserGuest(hostContentsId) {
761
+ function registeredBrowserGuest(hostContentsId, runtimeKey) {
762
+ if (runtimeKey) {
763
+ const binding = browserGuestBindingFor(runtimeKey);
764
+ if (binding && (!hostContentsId || binding.hostId === hostContentsId)) {
765
+ const guest = electron_1.webContents.fromId(binding.guestId);
766
+ if (guest && !guest.isDestroyed())
767
+ return guest;
768
+ }
769
+ }
740
770
  const hostIds = hostContentsId
741
771
  ? [hostContentsId]
742
772
  : [
@@ -751,8 +781,14 @@ function registeredBrowserGuest(hostContentsId) {
751
781
  if (!guestId)
752
782
  continue;
753
783
  const guest = electron_1.webContents.fromId(guestId);
754
- if (guest && !guest.isDestroyed() && guest.getType() === 'webview' && guest.hostWebContents?.id === hostId)
784
+ if (guest && !guest.isDestroyed() && guest.getType() === 'webview' && guest.hostWebContents?.id === hostId) {
785
+ if (runtimeKey) {
786
+ const bound = browserGuestBindingsByRuntime.get(runtimeKey);
787
+ if (!bound || bound.hostId !== hostId || bound.guestId !== guest.id)
788
+ continue;
789
+ }
755
790
  return guest;
791
+ }
756
792
  browserGuestContentsByHost.delete(hostId);
757
793
  }
758
794
  return null;
@@ -773,37 +809,64 @@ async function boundedOperation(operation, timeoutMs, label) {
773
809
  clearTimeout(timer);
774
810
  }
775
811
  }
776
- function registerBrowserGuest(host, guest) {
812
+ function registerBrowserGuest(host, guest, requestedTarget) {
777
813
  if (host.isDestroyed() || guest.isDestroyed() || guest.getType() !== 'webview' || guest.hostWebContents?.id !== host.id)
778
814
  return false;
815
+ let target;
816
+ try {
817
+ target = (0, conversationTarget_1.normalizeConversationTarget)(conversationRuntimeTarget(requestedTarget));
818
+ }
819
+ catch {
820
+ return false;
821
+ }
822
+ const prior = browserGuestBindingsByRuntime.get(target.runtimeKey);
823
+ if (prior && prior.guestId !== guest.id) {
824
+ const priorGuest = electron_1.webContents.fromId(prior.guestId);
825
+ if (priorGuest && !priorGuest.isDestroyed())
826
+ priorGuest.hostWebContents?.send('browser:guestVisibility', { runtimeKey: target.runtimeKey, visible: false });
827
+ }
828
+ browserGuestBindingsByRuntime.set(target.runtimeKey, {
829
+ hostId: host.id,
830
+ guestId: guest.id,
831
+ workspaceId: target.workspaceId,
832
+ conversationId: target.conversationId,
833
+ });
779
834
  browserGuestContentsByHost.set(host.id, guest.id);
780
835
  guest.once('destroyed', () => {
781
836
  if (browserGuestContentsByHost.get(host.id) === guest.id)
782
837
  browserGuestContentsByHost.delete(host.id);
838
+ const binding = browserGuestBindingsByRuntime.get(target.runtimeKey);
839
+ if (binding?.guestId === guest.id)
840
+ browserGuestBindingsByRuntime.delete(target.runtimeKey);
783
841
  });
784
842
  ensureElectronBrowserUseHost().attach(guest);
785
843
  return true;
786
844
  }
787
- async function waitForRegisteredBrowserGuest(host, timeoutMs = 12_000) {
845
+ async function waitForRegisteredBrowserGuest(host, runtimeKey, timeoutMs = 12_000) {
788
846
  const deadline = Date.now() + Math.max(250, timeoutMs);
789
847
  while (!host.isDestroyed() && Date.now() < deadline) {
790
- const guest = registeredBrowserGuest(host.id);
848
+ const guest = registeredBrowserGuest(host.id, runtimeKey);
791
849
  if (guest)
792
850
  return guest;
793
851
  await new Promise(resolve => setTimeout(resolve, 25));
794
852
  }
795
853
  throw new Error('Built-in Browser guest did not become ready before the Browser-Use timeout');
796
854
  }
797
- async function ensureBrowserWebContents(boundContentsId) {
855
+ async function ensureBrowserWebContents(boundContentsId, runtimeKey) {
798
856
  if (boundContentsId) {
799
857
  const bound = electron_1.webContents.fromId(boundContentsId);
800
858
  if (bound && !bound.isDestroyed() && bound.getType() === 'webview'
801
859
  && browserGuestContentsByHost.get(bound.hostWebContents?.id || 0) === bound.id) {
860
+ if (runtimeKey) {
861
+ const binding = browserGuestBindingsByRuntime.get(runtimeKey);
862
+ if (!binding || binding.guestId !== bound.id)
863
+ return await ensureBrowserWebContents(undefined, runtimeKey);
864
+ }
802
865
  bound.hostWebContents?.send('browser:ensureGuest');
803
866
  return bound;
804
867
  }
805
868
  }
806
- const registered = registeredBrowserGuest();
869
+ const registered = registeredBrowserGuest(undefined, runtimeKey);
807
870
  if (registered) {
808
871
  registered.hostWebContents?.send('browser:ensureGuest');
809
872
  return registered;
@@ -813,13 +876,13 @@ async function ensureBrowserWebContents(boundContentsId) {
813
876
  const host = hostWindow?.webContents;
814
877
  if (!host || host.isDestroyed())
815
878
  throw new Error('Built-in Browser UI is unavailable');
816
- host.send('browser:ensureGuest');
817
- return await waitForRegisteredBrowserGuest(host);
879
+ host.send('browser:ensureGuest', runtimeKey ? { runtimeKey } : undefined);
880
+ return await waitForRegisteredBrowserGuest(host, runtimeKey, 12_000);
818
881
  }
819
882
  function ensureElectronBrowserUseHost() {
820
883
  if (!electronBrowserUseHost) {
821
884
  electronBrowserUseHost = new electronBrowserUseHost_1.ElectronBrowserUseHost({
822
- resolveContents: async (_scope, boundContentsId) => await ensureBrowserWebContents(boundContentsId),
885
+ resolveContents: async (scope, boundContentsId) => await ensureBrowserWebContents(boundContentsId, scope.runtimeKey),
823
886
  openExternal: async (url) => { await electron_1.shell.openExternal(url); },
824
887
  });
825
888
  }
@@ -832,10 +895,18 @@ function ensureBrowserUseEngine() {
832
895
  }
833
896
  return browserUseEngine;
834
897
  }
835
- function currentBrowserUseContext() {
836
- const target = (0, conversationTarget_1.normalizeConversationTarget)(conversationRuntimeTarget());
898
+ function currentBrowserUseContext(requestedTarget) {
899
+ const target = (0, conversationTarget_1.normalizeConversationTarget)(conversationRuntimeTarget(requestedTarget));
837
900
  return { runtimeKey: target.runtimeKey, actorId: agent?.runtimeActorId || terminalTakeover_1.ROOT_TERMINAL_ACTOR_ID };
838
901
  }
902
+ function browserControlTargetInput(input) {
903
+ if (!input)
904
+ return undefined;
905
+ return {
906
+ workspaceId: String(input.workspaceId || ''),
907
+ conversationId: String(input.conversationId || 'default'),
908
+ };
909
+ }
839
910
  async function runBoundBrowserUse(input, context, signal) {
840
911
  return await ensureBrowserUseEngine().run((0, browserUse_1.bindBrowserUseRequest)(input, context), signal);
841
912
  }
@@ -855,8 +926,10 @@ function browserSnapshotScript(maxChars) {
855
926
  }
856
927
  async function runBrowserControl(request) {
857
928
  const action = request.action;
929
+ const requestedTarget = browserControlTargetInput(request.target);
930
+ const target = (0, conversationTarget_1.normalizeConversationTarget)(conversationRuntimeTarget(requestedTarget));
858
931
  if (action === 'use') {
859
- const receipt = await runBoundBrowserUse(request.browserUse, currentBrowserUseContext());
932
+ const receipt = await runBoundBrowserUse(request.browserUse, currentBrowserUseContext(requestedTarget));
860
933
  return {
861
934
  ok: receipt.ok,
862
935
  action,
@@ -867,7 +940,7 @@ async function runBrowserControl(request) {
867
940
  error: receipt.error,
868
941
  };
869
942
  }
870
- const contents = await ensureBrowserWebContents();
943
+ const contents = await ensureBrowserWebContents(undefined, target.runtimeKey);
871
944
  try {
872
945
  if (action === 'open') {
873
946
  await contents.loadURL(request.url || 'about:blank');
@@ -952,12 +1025,25 @@ function installBrowserControlBackend() {
952
1025
  const args = userArgs();
953
1026
  const command = args.find(a => a === 'flow' || a === 'edit');
954
1027
  const isTuiArg = args.some(arg => arg.toLowerCase() === '--tui');
1028
+ const hasCliCommand = args.some(a => cli_commands_1.CLI_COMMANDS.includes(a));
1029
+ const isHelpArg = !hasCliCommand && (args.some(arg => ['--help', '-h'].includes(arg.toLowerCase())) || args[0]?.toLowerCase() === 'help');
1030
+ const isVersionArg = !hasCliCommand && args.some(arg => ['--version', '-v'].includes(arg.toLowerCase()));
955
1031
  const isViewerArg = args.some(arg => arg.toLowerCase() === '--newmark-viewer');
956
1032
  const isCliArg = args.includes('--cli');
957
1033
  const isServerArg = args.includes('--server');
958
1034
  const isFlowArg = command === 'flow';
959
1035
  const isEditArg = command === 'edit';
960
- const hasCliCommand = args.some(a => cli_commands_1.CLI_COMMANDS.includes(a));
1036
+ // Help/version are terminating discovery commands. They must be handled
1037
+ // before Electron's GUI/TUI/server branches can initialize runtime state or
1038
+ // spawn a window, so a product-new tester can use them safely in any surface.
1039
+ if (isHelpArg) {
1040
+ console.log((0, cli_help_1.newmarkHelpText)((0, installUpdate_1.currentAppVersion)()));
1041
+ process.exit(0);
1042
+ }
1043
+ if (isVersionArg) {
1044
+ console.log((0, installUpdate_1.currentAppVersion)());
1045
+ process.exit(0);
1046
+ }
961
1047
  function viewerEscape(value) {
962
1048
  return String(value || '').replace(/[&<>"']/g, character => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[character] || character);
963
1049
  }
@@ -1512,7 +1598,12 @@ else {
1512
1598
  rejectUiReadinessById(Number(fileRouterOwnerId), new Error('Startup window closed before UI readiness'));
1513
1599
  fileRouter.revokeOwner(fileRouterOwnerId);
1514
1600
  pdfPreviewServer.revokeOwner(fileRouterOwnerId);
1515
- browserGuestContentsByHost.delete(Number(fileRouterOwnerId));
1601
+ const closedHostId = Number(fileRouterOwnerId);
1602
+ browserGuestContentsByHost.delete(closedHostId);
1603
+ for (const [runtimeKey, binding] of browserGuestBindingsByRuntime) {
1604
+ if (binding.hostId === closedHostId)
1605
+ browserGuestBindingsByRuntime.delete(runtimeKey);
1606
+ }
1516
1607
  const remaining = electron_1.BrowserWindow.getAllWindows().filter(candidate => !candidate.isDestroyed() && candidate !== win);
1517
1608
  if (mainWindow === win)
1518
1609
  mainWindow = remaining[0] || null;
@@ -2106,6 +2197,15 @@ else {
2106
2197
  }
2107
2198
  electron_1.app.quit();
2108
2199
  };
2200
+ const hasUnsettledGoalOrFlow = () => {
2201
+ const goalRunning = !!agent?.goal && !agent.goal.paused;
2202
+ const flowRunning = Array.from(activeFlowsByRuntimeKey.values()).some(state => !!state.abortController);
2203
+ return goalRunning || flowRunning;
2204
+ };
2205
+ const markMainLifecycleCleanIfSafe = () => {
2206
+ if (!hasUnsettledGoalOrFlow())
2207
+ (0, runtimeLifecycle_1.markRuntimeLifecycleClean)(root, 'main');
2208
+ };
2109
2209
  electron_1.app.on('will-quit', event => {
2110
2210
  if (_forceQuit)
2111
2211
  armForcedExitDeadline('will-quit');
@@ -2131,6 +2231,7 @@ else {
2131
2231
  throw error;
2132
2232
  })
2133
2233
  : undefined;
2234
+ let shutdownFailed = false;
2134
2235
  void (0, runtimeShutdown_1.runRuntimeShutdownBarrier)({
2135
2236
  operations: [
2136
2237
  legacyStop,
@@ -2139,9 +2240,12 @@ else {
2139
2240
  ],
2140
2241
  shutdownHelpers: async () => await (0, electronUtilityAgentClient_1.shutdownWindowsProcessHelpers)(2_000),
2141
2242
  }).catch(error => {
2243
+ shutdownFailed = true;
2142
2244
  console.error('[Newmark] Runtime shutdown cleanup failed:', error instanceof Error ? error.message : String(error));
2143
2245
  }).finally(() => {
2144
2246
  appExitCleanupComplete = true;
2247
+ if (!shutdownFailed)
2248
+ markMainLifecycleCleanIfSafe();
2145
2249
  electron_1.app.quit();
2146
2250
  });
2147
2251
  }
@@ -2165,11 +2269,13 @@ else {
2165
2269
  electronBrowserUseHost = null;
2166
2270
  browserControl_1.BrowserControl.setBackend(null);
2167
2271
  browserGuestContentsByHost.clear();
2272
+ browserGuestBindingsByRuntime.clear();
2168
2273
  if (sidecarProcess) {
2169
2274
  sidecarProcess.kill();
2170
2275
  sidecarProcess = null;
2171
2276
  }
2172
2277
  (0, terminalTakeover_1.shutdownTerminalTakeoverSessions)('app-exit');
2278
+ markMainLifecycleCleanIfSafe();
2173
2279
  if (forcedExitTimer) {
2174
2280
  clearTimeout(forcedExitTimer);
2175
2281
  forcedExitTimer = null;
@@ -2554,9 +2660,9 @@ else {
2554
2660
  }
2555
2661
  }
2556
2662
  });
2557
- electron_1.ipcMain.handle('browser:registerGuest', (event, guestContentsId) => {
2663
+ electron_1.ipcMain.handle('browser:registerGuest', (event, guestContentsId, target) => {
2558
2664
  const guest = electron_1.webContents.fromId(Number(guestContentsId || 0));
2559
- return { accepted: !!guest && registerBrowserGuest(event.sender, guest) };
2665
+ return { accepted: !!guest && registerBrowserGuest(event.sender, guest, target) };
2560
2666
  });
2561
2667
  electron_1.ipcMain.handle('browser:control', async (_event, request) => {
2562
2668
  return await browserControl_1.BrowserControl.run(request);
@@ -2940,6 +3046,28 @@ else {
2940
3046
  runtimeDeferred: false,
2941
3047
  };
2942
3048
  });
3049
+ electron_1.ipcMain.handle('agent:computerUseState', async (_event, targetInput) => {
3050
+ if (!agent)
3051
+ return { enabled: false, occupied: false, runtimeKey: '' };
3052
+ const target = (0, conversationTarget_1.normalizeConversationTarget)(conversationRuntimeTarget(targetInput || agent.activeConversationId || 'default'));
3053
+ return utilityHostToolHandler.computerUseState(target.runtimeKey);
3054
+ });
3055
+ electron_1.ipcMain.handle('agent:setComputerUseEnabled', async (_event, targetInput, enabled) => {
3056
+ if (!agent)
3057
+ return { ok: false, error: 'Agent not initialized' };
3058
+ const target = (0, conversationTarget_1.normalizeConversationTarget)(conversationRuntimeTarget(targetInput || agent.activeConversationId || 'default'));
3059
+ const result = utilityHostToolHandler.setComputerUseEnabled(target.runtimeKey, enabled !== false, `conversation:${target.conversationId}`);
3060
+ if (!result.ok)
3061
+ return result;
3062
+ if (enabled === false) {
3063
+ try {
3064
+ const { runComputerUse } = require('./tools/computerUse');
3065
+ await runComputerUse({ action: 'takeover_stop', workspacePath: target.workspace?.path || root, ownerId: target.runtimeKey, invocation: 'agent' });
3066
+ }
3067
+ catch { }
3068
+ }
3069
+ return result;
3070
+ });
2943
3071
  electron_1.ipcMain.handle('agent:updateGoal', async (_event, goal, targetInput) => {
2944
3072
  if (!agent)
2945
3073
  return null;
@@ -3042,6 +3170,7 @@ else {
3042
3170
  pendingOptions: conversationSnapshot.pendingOptions || agent.pendingOptions,
3043
3171
  flowSuspension: flowSuspensionForTarget(target),
3044
3172
  flowRunning: flowRunningForTarget(target),
3173
+ computerUse: utilityHostToolHandler.computerUseState((0, conversationTarget_1.normalizeConversationTarget)(target).runtimeKey),
3045
3174
  draft: agent.getStoredConversationDraft(target.conversationId) || '',
3046
3175
  proxyEnabled: agent.config.getBool('proxy', 'enabled'),
3047
3176
  proxyUrl: agent.config.getStr('proxy', 'url'),
package/dist/preload.js CHANGED
@@ -34,11 +34,13 @@ contextBridge.exposeInMainWorld('api', {
34
34
  setConversationPinned: (id, pinned) => ipcRenderer.invoke('agent:setConversationPinned', id, pinned),
35
35
  renameConversation: (id, title) => ipcRenderer.invoke('agent:renameConversation', id, title),
36
36
  reorderConversations: (ids) => ipcRenderer.invoke('agent:reorderConversations', ids),
37
- browserRegisterGuest: (guestContentsId) => ipcRenderer.invoke('browser:registerGuest', guestContentsId),
37
+ browserRegisterGuest: (guestContentsId, target) => ipcRenderer.invoke('browser:registerGuest', guestContentsId, target),
38
38
  onBrowserEnsureGuest: (callback) => {
39
- ipcRenderer.on('browser:ensureGuest', () => callback());
39
+ ipcRenderer.on('browser:ensureGuest', (_event, target) => callback(target));
40
40
  },
41
41
  browserControl: (request) => ipcRenderer.invoke('browser:control', request),
42
+ computerUseState: (target) => ipcRenderer.invoke('agent:computerUseState', target),
43
+ setComputerUseEnabled: (target, enabled) => ipcRenderer.invoke('agent:setComputerUseEnabled', target, enabled),
42
44
  runFlow: (name, input, start) => ipcRenderer.invoke('flow:run', name, input, start),
43
45
  resumeFlow: (response, target) => ipcRenderer.invoke('flow:resume', response, target),
44
46
  guideFlow: (message, target) => ipcRenderer.invoke('flow:guide', message, target),
@@ -57,8 +57,7 @@ const nativeBash_1 = require("../core/nativeBash");
57
57
  const toolArgumentValidator_1 = require("../core/toolArgumentValidator");
58
58
  const localOcr_1 = require("../core/localOcr");
59
59
  const visualTextFallback_1 = require("../core/visualTextFallback");
60
- let computerUseLock = null;
61
- const COMPUTER_USE_LOCK_TTL_MS = 10 * 60 * 1000;
60
+ const computerUseSession_1 = require("../core/computerUseSession");
62
61
  function normalizeComputerUseAction(action) {
63
62
  return String(action || '').trim().toLowerCase();
64
63
  }
@@ -154,50 +153,28 @@ async function abortableToolDelay(durationMs, signal) {
154
153
  abort();
155
154
  });
156
155
  }
157
- function clearStaleComputerUseLock(now = Date.now()) {
158
- if (computerUseLock && now - computerUseLock.updatedAt > COMPUTER_USE_LOCK_TTL_MS) {
159
- computerUseLock = null;
160
- }
161
- }
162
- function computerUseLockError(action, owner) {
163
- return JSON.stringify({
164
- ok: false,
165
- action,
166
- error: `ComputerUse is already active in ${computerUseLock?.owner || 'another conversation'}. Stop it with computer_use takeover_stop or wait before using ComputerUse from another conversation.`,
167
- lock_owner: computerUseLock?.owner || '',
168
- requested_owner: owner,
169
- }, null, 2);
170
- }
171
- function acquireComputerUseLock(action, owner, wsPath) {
172
- const now = Date.now();
173
- clearStaleComputerUseLock(now);
174
- if (computerUseLock && computerUseLock.owner !== owner) {
175
- return computerUseLockError(action, owner);
176
- }
177
- computerUseLock = {
178
- owner,
156
+ function computerUseSessionScope(context, wsPath, owner) {
157
+ return {
158
+ runtimeKey: browserUseScope(context, wsPath).runtimeKey,
159
+ ownerLabel: owner,
179
160
  workspacePath: path.resolve(wsPath || process.cwd()),
180
- acquiredAt: computerUseLock?.owner === owner ? computerUseLock.acquiredAt : now,
181
- updatedAt: now,
182
161
  };
183
- return null;
184
162
  }
185
- function releaseComputerUseLock(action, owner) {
186
- clearStaleComputerUseLock();
187
- if (computerUseLock && computerUseLock.owner !== owner) {
188
- return computerUseLockError(action, owner);
189
- }
190
- if (computerUseLock?.owner === owner)
191
- computerUseLock = null;
192
- return null;
163
+ function acquireComputerUseLock(action, owner, wsPath, context = {}, dryRun = false) {
164
+ return computerUseSession_1.defaultComputerUseSessionRegistry.authorize(action, computerUseSessionScope(context, wsPath, owner), dryRun);
193
165
  }
194
- function assertComputerUseLockOwner(action, owner) {
195
- clearStaleComputerUseLock();
196
- if (computerUseLock && computerUseLock.owner !== owner) {
197
- return computerUseLockError(action, owner);
198
- }
166
+ function releaseComputerUseLock(action, owner, context = {}, wsPath = context.workspacePath || '') {
167
+ const scope = computerUseSessionScope(context, wsPath, owner);
168
+ computerUseSession_1.defaultComputerUseSessionRegistry.complete(action, scope);
199
169
  return null;
200
170
  }
171
+ function assertComputerUseLockOwner(action, owner, context = {}, wsPath = context.workspacePath || '') {
172
+ return computerUseSession_1.defaultComputerUseSessionRegistry.authorize(action, computerUseSessionScope(context, wsPath, owner));
173
+ }
174
+ // A competing conversation receives the stable user-facing marker
175
+ // "ComputerUse is already active" / "computerUse occupied".
176
+ // The two-argument releaseComputerUseLock(action, owner) form remains valid
177
+ // for direct callers; routed Build calls additionally provide their target.
201
178
  class ToolExecutor {
202
179
  config;
203
180
  ssh;
@@ -374,13 +351,16 @@ class ToolExecutor {
374
351
  t('linked_plan', 'Read or update the current conversation linked Markdown plan. Update requires the current expected_revision.', { action: { type: 'string', enum: ['get', 'update'] }, markdown: { type: 'string' }, expected_revision: { type: 'number' } }, ['action']),
375
352
  t('build_history_query', 'Read the concrete public work details of one historical Build Block. The prompt already exposes user input, final summary, and completion status; call this read-only tool only when the user asks what specifically happened. Select by newest-to-oldest history_index, or by run_id returned from an earlier query.', { history_index: { type: 'number', minimum: 1, description: '1-based historical Build Block index from the request ledger; 1 is the newest previous task.' }, run_id: { type: 'string', description: 'Exact run id returned by an earlier build_history_query result.' }, max_events: { type: 'number', minimum: 1, maximum: 200, description: 'Maximum trailing public work events; defaults to 80.' } }, []),
376
353
  t('context_compress', 'Actively compress the LLM context history for this conversation. This collapses older history entries into a concise summary while preserving the recent tail, which reduces context tokens and cost. IMPORTANT: it affects only the LLM context (what the model sees); the displayed conversation history shown to the user is never altered. Call this when the conversation is long, token pressure is high, or you judge that older turns are no longer needed in full. Idempotent and safe: repeated calls produce incremental summaries.', { keep_recent: { type: 'number', minimum: 2, maximum: 60, description: 'Recent message count to keep uncompressed at the tail. Defaults to the configured keep_recent_messages.' }, force: { type: 'boolean', description: 'Compress even if the context is not yet over the automatic threshold. Defaults to false.' } }, []),
377
- t('context_history_manage', 'Manage the LLM context history for this conversation without affecting the displayed conversation history. This is the active context-management surface. Actions: list returns a bounded index of context entries (position, role, name, length, first-line preview); remove deletes the entry at a given position; summarize replaces a contiguous range of context entries with a concise local summary entry; restore reinserts the original messages of a folded segment from the compression cache using restore_id; search finds which cached folded segments contain a query (and the matching lines); status reports usage vs trigger/target budgets, the last compression, the compression cache, and the protected recent-message zone. The displayed conversation history (what the user sees) is never modified by any action. The recent context tail and the last user message are protected from remove/summarize unless dangerous is true.', {
378
- action: { type: 'string', enum: ['list', 'remove', 'summarize', 'restore', 'search', 'status'], description: 'list: index context entries. remove: delete the entry at position. summarize: fold entries [from, to] into one summary entry. restore: reinsert cached original messages by restore_id. search: find folded cache entries containing query. status: report context usage, budgets, cache, and protected zone.' },
354
+ t('context_history_manage', 'Manage the LLM context history for this conversation without affecting the displayed conversation history. This is the active context-management surface. The hot cache stays bounded; evicted folded segments remain in a conversation-isolated append-only cold archive and are loaded only by explicit search/read/restore calls. Actions: list returns a bounded index of current context entries; remove deletes one current entry; summarize folds a contiguous current range; restore reinserts a folded segment when its summary marker is still present; search finds matching hot or archived segments; read returns one bounded segment without injecting the whole archive; status reports budgets, hot cache, cold archive, and the protected recent zone. The recent context tail and last user message are protected from remove/summarize unless dangerous is true.', {
355
+ action: { type: 'string', enum: ['list', 'remove', 'summarize', 'restore', 'search', 'read', 'status'], description: 'list current entries; remove one; summarize a range; restore by restore_id; search hot/cold folded segments; read one bounded folded segment; status report context budgets and storage.' },
379
356
  position: { type: 'number', minimum: 0, description: '0-based context entry index for remove, or the start of the range for summarize.' },
380
357
  to: { type: 'number', minimum: 0, description: '0-based inclusive end of the range for summarize. Defaults to position.' },
381
- limit: { type: 'number', minimum: 5, maximum: 400, description: 'Maximum context entries to list (default 200), or search matches to return (default 20).' },
358
+ limit: { type: 'number', minimum: 1, maximum: 400, description: 'Maximum context entries/messages/matches to return. Current-history list still keeps a minimum page of 5.' },
382
359
  restore_id: { type: 'string', description: 'Cache id of a folded segment (from search or status) to restore into context.' },
383
360
  query: { type: 'string', description: 'Case-insensitive text to search for across cached folded segments and their summaries.' },
361
+ offset: { type: 'number', minimum: 0, description: 'Message offset for read pagination.' },
362
+ content_offset: { type: 'number', minimum: 0, description: 'Character offset within the first read message, used with nextContentOffset when one message exceeds max_chars.' },
363
+ max_chars: { type: 'number', minimum: 1000, maximum: 60000, description: 'Maximum message-content characters returned by read (default 12000).' },
384
364
  dangerous: { type: 'boolean', description: 'Set true to override the protected recent-message zone and allow removing/summarizing entries that include the recent context tail or the last user message.' },
385
365
  }, ['action']),
386
366
  t('question', 'Ask user a multiple-choice question', { questions: { type: 'array' } }, ['questions']),
@@ -751,8 +731,8 @@ class ToolExecutor {
751
731
  const action = normalizeComputerUseAction(g('action'));
752
732
  const owner = `${computerUseOwner(context, wsPath)}:${String(context.actorId || 'root')}`;
753
733
  const lockGuard = action === 'takeover_stop'
754
- ? assertComputerUseLockOwner(action, owner)
755
- : acquireComputerUseLock(action, owner, wsPath);
734
+ ? assertComputerUseLockOwner(action, owner, context, wsPath)
735
+ : acquireComputerUseLock(action, owner, wsPath, context, args.dry_run === true);
756
736
  if (lockGuard)
757
737
  return lockGuard;
758
738
  if (process.env.NEWMARK_WSL_DISTRO) {
@@ -769,7 +749,7 @@ class ToolExecutor {
769
749
  }
770
750
  finally {
771
751
  if (action === 'takeover_stop')
772
- releaseComputerUseLock(action, owner);
752
+ releaseComputerUseLock(action, owner, context, wsPath);
773
753
  }
774
754
  }
775
755
  if (process.env.NEWMARK_ISOLATED_RUNTIME === '1') {
@@ -791,7 +771,7 @@ class ToolExecutor {
791
771
  }
792
772
  finally {
793
773
  if (action === 'takeover_stop')
794
- releaseComputerUseLock(action, owner);
774
+ releaseComputerUseLock(action, owner, context, wsPath);
795
775
  }
796
776
  }
797
777
  const output = await (0, computerUse_1.runComputerUse)({
@@ -835,7 +815,7 @@ class ToolExecutor {
835
815
  : undefined,
836
816
  });
837
817
  if (action === 'takeover_stop')
838
- releaseComputerUseLock(action, owner);
818
+ releaseComputerUseLock(action, owner, context, wsPath);
839
819
  return output;
840
820
  }
841
821
  case 'terminal_takeover': {
@@ -1349,9 +1329,17 @@ class ToolExecutor {
1349
1329
  }
1350
1330
  }
1351
1331
  async browserRun(request, signal, context = {}, workspacePath = this.root) {
1332
+ const scope = browserUseScope(context, workspacePath);
1333
+ const scopedRequest = {
1334
+ ...request,
1335
+ target: {
1336
+ workspaceId: context.workspaceId || (0, terminalTakeover_1.terminalTakeoverWorkspaceId)(workspacePath),
1337
+ conversationId: context.conversationId || 'default',
1338
+ runtimeKey: scope.runtimeKey,
1339
+ },
1340
+ };
1352
1341
  if (process.env.NEWMARK_WSL_DISTRO) {
1353
- const scope = browserUseScope(context, workspacePath);
1354
- const result = await (0, wslHostToolBridge_1.requestWindowsHostTool)('browser_control', request, {
1342
+ const result = await (0, wslHostToolBridge_1.requestWindowsHostTool)('browser_control', scopedRequest, {
1355
1343
  conversationId: context.conversationId || process.env.NEWMARK_CONVERSATION_ID || 'default',
1356
1344
  workspaceId: process.env.NEWMARK_WORKSPACE_ID || context.workspaceId || (0, terminalTakeover_1.terminalTakeoverWorkspaceId)(workspacePath),
1357
1345
  actorId: context.actorId || terminalTakeover_1.ROOT_TERMINAL_ACTOR_ID,
@@ -1361,10 +1349,12 @@ class ToolExecutor {
1361
1349
  return this.formatBrowserResult(result);
1362
1350
  }
1363
1351
  if (process.env.NEWMARK_ISOLATED_RUNTIME === '1') {
1364
- const result = await (0, utilityHostToolBridge_1.requestUtilityHostTool)('browser_control', request, undefined, 30_000, signal);
1352
+ const result = await (0, utilityHostToolBridge_1.requestUtilityHostTool)('browser_control', scopedRequest, undefined, 30_000, signal);
1365
1353
  return this.formatBrowserResult(result);
1366
1354
  }
1367
- const result = await browserControl_1.BrowserControl.run(request, signal);
1355
+ // Preserve the cancellation contract of BrowserControl.run(request, signal)
1356
+ // while routing the concrete request through the target-bound copy above.
1357
+ const result = await browserControl_1.BrowserControl.run(scopedRequest, signal);
1368
1358
  return this.formatBrowserResult(result);
1369
1359
  }
1370
1360
  formatBrowserResult(result) {
@@ -41,7 +41,7 @@ exports.NATIVE_TOOL_CATALOG = [
41
41
  { name: 'linked_plan', label: 'Linked plan', description: 'Read or conservatively update the conversation-linked Markdown plan.', category: 'agent', defaultEnabled: true, protected: true, availability: 'mode-scoped' },
42
42
  { name: 'build_history_query', label: 'Build history query', description: 'Read concrete public work details for one historical Build Block.', category: 'agent', defaultEnabled: true, protected: true, availability: 'mode-scoped' },
43
43
  { name: 'context_compress', label: 'Context compress', description: 'Actively compress the LLM context history, leaving the displayed conversation history unchanged.', category: 'agent', defaultEnabled: true, protected: true, availability: 'mode-scoped' },
44
- { name: 'context_history_manage', label: 'Context history manage', description: 'List, remove, or summarize entries in the LLM context history without touching the displayed conversation history.', category: 'agent', defaultEnabled: true, protected: true, availability: 'mode-scoped' },
44
+ { name: 'context_history_manage', label: 'Context history manage', description: 'Inspect, search, restore, or fold LLM context history without touching the displayed conversation history.', category: 'agent', defaultEnabled: true, protected: true, availability: 'mode-scoped' },
45
45
  { name: 'question', label: 'Ask question', description: 'Ask the user for structured option feedback.', category: 'agent', defaultEnabled: true, protected: true, availability: 'mode-scoped' },
46
46
  { name: 'skill_download', label: 'Skill download', description: 'Download and install a skill.', category: 'agent', defaultEnabled: true },
47
47
  { name: 'skill', label: 'Skill', description: 'Search enabled skill metadata or load one skill body on demand.', category: 'agent', defaultEnabled: true, protected: true, availability: 'mode-scoped' },