newmark-agent 0.6.4 → 0.6.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/config.example.json +13 -3
  2. package/dist/cli-commands.d.ts +1 -1
  3. package/dist/cli-commands.js +54 -3
  4. package/dist/cli-discovery.js +3 -0
  5. package/dist/conversation-utility-host.bundle.cjs +4472 -1767
  6. package/dist/conversation-utility-host.js +94 -7
  7. package/dist/core/agent.d.ts +126 -12
  8. package/dist/core/agent.js +785 -172
  9. package/dist/core/agentKernelRunner.d.ts +7 -1
  10. package/dist/core/agentKernelRunner.js +160 -20
  11. package/dist/core/autoRouter.d.ts +49 -44
  12. package/dist/core/autoRouter.js +117 -302
  13. package/dist/core/computerUseSession.js +1 -1
  14. package/dist/core/config.js +27 -11
  15. package/dist/core/continuation/contracts.d.ts +1 -1
  16. package/dist/core/continuation/store.d.ts +91 -1
  17. package/dist/core/continuation/store.js +291 -61
  18. package/dist/core/conversationKernel.d.ts +49 -7
  19. package/dist/core/conversationKernel.js +303 -51
  20. package/dist/core/conversationStateDocument.d.ts +18 -0
  21. package/dist/core/conversationStateDocument.js +88 -0
  22. package/dist/core/conversationVisualBudget.d.ts +74 -0
  23. package/dist/core/conversationVisualBudget.js +153 -0
  24. package/dist/core/electronUtilityAgentClient.d.ts +3 -2
  25. package/dist/core/electronUtilityAgentClient.js +32 -8
  26. package/dist/core/electronUtilityRuntimePool.d.ts +28 -3
  27. package/dist/core/electronUtilityRuntimePool.js +93 -24
  28. package/dist/core/fileWriteObservation.d.ts +41 -0
  29. package/dist/core/fileWriteObservation.js +454 -0
  30. package/dist/core/hostRuntimeHooks.d.ts +23 -0
  31. package/dist/core/hostRuntimeHooks.js +17 -0
  32. package/dist/core/jevDecision.d.ts +144 -0
  33. package/dist/core/jevDecision.js +226 -0
  34. package/dist/core/memoryProbe.d.ts +24 -0
  35. package/dist/core/memoryProbe.js +104 -0
  36. package/dist/core/mobilePairing.d.ts +7 -1
  37. package/dist/core/mobilePairing.js +9 -1
  38. package/dist/core/performanceDiagnostics.d.ts +1 -1
  39. package/dist/core/routeDecisionValidator.d.ts +86 -0
  40. package/dist/core/routeDecisionValidator.js +249 -0
  41. package/dist/core/routeEligibility.d.ts +52 -0
  42. package/dist/core/routeEligibility.js +108 -0
  43. package/dist/core/runtimeMemoryBudget.d.ts +6 -0
  44. package/dist/core/runtimeMemoryBudget.js +12 -0
  45. package/dist/core/toolPolicy.d.ts +1 -1
  46. package/dist/core/toolPolicy.js +1 -1
  47. package/dist/core/utilityAgentProtocol.d.ts +8 -10
  48. package/dist/core/utilityHostToolRouter.js +2 -0
  49. package/dist/core/visualDownscale.d.ts +6 -0
  50. package/dist/core/visualDownscale.js +137 -0
  51. package/dist/core/wslAgentClient.d.ts +1 -2
  52. package/dist/core/wslAgentClient.js +0 -8
  53. package/dist/core/wslAgentProtocol.d.ts +1 -10
  54. package/dist/core/wslAgentRuntimePool.d.ts +1 -3
  55. package/dist/core/wslAgentRuntimePool.js +17 -22
  56. package/dist/llm/provider.d.ts +3 -1
  57. package/dist/llm/provider.js +53 -9
  58. package/dist/main.js +107 -30
  59. package/dist/preload.js +3 -1
  60. package/dist/server.js +30 -18
  61. package/dist/tools/computerUse.d.ts +6 -0
  62. package/dist/tools/computerUse.js +406 -21
  63. package/dist/tools/computerUsePowerShellHost.d.ts +6 -1
  64. package/dist/tools/computerUsePowerShellHost.js +105 -31
  65. package/dist/tools/index.d.ts +3 -4
  66. package/dist/tools/index.js +146 -66
  67. package/dist/tui/src/adapters/core-runtime-adapter.js +13 -1
  68. package/dist/tui/src/i18n.js +12 -1
  69. package/dist/tui/src/render.js +9 -2
  70. package/dist/tui/src/settings-schema.js +19 -0
  71. package/dist/tui/src/state.js +69 -1
  72. package/dist/ui/index.html +651 -233
  73. package/dist/ui/lucide-sprite.svg +0 -8
  74. package/dist/wsl-agent-host.bundle.cjs +4302 -1767
  75. package/dist/wsl-agent-host.js +0 -3
  76. package/package.json +39 -10
