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.
- package/config.example.json +13 -3
- package/dist/cli-commands.d.ts +1 -1
- package/dist/cli-commands.js +54 -3
- package/dist/cli-discovery.js +3 -0
- package/dist/conversation-utility-host.bundle.cjs +4472 -1767
- package/dist/conversation-utility-host.js +94 -7
- package/dist/core/agent.d.ts +126 -12
- package/dist/core/agent.js +785 -172
- package/dist/core/agentKernelRunner.d.ts +7 -1
- package/dist/core/agentKernelRunner.js +160 -20
- package/dist/core/autoRouter.d.ts +49 -44
- package/dist/core/autoRouter.js +117 -302
- package/dist/core/computerUseSession.js +1 -1
- package/dist/core/config.js +27 -11
- package/dist/core/continuation/contracts.d.ts +1 -1
- package/dist/core/continuation/store.d.ts +91 -1
- package/dist/core/continuation/store.js +291 -61
- package/dist/core/conversationKernel.d.ts +49 -7
- package/dist/core/conversationKernel.js +303 -51
- package/dist/core/conversationStateDocument.d.ts +18 -0
- package/dist/core/conversationStateDocument.js +88 -0
- package/dist/core/conversationVisualBudget.d.ts +74 -0
- package/dist/core/conversationVisualBudget.js +153 -0
- package/dist/core/electronUtilityAgentClient.d.ts +3 -2
- package/dist/core/electronUtilityAgentClient.js +32 -8
- package/dist/core/electronUtilityRuntimePool.d.ts +28 -3
- package/dist/core/electronUtilityRuntimePool.js +93 -24
- package/dist/core/fileWriteObservation.d.ts +41 -0
- package/dist/core/fileWriteObservation.js +454 -0
- package/dist/core/hostRuntimeHooks.d.ts +23 -0
- package/dist/core/hostRuntimeHooks.js +17 -0
- package/dist/core/jevDecision.d.ts +144 -0
- package/dist/core/jevDecision.js +226 -0
- package/dist/core/memoryProbe.d.ts +24 -0
- package/dist/core/memoryProbe.js +104 -0
- package/dist/core/mobilePairing.d.ts +7 -1
- package/dist/core/mobilePairing.js +9 -1
- package/dist/core/performanceDiagnostics.d.ts +1 -1
- package/dist/core/routeDecisionValidator.d.ts +86 -0
- package/dist/core/routeDecisionValidator.js +249 -0
- package/dist/core/routeEligibility.d.ts +52 -0
- package/dist/core/routeEligibility.js +108 -0
- package/dist/core/runtimeMemoryBudget.d.ts +6 -0
- package/dist/core/runtimeMemoryBudget.js +12 -0
- package/dist/core/toolPolicy.d.ts +1 -1
- package/dist/core/toolPolicy.js +1 -1
- package/dist/core/utilityAgentProtocol.d.ts +8 -10
- package/dist/core/utilityHostToolRouter.js +2 -0
- package/dist/core/visualDownscale.d.ts +6 -0
- package/dist/core/visualDownscale.js +137 -0
- package/dist/core/wslAgentClient.d.ts +1 -2
- package/dist/core/wslAgentClient.js +0 -8
- package/dist/core/wslAgentProtocol.d.ts +1 -10
- package/dist/core/wslAgentRuntimePool.d.ts +1 -3
- package/dist/core/wslAgentRuntimePool.js +17 -22
- package/dist/llm/provider.d.ts +3 -1
- package/dist/llm/provider.js +53 -9
- package/dist/main.js +107 -30
- package/dist/preload.js +3 -1
- package/dist/server.js +30 -18
- package/dist/tools/computerUse.d.ts +6 -0
- package/dist/tools/computerUse.js +406 -21
- package/dist/tools/computerUsePowerShellHost.d.ts +6 -1
- package/dist/tools/computerUsePowerShellHost.js +105 -31
- package/dist/tools/index.d.ts +3 -4
- package/dist/tools/index.js +146 -66
- package/dist/tui/src/adapters/core-runtime-adapter.js +13 -1
- package/dist/tui/src/i18n.js +12 -1
- package/dist/tui/src/render.js +9 -2
- package/dist/tui/src/settings-schema.js +19 -0
- package/dist/tui/src/state.js +69 -1
- package/dist/ui/index.html +651 -233
- package/dist/ui/lucide-sprite.svg +0 -8
- package/dist/wsl-agent-host.bundle.cjs +4302 -1767
- package/dist/wsl-agent-host.js +0 -3
- 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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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 ??
|
|
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.
|
|
839
|
-
generation: intent.
|
|
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 || [],
|