@@ -0,0 +1,88 @@
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.readConversationStateDocument = readConversationStateDocument;
37
+ exports.readConversationIndexText = readConversationIndexText;
38
+ /**
39
+ * Workspace conversation state on disk is an **index** document
40
+ * (`conversations/state.json`) plus one heavy payload shard per conversation
41
+ * (`conversations/shards/<key>.<hash>.json`).
42
+ *
43
+ * Product code reads through `Agent.readStoredConversationState`, which
44
+ * hydrates shards lazily. Fixtures, diagnostics and archive tooling that read
45
+ * the raw file need the same view: reading only the index makes every
46
+ * conversation look empty. These helpers are the single shard-aware reader for
47
+ * such callers.
48
+ */
49
+ const fs = __importStar(require("fs"));
50
+ const path = __importStar(require("path"));
51
+ function parseJson(text) {
52
+ try {
53
+ const parsed = JSON.parse(text.replace(/^\uFEFF/, ''));
54
+ return parsed && typeof parsed === 'object' ? parsed : {};
55
+ }
56
+ catch {
57
+ return {};
58
+ }
59
+ }
60
+ /** Read a workspace conversation index and hydrate every conversation shard. */
61
+ function readConversationStateDocument(workspacePath) {
62
+ const indexPath = path.join(workspacePath, 'conversations', 'state.json');
63
+ const indexText = fs.existsSync(indexPath) ? fs.readFileSync(indexPath, 'utf-8') : '{}';
64
+ const index = parseJson(indexText);
65
+ const shardDir = path.join(path.dirname(indexPath), 'shards');
66
+ const conversations = {};
67
+ for (const [key, rawEntry] of Object.entries((index.conversations || {}))) {
68
+ const entry = rawEntry || {};
69
+ const shardFile = typeof entry.shard === 'string' && entry.shard ? path.join(shardDir, entry.shard) : '';
70
+ if (shardFile && fs.existsSync(shardFile)) {
71
+ conversations[key] = { ...entry, ...parseJson(fs.readFileSync(shardFile, 'utf-8')) };
72
+ continue;
73
+ }
74
+ conversations[key] = { ...entry };
75
+ }
76
+ return {
77
+ path: indexPath,
78
+ index,
79
+ conversations,
80
+ indexText,
81
+ hydratedText: JSON.stringify({ ...index, conversations }, null, 2),
82
+ };
83
+ }
84
+ /** Index-only text, for assertions about what the index itself must not contain. */
85
+ function readConversationIndexText(workspacePath) {
86
+ return readConversationStateDocument(workspacePath).indexText;
87
+ }
88
+ //# sourceMappingURL=conversationStateDocument.js.map
@@ -0,0 +1,74 @@
1
+ /**
2
+ * dev-0.6.6: per-conversation memory budget for ComputerUse / BrowserControl
3
+ * visual material.
4
+ *
5
+ * Field evidence: a long conversation that repeatedly drove computer_use /
6
+ * browser_control grew the utility host until V8 aborted with
7
+ * `OOM error in V8: CALL_AND_RETRY_LAST Allocation failed` at ~3017 MB heap.
8
+ * Every historic screenshot is stored as a file reference and materialized into
9
+ * a base64 data URL for the provider request, so a late-stage conversation
10
+ * builds a request whose visual payload grows with the number of observations.
11
+ *
12
+ * The budget is derived from the machine's *available* memory (not just its
13
+ * total), so an 8 GB machine that is already busy reclaims much earlier than an
14
+ * idle 32 GB one. Reclaiming only ever drops visual detail that was already
15
+ * sent in an earlier provider round; it never truncates text, tool results,
16
+ * plans or the conversation transcript.
17
+ */
18
+ export type VisualMemoryPressure = 'roomy' | 'tight' | 'critical' | 'severe';
19
+ export interface VisualMemorySample {
20
+ /** Physical RAM installed. Defaults to `os.totalmem()`. */
21
+ totalBytes?: number;
22
+ /** Free physical RAM right now. Defaults to `os.freemem()`. */
23
+ freeBytes?: number;
24
+ /** Resident set size of the process that owns the conversation, if known. */
25
+ rssBytes?: number;
26
+ }
27
+ export interface ConversationVisualBudget {
28
+ pressure: VisualMemoryPressure;
29
+ reason: string;
30
+ /** Largest single screenshot accepted into a provider request. */
31
+ maxSingleImageBytes: number;
32
+ /** Total visual bytes one provider request may materialize for the conversation. */
33
+ maxRequestImageBytes: number;
34
+ /** How many historic screenshots may stay in context. */
35
+ maxRetainedImages: number;
36
+ /** Retained visual bytes for the conversation (history plus live captures). */
37
+ maxConversationVisualBytes: number;
38
+ /** Idle hosts above this RSS are recycled instead of being reused. */
39
+ recycleRssBytes: number;
40
+ }
41
+ /** Pressure tier from available memory, with a total-RAM floor so 4 GB hosts never get a roomy budget. */
42
+ export declare function visualMemoryPressure(sample?: VisualMemorySample): {
43
+ pressure: VisualMemoryPressure;
44
+ reason: string;
45
+ };
46
+ export declare function conversationVisualBudget(sample?: VisualMemorySample): ConversationVisualBudget;
47
+ /**
48
+ * Heap cost of a data URL: the base64 payload length. This is what the provider
49
+ * request actually holds (a JS string of that many characters), and it matches
50
+ * the historical `dataUrl.length` cap this budget replaces.
51
+ */
52
+ export declare function visualDataUrlBytes(value: string): number;
53
+ export interface VisualRetentionEntry {
54
+ key: string;
55
+ bytes: number;
56
+ }
57
+ /**
58
+ * Choose which visual entries stay materialized, newest first.
59
+ *
60
+ * `entries` must be ordered oldest → newest (the order they appear in the
61
+ * conversation); the plan keeps the newest ones within both the byte budget and
62
+ * the retained-image count, and reports how much was released.
63
+ */
64
+ export declare function planVisualRetention(entries: VisualRetentionEntry[], budget: ConversationVisualBudget): {
65
+ keep: Set<string>;
66
+ released: string[];
67
+ releasedBytes: number;
68
+ retainedBytes: number;
69
+ };
70
+ /** Should an idle host be recycled instead of reused? */
71
+ export declare function shouldRecycleIdleHost(rssBytes: number, budget: ConversationVisualBudget): boolean;
72
+ /** Short human-readable summary used in work-status lines. */
73
+ export declare function describeVisualBudget(budget: ConversationVisualBudget): string;
74
+ //# sourceMappingURL=conversationVisualBudget.d.ts.map
@@ -0,0 +1,153 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.visualMemoryPressure = visualMemoryPressure;
4
+ exports.conversationVisualBudget = conversationVisualBudget;
5
+ exports.visualDataUrlBytes = visualDataUrlBytes;
6
+ exports.planVisualRetention = planVisualRetention;
7
+ exports.shouldRecycleIdleHost = shouldRecycleIdleHost;
8
+ exports.describeVisualBudget = describeVisualBudget;
9
+ const node_os_1 = require("node:os");
10
+ const MIB = 1024 ** 2;
11
+ const GIB = 1024 ** 3;
12
+ const TIERS = {
13
+ roomy: {
14
+ maxSingleImageBytes: 2 * MIB,
15
+ maxRequestImageBytes: 48 * MIB,
16
+ maxRetainedImages: 12,
17
+ maxConversationVisualBytes: 192 * MIB,
18
+ recycleRssBytes: 1536 * MIB,
19
+ },
20
+ tight: {
21
+ maxSingleImageBytes: 768 * 1024,
22
+ maxRequestImageBytes: 16 * MIB,
23
+ maxRetainedImages: 6,
24
+ maxConversationVisualBytes: 64 * MIB,
25
+ recycleRssBytes: 1024 * MIB,
26
+ },
27
+ critical: {
28
+ maxSingleImageBytes: 320 * 1024,
29
+ maxRequestImageBytes: 6 * MIB,
30
+ maxRetainedImages: 3,
31
+ maxConversationVisualBytes: 24 * MIB,
32
+ recycleRssBytes: 640 * MIB,
33
+ },
34
+ severe: {
35
+ maxSingleImageBytes: 160 * 1024,
36
+ maxRequestImageBytes: 2 * MIB,
37
+ maxRetainedImages: 1,
38
+ maxConversationVisualBytes: 8 * MIB,
39
+ recycleRssBytes: 384 * MIB,
40
+ },
41
+ };
42
+ /** Pressure tier from available memory, with a total-RAM floor so 4 GB hosts never get a roomy budget. */
43
+ function visualMemoryPressure(sample = {}) {
44
+ const totalBytes = Number.isFinite(sample.totalBytes) ? Number(sample.totalBytes) : (0, node_os_1.totalmem)();
45
+ const freeBytes = Number.isFinite(sample.freeBytes) ? Number(sample.freeBytes) : (0, node_os_1.freemem)();
46
+ if (Number.isFinite(sample.freeBytes) && Number(sample.freeBytes) <= 0) {
47
+ return { pressure: 'critical', reason: 'available memory is unknown' };
48
+ }
49
+ const available = Math.max(0, freeBytes);
50
+ if (available < 1.2 * GIB)
51
+ return { pressure: 'severe', reason: `only ${(available / MIB).toFixed(0)} MiB available` };
52
+ if (available < 3 * GIB)
53
+ return { pressure: 'critical', reason: `${(available / GIB).toFixed(1)} GiB available` };
54
+ if (available < 6 * GIB)
55
+ return { pressure: 'tight', reason: `${(available / GIB).toFixed(1)} GiB available` };
56
+ // A small machine rarely has 6 GiB free for long; never hand it the roomy caps.
57
+ if (Number.isFinite(totalBytes) && totalBytes > 0 && totalBytes < 8 * GIB) {
58
+ return { pressure: 'tight', reason: `installed RAM is only ${(totalBytes / GIB).toFixed(1)} GiB` };
59
+ }
60
+ return { pressure: 'roomy', reason: `${(available / GIB).toFixed(1)} GiB available` };
61
+ }
62
+ function conversationVisualBudget(sample = {}) {
63
+ const totalBytes = Number.isFinite(sample.totalBytes) ? Number(sample.totalBytes) : (0, node_os_1.totalmem)();
64
+ const base = visualMemoryPressure(sample);
65
+ const rssBytes = Math.max(0, Number(sample.rssBytes) || 0);
66
+ // V8 aborts on the process's own heap, not on the machine's free memory: the
67
+ // real host died at ~3017 MB while the machine still had memory available. So
68
+ // a process that already approaches its recycle watermark escalates one tier
69
+ // even when the machine looks roomy.
70
+ const escalate = {
71
+ roomy: 'tight',
72
+ tight: 'critical',
73
+ critical: 'severe',
74
+ severe: 'severe',
75
+ };
76
+ const approachingProcessLimit = rssBytes > 0 && rssBytes >= TIERS[base.pressure].recycleRssBytes * 0.6;
77
+ const pressure = approachingProcessLimit ? escalate[base.pressure] : base.pressure;
78
+ const reason = approachingProcessLimit
79
+ ? `${base.reason}; this host already uses ${Math.round(rssBytes / MIB)} MiB`
80
+ : base.reason;
81
+ const tier = TIERS[pressure];
82
+ // Clamp by installed RAM as well: an idle 4 GB machine must not accept a
83
+ // budget sized for a workstation.
84
+ const totalClamp = (fraction, floorBytes) => Number.isFinite(totalBytes) && totalBytes > 0 ? Math.max(floorBytes, Math.floor(totalBytes * fraction)) : Number.POSITIVE_INFINITY;
85
+ return {
86
+ pressure,
87
+ reason,
88
+ maxSingleImageBytes: tier.maxSingleImageBytes,
89
+ maxRequestImageBytes: Math.min(tier.maxRequestImageBytes, totalClamp(1 / 64, 2 * MIB)),
90
+ maxRetainedImages: tier.maxRetainedImages,
91
+ maxConversationVisualBytes: Math.min(tier.maxConversationVisualBytes, totalClamp(1 / 16, 8 * MIB)),
92
+ recycleRssBytes: Math.min(tier.recycleRssBytes, totalClamp(1 / 8, 256 * MIB)),
93
+ };
94
+ }
95
+ /**
96
+ * Heap cost of a data URL: the base64 payload length. This is what the provider
97
+ * request actually holds (a JS string of that many characters), and it matches
98
+ * the historical `dataUrl.length` cap this budget replaces.
99
+ */
100
+ function visualDataUrlBytes(value) {
101
+ const text = String(value || '');
102
+ const comma = text.indexOf(',');
103
+ if (!text.startsWith('data:') || comma < 0)
104
+ return text.length;
105
+ return text.length - comma - 1;
106
+ }
107
+ /**
108
+ * Choose which visual entries stay materialized, newest first.
109
+ *
110
+ * `entries` must be ordered oldest → newest (the order they appear in the
111
+ * conversation); the plan keeps the newest ones within both the byte budget and
112
+ * the retained-image count, and reports how much was released.
113
+ */
114
+ function planVisualRetention(entries, budget) {
115
+ const keep = new Set();
116
+ const released = [];
117
+ let releasedBytes = 0;
118
+ let retainedBytes = 0;
119
+ let retainedCount = 0;
120
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
121
+ const entry = entries[index];
122
+ const bytes = Math.max(0, Number(entry.bytes) || 0);
123
+ const fitsCount = retainedCount < Math.max(1, budget.maxRetainedImages);
124
+ const fitsBytes = retainedBytes + bytes <= budget.maxRequestImageBytes;
125
+ // The newest observation is always kept while it fits the single-image cap:
126
+ // dropping every screenshot would blind the agent, and the per-image cap
127
+ // already bounds what that one picture can cost.
128
+ const newestWithinSingleCap = index === entries.length - 1 && bytes <= Math.max(1, budget.maxSingleImageBytes);
129
+ if (newestWithinSingleCap || (fitsCount && fitsBytes)) {
130
+ keep.add(entry.key);
131
+ retainedBytes += bytes;
132
+ retainedCount += 1;
133
+ continue;
134
+ }
135
+ released.push(entry.key);
136
+ releasedBytes += bytes;
137
+ }
138
+ return { keep, released, releasedBytes, retainedBytes };
139
+ }
140
+ /** Should an idle host be recycled instead of reused? */
141
+ function shouldRecycleIdleHost(rssBytes, budget) {
142
+ const value = Number(rssBytes);
143
+ if (!Number.isFinite(value) || value <= 0)
144
+ return false;
145
+ return value >= budget.recycleRssBytes;
146
+ }
147
+ /** Short human-readable summary used in work-status lines. */
148
+ function describeVisualBudget(budget) {
149
+ return `${budget.pressure} (${budget.reason}; single image ≤ ${Math.round(budget.maxSingleImageBytes / 1024)} KiB, `
150
+ + `request visual ≤ ${Math.round(budget.maxRequestImageBytes / MIB)} MiB, keep ≤ ${budget.maxRetainedImages} screenshots, `
151
+ + `recycle host at ${Math.round(budget.recycleRssBytes / MIB)} MiB RSS)`;
152
+ }
153
+ //# sourceMappingURL=conversationVisualBudget.js.map
@@ -2,7 +2,7 @@ import { spawn } from 'child_process';
2
2
  import { NormalizedConversationTarget } from './conversationTarget';
3
3
  import { AgentMode, AgentWorkEvent, ConversationInputEnvelope, GuideReceipt } from './types';
4
4
  import { ConversationQueueAction, ConversationQueueActionInput } from './conversationKernel';
5
- import { UtilityAgentPromptResult, UtilityAutoRouteRatingResult, UtilityAgentSnapshotResult, UtilityAgentStopResult, UtilityConversationRewindResult, UtilityHostToolRequest, UtilityPromptRequest } from './utilityAgentProtocol';
5
+ import { UtilityAgentPromptResult, UtilityAgentSnapshotResult, UtilityAgentStopResult, UtilityConversationRewindResult, UtilityHostToolRequest, UtilityPromptRequest } from './utilityAgentProtocol';
6
6
  type WindowsProcessTreeHelperRuntime = {
7
7
  kind: 'precompiled' | 'runtime_compile';
8
8
  path: string;
@@ -95,6 +95,8 @@ export declare class ElectronUtilityAgentClient {
95
95
  snapshot(options?: {
96
96
  window?: number;
97
97
  before?: number;
98
+ transcript?: 'auto' | 'force';
99
+ transcriptRevision?: string;
98
100
  }): Promise<UtilityAgentSnapshotResult>;
99
101
  rewind(messageIndex: number): Promise<UtilityConversationRewindResult>;
100
102
  requestStop(runId?: string): Promise<UtilityAgentStopResult>;
@@ -105,7 +107,6 @@ export declare class ElectronUtilityAgentClient {
105
107
  keepRecent?: number;
106
108
  force?: boolean;
107
109
  }): Promise<Record<string, unknown>>;
108
- rateAutoRoute(score: number, routeId?: string): Promise<UtilityAutoRouteRatingResult>;
109
110
  setWorkRunExpanded(runId: string, expanded: boolean): Promise<boolean>;
110
111
  setMode(mode: AgentMode): Promise<AgentMode>;
111
112
  setModel(model: string): Promise<string>;
@@ -52,6 +52,23 @@ const child_process_1 = require("child_process");
52
52
  const runtimeDiagnostics_1 = require("./runtimeDiagnostics");
53
53
  const runtimeLifecycle_1 = require("./runtimeLifecycle");
54
54
  const conversationTarget_1 = require("./conversationTarget");
55
+ /**
56
+ * Explicit heap ceiling for one conversation host: half of what the machine has
57
+ * free right now, clamped to a range that is useful on an 8 GB laptop and still
58
+ * generous on a workstation. Reclamation (idle recycle + the snapshot/visual
59
+ * budgets) is what keeps the host below this; the ceiling only makes the failure
60
+ * mode predictable instead of a silent ~3 GB V8 abort.
61
+ */
62
+ function hostHeapLimitMiB() {
63
+ let freeMiB = 0;
64
+ try {
65
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
66
+ const os = require('node:os');
67
+ freeMiB = Math.floor(os.freemem() / 1024 ** 2);
68
+ }
69
+ catch { /* fall back to the lower bound */ }
70
+ return Math.max(1024, Math.min(4096, Math.floor(freeMiB / 2) || 1024));
71
+ }
55
72
  // PowerShell startup can be slow on a cold or busy Windows host even though
56
73
  // the precompiled Toolhelp helper itself is healthy.
57
74
  const WINDOWS_TREE_SNAPSHOT_TIMEOUT_MS = 12_000;
@@ -943,6 +960,21 @@ class ElectronUtilityAgentClient {
943
960
  async startNewChild() {
944
961
  const child = electron_1.utilityProcess.fork(this.hostScript, [], {
945
962
  cwd: this.root,
963
+ /**
964
+ * dev-0.6.6 long-run stability: give the per-conversation Agent host an
965
+ * explicit heap ceiling derived from the machine's available memory
966
+ * instead of relying on the process default (the field crash aborted at
967
+ * ~3017 MB with no ceiling policy at all). Recycling an idle host that is
968
+ * over its budget is handled by the pool.
969
+ *
970
+ * `NEWMARK_HOST_HEAP_SNAPSHOT=1` additionally enables
971
+ * `--heapsnapshot-near-heap-limit` for diagnosis; it is off by default so
972
+ * no snapshot capability is active in normal runs.
973
+ */
974
+ execArgv: [
975
+ `--max-old-space-size=${hostHeapLimitMiB()}`,
976
+ ...(process.env.NEWMARK_HOST_HEAP_SNAPSHOT === '1' ? ['--heapsnapshot-near-heap-limit=1'] : []),
977
+ ],
946
978
  env: {
947
979
  ...process.env,
948
980
  NEWMARK_RUNTIME_ROOT: this.root,
@@ -1043,14 +1075,6 @@ class ElectronUtilityAgentClient {
1043
1075
  await this.start();
1044
1076
  return await this.request('context_compress', { target: this.target, options }, 120_000);
1045
1077
  }
1046
- async rateAutoRoute(score, routeId = '') {
1047
- await this.start();
1048
- return await this.request('rate_auto_route', {
1049
- target: this.target,
1050
- score,
1051
- routeId: String(routeId || ''),
1052
- }, 5_000);
1053
- }
1054
1078
  async setWorkRunExpanded(runId, expanded) {
1055
1079
  await this.start();
1056
1080
  return !!await this.request('set_work_run_expanded', { target: this.target, runId, expanded }, 5_000);
@@ -1,8 +1,9 @@
1
+ import { type ConversationVisualBudget } from './conversationVisualBudget';
1
2
  import { AgentMode, AgentWorkEvent, ConversationInputEnvelope, GuideReceipt } from './types';
2
3
  import { ConversationQueueAction, ConversationQueueActionInput } from './conversationKernel';
3
4
  import { ConversationRuntimeTarget, NormalizedConversationTarget } from './conversationTarget';
4
5
  import { UtilityHostToolHandler } from './electronUtilityAgentClient';
5
- import { UtilityAgentPromptResult, UtilityAutoRouteRatingResult, UtilityConversationRewindResult, UtilityAgentSnapshotResult, UtilityAgentStopResult, UtilityPromptRequest } from './utilityAgentProtocol';
6
+ import { UtilityAgentPromptResult, UtilityConversationRewindResult, UtilityAgentSnapshotResult, UtilityAgentStopResult, UtilityPromptRequest } from './utilityAgentProtocol';
6
7
  export interface ElectronTargetRuntimeClient {
7
8
  subscribe(listener: (event: AgentWorkEvent) => void): () => void;
8
9
  setHostToolHandler(handler: UtilityHostToolHandler | null): void;
@@ -10,6 +11,8 @@ export interface ElectronTargetRuntimeClient {
10
11
  snapshot(options?: {
11
12
  window?: number;
12
13
  before?: number;
14
+ transcript?: 'auto' | 'force';
15
+ transcriptRevision?: string;
13
16
  }): Promise<UtilityAgentSnapshotResult>;
14
17
  rewind(messageIndex: number): Promise<UtilityConversationRewindResult>;
15
18
  requestStop(runId?: string): Promise<UtilityAgentStopResult>;
@@ -20,7 +23,6 @@ export interface ElectronTargetRuntimeClient {
20
23
  keepRecent?: number;
21
24
  force?: boolean;
22
25
  }): Promise<Record<string, unknown>>;
23
- rateAutoRoute?(score: number, routeId?: string): Promise<UtilityAutoRouteRatingResult>;
24
26
  setWorkRunExpanded(runId: string, expanded: boolean): Promise<boolean>;
25
27
  setMode?(mode: AgentMode): Promise<AgentMode>;
26
28
  setModel?(model: string): Promise<string>;
@@ -52,6 +54,15 @@ export interface ElectronUtilityRuntimePoolOptions {
52
54
  stopRequestTimeoutMs?: number;
53
55
  forceStopAckTimeoutMs?: number;
54
56
  maxResidentRuntimes?: number;
57
+ /** Resident bytes of a host process, supplied by the Electron main process. */
58
+ sampleProcessMemoryBytes?: (pid: number) => number;
59
+ /**
60
+ * dev-0.6.6: per-conversation visual budget. Hosts that exceed its RSS
61
+ * watermark are recycled when idle instead of being reused, so a conversation
62
+ * that flooded memory through ComputerUse/BrowserControl observations starts
63
+ * from a clean process next time.
64
+ */
65
+ visualBudget?: () => ConversationVisualBudget;
55
66
  }
56
67
  /** One real Electron utilityProcess per active ConversationTarget. */
57
68
  export declare class ElectronUtilityRuntimePool {
@@ -72,9 +83,18 @@ export declare class ElectronUtilityRuntimePool {
72
83
  subscribe(listener: (event: AgentWorkEvent) => void): () => void;
73
84
  setHostToolHandler(handler: UtilityHostToolHandler | null): void;
74
85
  prompt(params: UtilityPromptRequest): Promise<UtilityAgentPromptResult>;
86
+ /**
87
+ * Stop an idle host whose resident memory is still above the machine-derived
88
+ * watermark after its turn settled. Safe by construction: it only runs after
89
+ * `release()` and skips anything that is busy, stopping, restarting or
90
+ * disposing, and the runtime pool rebuilds a fresh host on the next request.
91
+ */
92
+ private recycleAfterSettlementIfOverBudget;
75
93
  snapshot(target: ConversationRuntimeTarget, options?: {
76
94
  window?: number;
77
95
  before?: number;
96
+ transcript?: 'auto' | 'force';
97
+ transcriptRevision?: string;
78
98
  }): Promise<UtilityAgentSnapshotResult>;
79
99
  rewind(target: ConversationRuntimeTarget, messageIndex: number): Promise<UtilityConversationRewindResult>;
80
100
  requestStop(target: ConversationRuntimeTarget, runId?: string): Promise<ElectronPoolStopResult>;
@@ -85,7 +105,6 @@ export declare class ElectronUtilityRuntimePool {
85
105
  keepRecent?: number;
86
106
  force?: boolean;
87
107
  }): Promise<Record<string, unknown>>;
88
- rateAutoRoute(target: ConversationRuntimeTarget, score: number, routeId?: string): Promise<UtilityAutoRouteRatingResult>;
89
108
  setWorkRunExpanded(target: ConversationRuntimeTarget, runId: string, expanded: boolean): Promise<boolean>;
90
109
  setInputMode(target: ConversationRuntimeTarget, mode: string): Promise<'guide' | 'next' | null>;
91
110
  setMode(target: ConversationRuntimeTarget, mode: AgentMode): Promise<AgentMode | null>;
@@ -130,6 +149,12 @@ export declare class ElectronUtilityRuntimePool {
130
149
  private evictLeastRecentlyUsedIdle;
131
150
  private scheduleIdle;
132
151
  private evictIfIdle;
152
+ /**
153
+ * Recycle every idle host whose resident memory is over the machine's visual
154
+ * budget. Returns the runtime keys that were recycled.
155
+ */
156
+ reclaimIdleHostsOverMemory(): Promise<string[]>;
157
+ private hostOverMemoryBudget;
133
158
  private findEntry;
134
159
  private gracefulSupervisorResult;
135
160
  private forceTerminateEntry;
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ElectronUtilityRuntimePool = void 0;
4
+ const runtimeMemoryBudget_1 = require("./runtimeMemoryBudget");
5
+ const conversationVisualBudget_1 = require("./conversationVisualBudget");
4
6
  const conversationTarget_1 = require("./conversationTarget");
5
7
  const electronUtilityAgentClient_1 = require("./electronUtilityAgentClient");
6
8
  const runtimePoolCapacity_1 = require("./runtimePoolCapacity");
@@ -48,10 +50,48 @@ class ElectronUtilityRuntimePool {
48
50
  }
49
51
  finally {
50
52
  this.release(entry, true);
53
+ // dev-0.6.6: a long turn can leave the host far above its budget even
54
+ // after it settles. Recycling right after settlement (never mid-turn — the
55
+ // promise has already resolved here) gives the next turn a clean process
56
+ // instead of waiting for the idle TTL.
57
+ void this.recycleAfterSettlementIfOverBudget(entry);
51
58
  }
52
59
  }
60
+ /**
61
+ * Stop an idle host whose resident memory is still above the machine-derived
62
+ * watermark after its turn settled. Safe by construction: it only runs after
63
+ * `release()` and skips anything that is busy, stopping, restarting or
64
+ * disposing, and the runtime pool rebuilds a fresh host on the next request.
65
+ */
66
+ async recycleAfterSettlementIfOverBudget(entry) {
67
+ const runtimeKey = entry.target.runtimeKey;
68
+ if (this.entries.get(runtimeKey) !== entry)
69
+ return;
70
+ if (entry.activeOperations > 0 || entry.stopIntent || this.restarting.has(runtimeKey) || this.disposing.has(runtimeKey))
71
+ return;
72
+ if (!this.hostOverMemoryBudget(entry))
73
+ return;
74
+ try {
75
+ await this.stopEntry(entry);
76
+ }
77
+ catch { /* a host that refuses to stop is left to the idle sweep */ }
78
+ }
53
79
  async snapshot(target, options = {}) {
54
- const entry = await this.acquire((0, conversationTarget_1.normalizeConversationTarget)(target));
80
+ const normalized = (0, conversationTarget_1.normalizeConversationTarget)(target);
81
+ const wantsDefaultWindow = options.before == null && (options.window == null || options.window === 200);
82
+ // A read must survive the runtime's own teardown. Clients poll this on a
83
+ // timer (the paired mobile client polls conversation-ui-state every second)
84
+ // and an archive deliberately force-stops the target for several seconds;
85
+ // answering 500 for that window makes the remote UI drop the conversation
86
+ // it is looking at. Earlier pages really are unavailable mid-teardown, so
87
+ // only the default window is served from the supervisor's cached view.
88
+ if (wantsDefaultWindow) {
89
+ const pending = this.entries.get(normalized.runtimeKey);
90
+ if (pending && (this.disposing.has(normalized.runtimeKey) || this.restarting.has(normalized.runtimeKey))) {
91
+ return this.supervisorSnapshot(pending);
92
+ }
93
+ }
94
+ const entry = await this.acquire(normalized);
55
95
  let scheduleIdle = false;
56
96
  try {
57
97
  const customWindow = options.before != null || (options.window != null && options.window !== 200);
@@ -234,19 +274,6 @@ class ElectronUtilityRuntimePool {
234
274
  this.release(entry, true);
235
275
  }
236
276
  }
237
- async rateAutoRoute(target, score, routeId = '') {
238
- const normalized = (0, conversationTarget_1.normalizeConversationTarget)(target);
239
- const entry = await this.acquireExisting(normalized);
240
- if (!entry || !entry.client.rateAutoRoute) {
241
- return { ok: false, reason: 'no_active_auto_route' };
242
- }
243
- try {
244
- return await entry.client.rateAutoRoute(score, routeId);
245
- }
246
- finally {
247
- this.release(entry, true);
248
- }
249
- }
250
277
  async setWorkRunExpanded(target, runId, expanded) {
251
278
  const entry = await this.acquire((0, conversationTarget_1.normalizeConversationTarget)(target));
252
279
  try {
@@ -607,13 +634,9 @@ class ElectronUtilityRuntimePool {
607
634
  return this.accessSequence;
608
635
  }
609
636
  maxResidentRuntimes() {
610
- // The pool hosts one utility runtime per active conversation target.
611
- // A small default (2) silently blocked users from running more than two
612
- // conversations at once with "utility runtime pool capacity reached".
613
- // Idle runtimes are still evicted on LRU after the idle TTL, so a higher
614
- // default bounds memory by activity, not by an arbitrary conversation cap.
615
- const configured = Number(this.options.maxResidentRuntimes ?? 8);
616
- return Number.isFinite(configured) ? Math.max(1, Math.floor(configured)) : 8;
637
+ const fallback = (0, runtimeMemoryBudget_1.runtimeMemoryBudget)().maxResidentRuntimes;
638
+ const configured = Number(this.options.maxResidentRuntimes ?? fallback);
639
+ return Number.isFinite(configured) ? Math.max(1, Math.floor(configured)) : fallback;
617
640
  }
618
641
  async serializeCapacity(operation) {
619
642
  const previous = this.capacityTail;
@@ -661,7 +684,7 @@ class ElectronUtilityRuntimePool {
661
684
  }
662
685
  scheduleIdle(entry) {
663
686
  this.touch(entry);
664
- const ttl = Math.max(10, Number(this.options.idleTtlMs ?? 5 * 60 * 1000));
687
+ const ttl = Math.max(10, Number(this.options.idleTtlMs ?? (0, runtimeMemoryBudget_1.runtimeMemoryBudget)().idleTtlMs));
665
688
  const timer = setTimeout(() => {
666
689
  void this.evictIfIdle(entry.target.runtimeKey, entry.lastUsedAt);
667
690
  }, ttl);
@@ -677,6 +700,13 @@ class ElectronUtilityRuntimePool {
677
700
  this.scheduleIdle(entry);
678
701
  return;
679
702
  }
703
+ // dev-0.6.6: an idle host that grew past this machine's visual budget
704
+ // (long ComputerUse/BrowserControl conversations) is recycled rather than
705
+ // reused, so the next turn starts from a clean process.
706
+ if (this.hostOverMemoryBudget(entry)) {
707
+ await this.stopEntry(entry);
708
+ return;
709
+ }
680
710
  let snapshot;
681
711
  try {
682
712
  snapshot = await entry.client.snapshot();
@@ -697,6 +727,42 @@ class ElectronUtilityRuntimePool {
697
727
  await this.stopEntry(entry);
698
728
  });
699
729
  }
730
+ /**
731
+ * Recycle every idle host whose resident memory is over the machine's visual
732
+ * budget. Returns the runtime keys that were recycled.
733
+ */
734
+ async reclaimIdleHostsOverMemory() {
735
+ const recycled = [];
736
+ for (const entry of Array.from(this.entries.values())) {
737
+ const runtimeKey = entry.target.runtimeKey;
738
+ if (entry.activeOperations > 0 || entry.stopIntent || this.restarting.has(runtimeKey) || this.disposing.has(runtimeKey))
739
+ continue;
740
+ if (!this.hostOverMemoryBudget(entry))
741
+ continue;
742
+ try {
743
+ await this.stopEntry(entry);
744
+ recycled.push(runtimeKey);
745
+ }
746
+ catch { /* a host that cannot be stopped stays until it exits by itself */ }
747
+ }
748
+ return recycled;
749
+ }
750
+ hostOverMemoryBudget(entry) {
751
+ const sampler = this.options.sampleProcessMemoryBytes;
752
+ if (!sampler)
753
+ return false;
754
+ const pid = Number(entry.client.status().pid || 0);
755
+ if (!pid)
756
+ return false;
757
+ let rssBytes = 0;
758
+ try {
759
+ rssBytes = Number(sampler(pid) || 0);
760
+ }
761
+ catch {
762
+ return false;
763
+ }
764
+ return (0, conversationVisualBudget_1.shouldRecycleIdleHost)(rssBytes, (this.options.visualBudget || conversationVisualBudget_1.conversationVisualBudget)());
765
+ }
700
766
  findEntry(target) {
701
767
  const direct = this.entries.get((0, conversationTarget_1.normalizeConversationTarget)(target).runtimeKey);
702
768
  if (direct)
@@ -826,17 +892,20 @@ class ElectronUtilityRuntimePool {
826
892
  return true;
827
893
  }
828
894
  supervisorSnapshot(entry) {
895
+ // `stopIntent` is present for a requested stop; a force stop run driven by
896
+ // the archive path may reach here purely through `disposing`/`restarting`.
829
897
  const intent = entry.stopIntent;
830
898
  const cached = entry.lastSnapshot;
831
899
  return {
832
900
  ...cached,
833
901
  target: entry.target,
834
902
  runtime: {
903
+ ...(cached?.runtime || {}),
835
904
  target: entry.target,
836
905
  workspaceKey: entry.target.workspaceKey,
837
906
  runtimeKey: entry.target.runtimeKey,
838
- runId: intent.runId,
839
- generation: intent.generation,
907
+ runId: intent?.runId || entry.lastRunId,
908
+ generation: intent?.generation || entry.lastGeneration,
840
909
  running: true,
841
910
  stopRequested: true,
842
911
  workRuns: cached?.runtime?.workRuns || [],