newmark-agent 0.5.14 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/conversation-utility-host.bundle.cjs +4565 -2998
- package/dist/conversation-utility-host.js +1 -1
- package/dist/core/agent.d.ts +108 -16
- package/dist/core/agent.js +698 -195
- package/dist/core/agentKernel/agent.d.ts +1 -0
- package/dist/core/agentKernel/agent.js +9 -0
- package/dist/core/agentKernelDiagnostics.d.ts +4 -6
- package/dist/core/agentKernelDiagnostics.js +12 -9
- package/dist/core/agentKernelRunner.d.ts +5 -0
- package/dist/core/agentKernelRunner.js +250 -107
- package/dist/core/autoRouter.js +17 -27
- package/dist/core/config.d.ts +2 -0
- package/dist/core/config.js +7 -1
- package/dist/core/conversationCommandState.d.ts +17 -0
- package/dist/core/conversationCommandState.js +140 -0
- package/dist/core/conversationKernel.d.ts +25 -4
- package/dist/core/conversationKernel.js +360 -89
- package/dist/core/conversationListEvent.d.ts +6 -0
- package/dist/core/conversationListEvent.js +14 -0
- package/dist/core/electronUtilityAgentClient.d.ts +4 -1
- package/dist/core/electronUtilityAgentClient.js +4 -4
- package/dist/core/electronUtilityRuntimePool.d.ts +8 -2
- package/dist/core/electronUtilityRuntimePool.js +11 -5
- package/dist/core/installUpdate.js +41 -29
- package/dist/core/modelResponseHealth.d.ts +17 -0
- package/dist/core/modelResponseHealth.js +193 -0
- package/dist/core/providerUsageAccounting.d.ts +47 -0
- package/dist/core/providerUsageAccounting.js +71 -0
- package/dist/core/requestContextEstimate.d.ts +18 -0
- package/dist/core/requestContextEstimate.js +54 -0
- package/dist/core/subagent.d.ts +89 -5
- package/dist/core/subagent.js +264 -41
- package/dist/core/subagentCommunication.d.ts +43 -0
- package/dist/core/subagentCommunication.js +167 -0
- package/dist/core/types.d.ts +34 -1
- package/dist/core/utilityAgentProtocol.d.ts +4 -0
- package/dist/core/workEventCoalescer.js +4 -1
- package/dist/core/wslAgentClient.d.ts +18 -5
- package/dist/core/wslAgentClient.js +79 -27
- package/dist/core/wslAgentProtocol.d.ts +7 -0
- package/dist/core/wslAgentRuntimePool.d.ts +8 -2
- package/dist/core/wslAgentRuntimePool.js +19 -6
- package/dist/core/wslRuntimeProcessTree.d.ts +10 -0
- package/dist/core/wslRuntimeProcessTree.js +104 -0
- package/dist/llm/provider.d.ts +5 -12
- package/dist/llm/provider.js +150 -269
- package/dist/main.js +582 -356
- package/dist/preload.js +3 -3
- package/dist/providers/chat-completions.adapter.d.ts +2 -0
- package/dist/providers/chat-completions.adapter.js +84 -36
- package/dist/providers/provider-adapter.d.ts +2 -0
- package/dist/providers/provider-events.d.ts +23 -14
- package/dist/providers/provider-events.js +148 -43
- package/dist/providers/provider-headers.d.ts +7 -3
- package/dist/providers/provider-headers.js +109 -39
- package/dist/providers/provider-request-compat.d.ts +6 -0
- package/dist/providers/provider-request-compat.js +37 -0
- package/dist/providers/responses.adapter.d.ts +2 -0
- package/dist/providers/responses.adapter.js +192 -126
- package/dist/server.d.ts +9 -2
- package/dist/server.js +100 -24
- package/dist/tools/index.js +17 -1
- package/dist/ui/index.html +2515 -693
- package/dist/ui/lucide-sprite.svg +7 -0
- package/dist/ui/startup.html +6 -6
- package/dist/wsl-agent-host.bundle.cjs +4575 -3006
- package/dist/wsl-agent-host.js +9 -4
- package/package.json +23 -4
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SubagentCommunicationSelectionError = void 0;
|
|
4
|
+
exports.selectSubagentCommunication = selectSubagentCommunication;
|
|
5
|
+
class SubagentCommunicationSelectionError extends Error {
|
|
6
|
+
code;
|
|
7
|
+
constructor(code, message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.name = 'SubagentCommunicationSelectionError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
exports.SubagentCommunicationSelectionError = SubagentCommunicationSelectionError;
|
|
14
|
+
const MAX_BODY_CHARS = 30000;
|
|
15
|
+
const own = (object, key) => Object.prototype.hasOwnProperty.call(object, key);
|
|
16
|
+
const record = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
|
|
17
|
+
function fail(code, message) { throw new SubagentCommunicationSelectionError(code, message); }
|
|
18
|
+
function messageText(value) {
|
|
19
|
+
if (typeof value === 'string')
|
|
20
|
+
return value;
|
|
21
|
+
if (!Array.isArray(value))
|
|
22
|
+
return '';
|
|
23
|
+
return value.filter(part => record(part) && ['text', 'input_text', 'output_text'].includes(String(part.type)))
|
|
24
|
+
.map(part => typeof part.text === 'string' ? part.text : '').join('\n');
|
|
25
|
+
}
|
|
26
|
+
function toolPayload(value) {
|
|
27
|
+
// Only the selected message.content / function.arguments enter this path.
|
|
28
|
+
// Business fields and code snippets inside a tool payload are evidence, not
|
|
29
|
+
// message-level private metadata, and must never be rewritten or filtered.
|
|
30
|
+
if (typeof value === 'string')
|
|
31
|
+
return value;
|
|
32
|
+
if (value === undefined)
|
|
33
|
+
return '';
|
|
34
|
+
try {
|
|
35
|
+
return JSON.stringify(value) ?? '';
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return fail('invalid_content', 'Selected tool payload is not serializable; provide an explicit summary instead.');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function units(history) {
|
|
42
|
+
const texts = [], tools = [];
|
|
43
|
+
const pending = new Map();
|
|
44
|
+
const ambiguous = new Set();
|
|
45
|
+
history.forEach((message, sourceIndex) => {
|
|
46
|
+
if (!message || message.hidden_user_input === true)
|
|
47
|
+
return;
|
|
48
|
+
if (message.role === 'user' || message.role === 'assistant') {
|
|
49
|
+
const text = messageText(message.content);
|
|
50
|
+
if (text.trim())
|
|
51
|
+
texts.push({ role: message.role, text, sourceIndex });
|
|
52
|
+
}
|
|
53
|
+
if (message.role === 'assistant' && Array.isArray(message.tool_calls)) {
|
|
54
|
+
for (const [callIndex, call] of message.tool_calls.entries()) {
|
|
55
|
+
if (!record(call) || !record(call.function))
|
|
56
|
+
continue;
|
|
57
|
+
const id = typeof call.id === 'string' ? call.id : '';
|
|
58
|
+
const name = typeof call.function.name === 'string' ? call.function.name : '';
|
|
59
|
+
if (!id.trim() || !name.trim())
|
|
60
|
+
continue;
|
|
61
|
+
if (pending.has(id) || ambiguous.has(id)) {
|
|
62
|
+
pending.delete(id);
|
|
63
|
+
ambiguous.add(id);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
pending.set(id, { id, name, arguments: call.function.arguments, sourceIndex, callIndex });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else if (message.role === 'tool' && typeof message.tool_call_id === 'string') {
|
|
70
|
+
const call = pending.get(message.tool_call_id);
|
|
71
|
+
if (!call || (typeof message.name === 'string' && message.name.trim() && message.name !== call.name))
|
|
72
|
+
return;
|
|
73
|
+
tools.push({ ...call, result: message.content });
|
|
74
|
+
pending.delete(message.tool_call_id);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
tools.sort((a, b) => a.sourceIndex - b.sourceIndex || a.callIndex - b.callIndex);
|
|
78
|
+
const timeline = [...texts, ...tools];
|
|
79
|
+
timeline.sort((a, b) => a.sourceIndex - b.sourceIndex || ('text' in a ? -1 : 'text' in b ? 1 : a.callIndex - b.callIndex));
|
|
80
|
+
return { texts, tools, timeline };
|
|
81
|
+
}
|
|
82
|
+
function resolveRange(value, total, kind) {
|
|
83
|
+
if (!record(value))
|
|
84
|
+
return fail('invalid_range', `${kind} requires range: {last:N} or {start:N,end:N}.`);
|
|
85
|
+
const hasLast = own(value, 'last'), hasStart = own(value, 'start'), hasEnd = own(value, 'end');
|
|
86
|
+
if (Object.keys(value).some(key => !['last', 'start', 'end'].includes(key)) || (hasLast && (hasStart || hasEnd)) || (!hasLast && (!hasStart || !hasEnd))) {
|
|
87
|
+
return fail('invalid_range', `${kind} range must use last alone or both start/end; ranges are 1-based and inclusive.`);
|
|
88
|
+
}
|
|
89
|
+
const integer = (n) => typeof n === 'number' && Number.isSafeInteger(n) && n >= 1;
|
|
90
|
+
if (hasLast) {
|
|
91
|
+
if (!integer(value.last) || value.last > total)
|
|
92
|
+
return fail('invalid_range', `${kind} range.last must be between 1 and ${total}; no selected units are silently omitted.`);
|
|
93
|
+
return { start: total - value.last + 1, end: total, total };
|
|
94
|
+
}
|
|
95
|
+
if (!integer(value.start) || !integer(value.end) || value.start > value.end || value.end > total) {
|
|
96
|
+
return fail('invalid_range', `${kind} range must satisfy 1 <= start <= end <= ${total}.`);
|
|
97
|
+
}
|
|
98
|
+
return { start: value.start, end: value.end, total };
|
|
99
|
+
}
|
|
100
|
+
function rangeLabel(range) { return `${range.start}-${range.end} of ${range.total}`; }
|
|
101
|
+
function textSection(selected, range) {
|
|
102
|
+
return `[Selected text history: ${rangeLabel(range)}]\n${selected.map((unit, index) => `${range.start + index}. ${unit.role}:\n${unit.text}`).join('\n\n')}`;
|
|
103
|
+
}
|
|
104
|
+
function toolSection(selected, range) {
|
|
105
|
+
return `[Selected tool history: ${rangeLabel(range)}; complete call/result pairs]\n${selected.map((unit, index) => `${range.start + index}. ${JSON.stringify(unit.name)} call_id=${JSON.stringify(unit.id)}\nCall arguments:\n${toolPayload(unit.arguments) || '(empty)'}\nTool result:\n${toolPayload(unit.result) || '(empty public result)'}`).join('\n\n')}`;
|
|
106
|
+
}
|
|
107
|
+
function selectSubagentCommunication(history, params, options = {}) {
|
|
108
|
+
const limit = options.maxBodyChars ?? MAX_BODY_CHARS;
|
|
109
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_BODY_CHARS)
|
|
110
|
+
return fail('invalid_content', 'maxBodyChars must be an integer between 1 and 30000.');
|
|
111
|
+
if (!record(params))
|
|
112
|
+
return fail('invalid_content', 'Communication parameters must be an object.');
|
|
113
|
+
const hasMessage = own(params, 'message'), hasPrompt = own(params, 'prompt'), hasContent = own(params, 'content');
|
|
114
|
+
if ((hasMessage || hasPrompt) && hasContent)
|
|
115
|
+
return fail('ambiguous_content', 'Use explicit message/prompt or content selections, not both.');
|
|
116
|
+
if (hasMessage && hasPrompt && params.message !== params.prompt)
|
|
117
|
+
return fail('ambiguous_content', 'message and prompt contain conflicting explicit text.');
|
|
118
|
+
const segments = [];
|
|
119
|
+
let body = '';
|
|
120
|
+
const append = (section) => {
|
|
121
|
+
const next = body ? `${body}\n\n${section}` : section;
|
|
122
|
+
if (next.length > limit)
|
|
123
|
+
fail('budget_exceeded', `Selected communication exceeds the ${limit}-character budget. Select a smaller range or provide a shorter summary; no selected text or tool pair was truncated.`);
|
|
124
|
+
body = next;
|
|
125
|
+
};
|
|
126
|
+
if (hasMessage || hasPrompt) {
|
|
127
|
+
const explicit = hasMessage ? params.message : params.prompt;
|
|
128
|
+
if (typeof explicit !== 'string' || !explicit.trim())
|
|
129
|
+
return fail('invalid_content', 'Explicit message/prompt must be non-empty text.');
|
|
130
|
+
append(explicit);
|
|
131
|
+
segments.push({ kind: 'message', units: 1 });
|
|
132
|
+
return { body, selection: { source: 'explicit', segments, bodyChars: body.length } };
|
|
133
|
+
}
|
|
134
|
+
const available = units(history);
|
|
135
|
+
if (!hasContent) {
|
|
136
|
+
if (!available.texts.length)
|
|
137
|
+
return fail('empty_history', 'No visible text history is available. Supply an explicit message or a scoped content selection.');
|
|
138
|
+
const range = resolveRange({ last: 1 }, available.texts.length, 'text_history');
|
|
139
|
+
append(textSection(available.texts.slice(-1), range));
|
|
140
|
+
segments.push({ kind: 'text_history', range, units: 1 });
|
|
141
|
+
return { body, selection: { source: 'default_history', segments, bodyChars: body.length } };
|
|
142
|
+
}
|
|
143
|
+
if (!Array.isArray(params.content) || !params.content.length)
|
|
144
|
+
return fail('invalid_content', 'content must contain at least one scoped selection.');
|
|
145
|
+
for (const part of params.content) {
|
|
146
|
+
if (!record(part) || !['summary', 'text_history', 'tool_history'].includes(String(part.kind)))
|
|
147
|
+
return fail('invalid_content', 'content kind must be summary, text_history, or tool_history.');
|
|
148
|
+
const kind = part.kind;
|
|
149
|
+
const total = kind === 'text_history' ? available.texts.length : kind === 'tool_history' ? available.tools.length : available.timeline.length;
|
|
150
|
+
const range = resolveRange(part.range, total, kind);
|
|
151
|
+
if (kind === 'summary') {
|
|
152
|
+
if (typeof part.text !== 'string' || !part.text.trim())
|
|
153
|
+
return fail('invalid_content', 'summary requires non-empty sender-provided text; the tool does not generate a summary.');
|
|
154
|
+
append(`[Sender-provided summary; visible history units ${rangeLabel(range)}]\n${part.text}`);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
if (own(part, 'text'))
|
|
158
|
+
return fail('invalid_content', `${kind} selects source history; use summary for sender-provided text.`);
|
|
159
|
+
append(kind === 'text_history'
|
|
160
|
+
? textSection(available.texts.slice(range.start - 1, range.end), range)
|
|
161
|
+
: toolSection(available.tools.slice(range.start - 1, range.end), range));
|
|
162
|
+
}
|
|
163
|
+
segments.push({ kind, range, units: range.end - range.start + 1 });
|
|
164
|
+
}
|
|
165
|
+
return { body, selection: { source: 'content', segments, bodyChars: body.length } };
|
|
166
|
+
}
|
|
167
|
+
//# sourceMappingURL=subagentCommunication.js.map
|
package/dist/core/types.d.ts
CHANGED
|
@@ -38,7 +38,16 @@ export interface GuideReceipt {
|
|
|
38
38
|
/** Durable user-authored media referenced while the Guide is pending. */
|
|
39
39
|
attachments?: ConversationImageAttachment[];
|
|
40
40
|
}
|
|
41
|
+
/** Presence is separate from value: a reported zero is not missing usage. */
|
|
42
|
+
export interface ProviderUsageReported {
|
|
43
|
+
input: boolean;
|
|
44
|
+
output: boolean;
|
|
45
|
+
cacheRead: boolean;
|
|
46
|
+
cacheWrite: boolean;
|
|
47
|
+
}
|
|
41
48
|
export interface StreamToken {
|
|
49
|
+
/** Transport-originated failure, distinct from model-generated prose. */
|
|
50
|
+
providerError?: boolean;
|
|
42
51
|
type: 'text' | 'tool_call' | 'status' | 'usage';
|
|
43
52
|
text: string;
|
|
44
53
|
toolCall?: {
|
|
@@ -52,6 +61,7 @@ export interface StreamToken {
|
|
|
52
61
|
output: number;
|
|
53
62
|
cacheRead: number;
|
|
54
63
|
cacheWrite: number;
|
|
64
|
+
reported?: ProviderUsageReported;
|
|
55
65
|
};
|
|
56
66
|
}
|
|
57
67
|
export interface ConversationImageAttachment {
|
|
@@ -100,7 +110,7 @@ export interface ChatMessage {
|
|
|
100
110
|
export interface AgentWorkEvent {
|
|
101
111
|
id: string;
|
|
102
112
|
conversationId: string;
|
|
103
|
-
type: 'start' | 'text' | 'response' | 'final_response' | 'tool_call' | 'tool_result' | 'thought' | 'thought_delta' | 'thought_result' | 'status' | 'done' | 'error' | 'queue_update' | 'guide' | 'conversation_title';
|
|
113
|
+
type: 'start' | 'text' | 'response' | 'final_response' | 'tool_call' | 'tool_result' | 'thought' | 'thought_delta' | 'thought_result' | 'status' | 'done' | 'error' | 'queue_update' | 'guide' | 'conversation_title' | 'conversation_list';
|
|
104
114
|
content: string;
|
|
105
115
|
mode: string;
|
|
106
116
|
model: string;
|
|
@@ -121,8 +131,24 @@ export interface AgentWorkEvent {
|
|
|
121
131
|
goalObjective?: string;
|
|
122
132
|
runId?: string;
|
|
123
133
|
createdAt: string;
|
|
134
|
+
images?: Array<{
|
|
135
|
+
dataUrl: string;
|
|
136
|
+
name?: string;
|
|
137
|
+
type?: string;
|
|
138
|
+
}>;
|
|
124
139
|
}>;
|
|
125
140
|
queuePaused?: boolean;
|
|
141
|
+
/** Authoritative metadata events bypass turn identity without changing selection. */
|
|
142
|
+
stateScope?: 'conversation' | 'workspace';
|
|
143
|
+
conversations?: Array<{
|
|
144
|
+
id: string;
|
|
145
|
+
title: string;
|
|
146
|
+
messageCount: number;
|
|
147
|
+
historyCount: number;
|
|
148
|
+
updatedAt: string;
|
|
149
|
+
pinned?: boolean;
|
|
150
|
+
order?: number;
|
|
151
|
+
}>;
|
|
126
152
|
workspaceId?: string;
|
|
127
153
|
workspaceKey?: string;
|
|
128
154
|
runtimeKey?: string;
|
|
@@ -132,6 +158,13 @@ export interface AgentWorkEvent {
|
|
|
132
158
|
actorId?: string;
|
|
133
159
|
generation?: number;
|
|
134
160
|
sequence?: number;
|
|
161
|
+
/** Original delta identities retained across IPC batching for snapshot reconciliation. */
|
|
162
|
+
coalescedDeltas?: Array<{
|
|
163
|
+
id: string;
|
|
164
|
+
sequence?: number;
|
|
165
|
+
content: string;
|
|
166
|
+
timestamp: string;
|
|
167
|
+
}>;
|
|
135
168
|
status?: GuideReceiptStatus | ConversationWorkRunStatus | 'stopping' | 'force_restarting';
|
|
136
169
|
guide?: GuideReceipt;
|
|
137
170
|
displayImage?: DisplayImageAttachment;
|
|
@@ -21,14 +21,17 @@ class WorkEventCoalescer {
|
|
|
21
21
|
}
|
|
22
22
|
const key = `${event.type}::${event.workspaceId || ''}::${event.conversationId}::${event.runtimeKey || ''}::${event.runId || ''}`;
|
|
23
23
|
const current = this.pending.get(key);
|
|
24
|
+
const deltas = event.coalescedDeltas?.length ? event.coalescedDeltas : [{ id: event.id, sequence: event.sequence, content: event.content, timestamp: event.timestamp }];
|
|
24
25
|
if (current) {
|
|
25
26
|
current.content += event.content;
|
|
27
|
+
current.deltas.push(...deltas);
|
|
26
28
|
current.event = event;
|
|
27
29
|
return;
|
|
28
30
|
}
|
|
29
31
|
const entry = {
|
|
30
32
|
event,
|
|
31
33
|
content: event.content,
|
|
34
|
+
deltas: deltas.slice(),
|
|
32
35
|
timer: setTimeout(() => this.flush(key), this.windowMs),
|
|
33
36
|
};
|
|
34
37
|
this.pending.set(key, entry);
|
|
@@ -40,7 +43,7 @@ class WorkEventCoalescer {
|
|
|
40
43
|
this.pending.delete(key);
|
|
41
44
|
clearTimeout(entry.timer);
|
|
42
45
|
if (entry.content)
|
|
43
|
-
this.emit({ ...entry.event, content: entry.content });
|
|
46
|
+
this.emit({ ...entry.event, content: entry.content, coalescedDeltas: entry.deltas });
|
|
44
47
|
}
|
|
45
48
|
flushAll() {
|
|
46
49
|
for (const key of [...this.pending.keys()])
|
|
@@ -19,12 +19,13 @@ export interface WslRuntimeIdentity {
|
|
|
19
19
|
pid: number;
|
|
20
20
|
pgid: number;
|
|
21
21
|
sessionId: number;
|
|
22
|
+
startTimeTicks: string;
|
|
23
|
+
bootId: string;
|
|
22
24
|
}
|
|
23
25
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* timeout/cancellation boundary so Electron's event loop is never blocked.
|
|
26
|
+
* Terminates the verified runtime and its descendants, including detached
|
|
27
|
+
* tools, through identity-bound Linux pidfds. The historical export name is
|
|
28
|
+
* retained for callers; no numeric process-group signal is issued.
|
|
28
29
|
*/
|
|
29
30
|
export declare function terminateWslRuntimeProcessGroup(distro: string, identityInput: WslRuntimeIdentity, runner?: WslCommandRunner, signal?: AbortSignal): Promise<void>;
|
|
30
31
|
export declare class WslAgentClient {
|
|
@@ -46,6 +47,10 @@ export declare class WslAgentClient {
|
|
|
46
47
|
private remotePid;
|
|
47
48
|
private remotePgid;
|
|
48
49
|
private remoteSessionId;
|
|
50
|
+
private remoteStartTimeTicks;
|
|
51
|
+
private remoteBootId;
|
|
52
|
+
private restartQuarantined;
|
|
53
|
+
private unconfirmedExit;
|
|
49
54
|
private startPromise;
|
|
50
55
|
constructor(distro: string, windowsRoot: string, windowsHostScript: string, runtimeTarget?: NormalizedConversationTarget | null, commandRunner?: WslCommandRunner);
|
|
51
56
|
subscribe(listener: WorkListener): () => void;
|
|
@@ -58,6 +63,9 @@ export declare class WslAgentClient {
|
|
|
58
63
|
pid: number;
|
|
59
64
|
pgid: number;
|
|
60
65
|
sessionId: number;
|
|
66
|
+
startTimeTicks: string;
|
|
67
|
+
bootId: string;
|
|
68
|
+
quarantined: boolean;
|
|
61
69
|
error: string;
|
|
62
70
|
};
|
|
63
71
|
start(): Promise<void>;
|
|
@@ -69,7 +77,10 @@ export declare class WslAgentClient {
|
|
|
69
77
|
abort(conversationId: string): Promise<boolean>;
|
|
70
78
|
requestStop(target: ConversationRuntimeTarget, runId?: string): Promise<WslAgentStopResult>;
|
|
71
79
|
snapshot(conversationId: string, workspace: WslAgentWorkspace | null): Promise<Record<string, unknown>>;
|
|
72
|
-
snapshotTarget(target: ConversationRuntimeTarget
|
|
80
|
+
snapshotTarget(target: ConversationRuntimeTarget, options?: {
|
|
81
|
+
window?: number;
|
|
82
|
+
before?: number;
|
|
83
|
+
}): Promise<Record<string, unknown>>;
|
|
73
84
|
rewind(target: ConversationRuntimeTarget, messageIndex: number): Promise<WslConversationRewindResult>;
|
|
74
85
|
enqueueGuide(target: ConversationRuntimeTarget, envelope: ConversationInputEnvelope): Promise<GuideReceipt>;
|
|
75
86
|
queueAction(target: ConversationRuntimeTarget, action: ConversationQueueAction, input?: ConversationQueueActionInput): Promise<Record<string, unknown>>;
|
|
@@ -88,6 +99,8 @@ export declare class WslAgentClient {
|
|
|
88
99
|
updateSetting(section: string, key: string, value: unknown): Promise<void>;
|
|
89
100
|
forceStopRuntimeGroup(signal?: AbortSignal): Promise<'terminated' | 'stale'>;
|
|
90
101
|
forceRestartRuntimeGroup(signal?: AbortSignal): Promise<void>;
|
|
102
|
+
private throwIfRestartQuarantined;
|
|
103
|
+
private confirmTerminatedGeneration;
|
|
91
104
|
terminalState(owner: TerminalTakeoverOwnerFilter, persistenceRoot?: string): Promise<TerminalTakeoverState[]>;
|
|
92
105
|
terminalWrite(owner: TerminalTakeoverOwnerFilter, sessionId: string, data: string, persistenceRoot?: string): Promise<{
|
|
93
106
|
ok: boolean;
|
|
@@ -43,6 +43,7 @@ const asyncProcess_1 = require("./asyncProcess");
|
|
|
43
43
|
const crypto_1 = require("crypto");
|
|
44
44
|
const fs_1 = require("fs");
|
|
45
45
|
const performanceDiagnostics_1 = require("./performanceDiagnostics");
|
|
46
|
+
const wslRuntimeProcessTree_1 = require("./wslRuntimeProcessTree");
|
|
46
47
|
const defaultWslCommandRunner = async (args, options) => await new Promise(resolve => {
|
|
47
48
|
let settled = false;
|
|
48
49
|
const finish = (result) => {
|
|
@@ -50,10 +51,9 @@ const defaultWslCommandRunner = async (args, options) => await new Promise(resol
|
|
|
50
51
|
return;
|
|
51
52
|
settled = true;
|
|
52
53
|
clearTimeout(hardTimeout);
|
|
53
|
-
options.signal?.removeEventListener('abort', abortListener);
|
|
54
54
|
resolve(result);
|
|
55
55
|
};
|
|
56
|
-
const
|
|
56
|
+
const aborted = () => ({
|
|
57
57
|
status: null,
|
|
58
58
|
stdout: '',
|
|
59
59
|
stderr: '',
|
|
@@ -71,41 +71,44 @@ const defaultWslCommandRunner = async (args, options) => await new Promise(resol
|
|
|
71
71
|
timedOut: true,
|
|
72
72
|
overflowed: false,
|
|
73
73
|
}), options.timeoutMs + 200);
|
|
74
|
-
options.signal?.
|
|
74
|
+
if (options.signal?.aborted) {
|
|
75
|
+
finish(aborted());
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// Once a Linux pidfd transaction may have paused its owned processes, external
|
|
79
|
+
// cancellation must wait for its bounded cleanup. Killing the Windows pipe
|
|
80
|
+
// mid-transaction could strand a held process. The helper has its own alarm
|
|
81
|
+
// and TERM/HUP/INT cleanup, and the outer timeout is the final failure bound.
|
|
75
82
|
void (0, asyncProcess_1.runAsyncProcess)('wsl.exe', args, {
|
|
76
83
|
timeoutMs: options.timeoutMs,
|
|
77
|
-
signal: options.signal,
|
|
78
84
|
maxBuffer: 64 * 1024,
|
|
79
85
|
windowsHide: true,
|
|
80
86
|
}).then(finish);
|
|
81
87
|
});
|
|
82
88
|
/**
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* timeout/cancellation boundary so Electron's event loop is never blocked.
|
|
89
|
+
* Terminates the verified runtime and its descendants, including detached
|
|
90
|
+
* tools, through identity-bound Linux pidfds. The historical export name is
|
|
91
|
+
* retained for callers; no numeric process-group signal is issued.
|
|
87
92
|
*/
|
|
88
93
|
async function terminateWslRuntimeProcessGroup(distro, identityInput, runner = defaultWslCommandRunner, signal) {
|
|
89
94
|
const identity = normalizeRuntimeIdentity(identityInput);
|
|
90
95
|
if (!identity || identity.pid !== identity.pgid || identity.pgid !== identity.sessionId) {
|
|
91
|
-
throw new Error(`WSL runtime process group termination refused: unverified pid/pgid/session identity`);
|
|
96
|
+
throw new Error(`WSL runtime process group termination refused: unverified pid/pgid/session/birth identity`);
|
|
92
97
|
}
|
|
93
98
|
const target = identity.pgid;
|
|
94
|
-
const
|
|
95
|
-
`if kill -0 -- "-${target}" 2>/dev/null; then kill -KILL -- "-${target}" 2>/dev/null || { echo 'runtime process group kill failed' >&2; exit 71; }; fi`,
|
|
96
|
-
`for attempt in {1..24}; do if ! kill -0 -- "-${target}" 2>/dev/null; then printf 'terminated:%s\\n' "${target}"; exit 0; fi; sleep 0.05; done`,
|
|
97
|
-
`echo 'runtime process group is still alive' >&2`,
|
|
98
|
-
`exit 72`,
|
|
99
|
-
].join('; ');
|
|
100
|
-
// The Linux poll itself is bounded to about 1.2 s, but starting a second
|
|
101
|
-
// wsl.exe/bash helper and reaping it can exceed two seconds under Windows
|
|
102
|
-
// load even while the distribution is already warm. Keep this well below
|
|
103
|
-
// the 30 s cold-start budget while leaving enough room to verify teardown.
|
|
104
|
-
const result = await runner(['-d', distro, '--', 'bash', '-lc', command], { timeoutMs: 5_500, signal });
|
|
99
|
+
const result = await runner(['-d', distro, '--', 'bash', '-lc', (0, wslRuntimeProcessTree_1.wslRuntimeProcessTreeCommand)(identity)], { timeoutMs: 8_000, signal });
|
|
105
100
|
if (result.status !== 0 || result.error || result.timedOut || result.aborted || result.overflowed) {
|
|
106
101
|
const detail = String(result.stderr || result.error || `exit ${String(result.status)}`).trim().slice(-500);
|
|
107
102
|
throw new Error(`WSL runtime process group termination failed for pgid ${target}: ${detail || 'unknown helper failure'}`);
|
|
108
103
|
}
|
|
104
|
+
let receipt = null;
|
|
105
|
+
try {
|
|
106
|
+
receipt = JSON.parse(result.stdout.trim());
|
|
107
|
+
}
|
|
108
|
+
catch { }
|
|
109
|
+
if (receipt?.terminated !== true || receipt.method !== 'pidfd_tree' || receipt.rootPid !== identity.pid) {
|
|
110
|
+
throw new Error(`WSL runtime process tree termination remains unconfirmed: missing identity-bound completion receipt`);
|
|
111
|
+
}
|
|
109
112
|
}
|
|
110
113
|
class WslAgentClient {
|
|
111
114
|
distro;
|
|
@@ -126,6 +129,10 @@ class WslAgentClient {
|
|
|
126
129
|
remotePid = 0;
|
|
127
130
|
remotePgid = 0;
|
|
128
131
|
remoteSessionId = 0;
|
|
132
|
+
remoteStartTimeTicks = '';
|
|
133
|
+
remoteBootId = '';
|
|
134
|
+
restartQuarantined = '';
|
|
135
|
+
unconfirmedExit = null;
|
|
129
136
|
startPromise = null;
|
|
130
137
|
constructor(distro, windowsRoot, windowsHostScript, runtimeTarget = null, commandRunner = defaultWslCommandRunner) {
|
|
131
138
|
this.distro = distro;
|
|
@@ -153,10 +160,14 @@ class WslAgentClient {
|
|
|
153
160
|
pid: this.remotePid,
|
|
154
161
|
pgid: this.remotePgid,
|
|
155
162
|
sessionId: this.remoteSessionId,
|
|
163
|
+
startTimeTicks: this.remoteStartTimeTicks,
|
|
164
|
+
bootId: this.remoteBootId,
|
|
165
|
+
quarantined: !!this.restartQuarantined,
|
|
156
166
|
error: this.lastError,
|
|
157
167
|
};
|
|
158
168
|
}
|
|
159
169
|
async start() {
|
|
170
|
+
this.throwIfRestartQuarantined();
|
|
160
171
|
if (this.child && !this.child.killed)
|
|
161
172
|
return;
|
|
162
173
|
if (this.startPromise)
|
|
@@ -226,6 +237,7 @@ class WslAgentClient {
|
|
|
226
237
|
catch { }
|
|
227
238
|
this.detachChild(child, error);
|
|
228
239
|
this.lastError = error.message;
|
|
240
|
+
this.restartQuarantined = this.lastError;
|
|
229
241
|
throw error;
|
|
230
242
|
}
|
|
231
243
|
if (this.child !== child || this.childGeneration !== generation)
|
|
@@ -233,6 +245,8 @@ class WslAgentClient {
|
|
|
233
245
|
this.remotePid = identity.pid;
|
|
234
246
|
this.remotePgid = identity.pgid;
|
|
235
247
|
this.remoteSessionId = identity.sessionId;
|
|
248
|
+
this.remoteStartTimeTicks = identity.startTimeTicks;
|
|
249
|
+
this.remoteBootId = identity.bootId;
|
|
236
250
|
this.lastError = '';
|
|
237
251
|
}
|
|
238
252
|
finally {
|
|
@@ -240,20 +254,29 @@ class WslAgentClient {
|
|
|
240
254
|
}
|
|
241
255
|
}
|
|
242
256
|
async stop() {
|
|
257
|
+
// Pool disposal must not erase an owner whose descendant cleanup was never
|
|
258
|
+
// confirmed, including after its launcher exit cleared this.child.
|
|
259
|
+
this.throwIfRestartQuarantined();
|
|
243
260
|
const child = this.child;
|
|
244
261
|
if (!child)
|
|
245
262
|
return;
|
|
246
263
|
const generation = this.childGeneration;
|
|
247
|
-
const identity = { pid: this.remotePid, pgid: this.remotePgid, sessionId: this.remoteSessionId };
|
|
264
|
+
const identity = { pid: this.remotePid, pgid: this.remotePgid, sessionId: this.remoteSessionId, startTimeTicks: this.remoteStartTimeTicks, bootId: this.remoteBootId };
|
|
265
|
+
// Keep the identity anchor alive until the supervisor has captured all
|
|
266
|
+
// descendants. A detached tool can outlive a worker that exits first.
|
|
248
267
|
try {
|
|
249
|
-
await this.request('shutdown',
|
|
268
|
+
await this.request('shutdown', { supervisorTerminates: true }, 250);
|
|
250
269
|
}
|
|
251
270
|
catch { }
|
|
271
|
+
if (this.childGeneration !== generation)
|
|
272
|
+
return;
|
|
252
273
|
try {
|
|
253
274
|
await terminateWslRuntimeProcessGroup(this.distro, identity, this.commandRunner);
|
|
275
|
+
this.confirmTerminatedGeneration(generation);
|
|
254
276
|
}
|
|
255
277
|
catch (error) {
|
|
256
278
|
this.lastError = error instanceof Error ? error.message : String(error);
|
|
279
|
+
this.restartQuarantined = this.lastError;
|
|
257
280
|
throw error;
|
|
258
281
|
}
|
|
259
282
|
if (this.childGeneration !== generation)
|
|
@@ -298,9 +321,9 @@ class WslAgentClient {
|
|
|
298
321
|
const target = await this.mapTarget(this.targetFromLegacy(conversationId, workspace));
|
|
299
322
|
return await this.request('snapshot', { target }, 15000);
|
|
300
323
|
}
|
|
301
|
-
async snapshotTarget(target) {
|
|
324
|
+
async snapshotTarget(target, options = {}) {
|
|
302
325
|
await this.start();
|
|
303
|
-
return await this.request('snapshot', { target: await this.mapTarget(target) }, 15000);
|
|
326
|
+
return await this.request('snapshot', { target: await this.mapTarget(target), options }, 15000);
|
|
304
327
|
}
|
|
305
328
|
async rewind(target, messageIndex) {
|
|
306
329
|
await this.start();
|
|
@@ -363,17 +386,18 @@ class WslAgentClient {
|
|
|
363
386
|
await this.request('update_setting', { section, key, value }, 5_000);
|
|
364
387
|
}
|
|
365
388
|
async forceStopRuntimeGroup(signal) {
|
|
389
|
+
this.throwIfRestartQuarantined();
|
|
366
390
|
const child = this.child;
|
|
367
391
|
const generation = this.childGeneration;
|
|
368
392
|
const identity = {
|
|
369
393
|
pid: this.remotePid,
|
|
370
394
|
pgid: this.remotePgid,
|
|
371
395
|
sessionId: this.remoteSessionId,
|
|
396
|
+
startTimeTicks: this.remoteStartTimeTicks,
|
|
397
|
+
bootId: this.remoteBootId,
|
|
372
398
|
};
|
|
373
399
|
if (!child || child.killed)
|
|
374
400
|
throw new Error('WSL runtime process group termination failed: runtime launcher is not connected');
|
|
375
|
-
if (!normalizeRuntimeIdentity(identity))
|
|
376
|
-
throw new Error('WSL runtime process group termination failed: runtime identity was not recorded');
|
|
377
401
|
for (const [requestId, run] of this.hostToolRuns) {
|
|
378
402
|
if (run.generation !== generation)
|
|
379
403
|
continue;
|
|
@@ -384,9 +408,11 @@ class WslAgentClient {
|
|
|
384
408
|
this.hostToolHandler?.cancelTarget?.(this.runtimeTarget.runtimeKey);
|
|
385
409
|
try {
|
|
386
410
|
await terminateWslRuntimeProcessGroup(this.distro, identity, this.commandRunner, signal);
|
|
411
|
+
this.confirmTerminatedGeneration(generation);
|
|
387
412
|
}
|
|
388
413
|
catch (error) {
|
|
389
414
|
this.lastError = error instanceof Error ? error.message : String(error);
|
|
415
|
+
this.restartQuarantined = this.lastError;
|
|
390
416
|
throw error;
|
|
391
417
|
}
|
|
392
418
|
if (this.childGeneration !== generation)
|
|
@@ -402,11 +428,25 @@ class WslAgentClient {
|
|
|
402
428
|
return 'terminated';
|
|
403
429
|
}
|
|
404
430
|
async forceRestartRuntimeGroup(signal) {
|
|
431
|
+
this.throwIfRestartQuarantined();
|
|
405
432
|
const outcome = await this.forceStopRuntimeGroup(signal);
|
|
406
433
|
if (outcome === 'stale')
|
|
407
434
|
return;
|
|
408
435
|
await this.start();
|
|
409
436
|
}
|
|
437
|
+
throwIfRestartQuarantined() {
|
|
438
|
+
if (this.restartQuarantined)
|
|
439
|
+
throw new Error(`WSL runtime is quarantined until the app backend is restarted: ${this.restartQuarantined}`);
|
|
440
|
+
}
|
|
441
|
+
confirmTerminatedGeneration(generation) {
|
|
442
|
+
if (this.unconfirmedExit?.generation !== generation)
|
|
443
|
+
return;
|
|
444
|
+
if (this.restartQuarantined === this.unconfirmedExit.reason) {
|
|
445
|
+
this.restartQuarantined = '';
|
|
446
|
+
this.lastError = '';
|
|
447
|
+
}
|
|
448
|
+
this.unconfirmedExit = null;
|
|
449
|
+
}
|
|
410
450
|
async terminalState(owner, persistenceRoot) {
|
|
411
451
|
await this.start();
|
|
412
452
|
return await this.request('terminal_state', { owner, persistenceRoot: persistenceRoot ? await this.toWslPath(persistenceRoot) : undefined }, 15000);
|
|
@@ -573,6 +613,12 @@ class WslAgentClient {
|
|
|
573
613
|
if (this.child !== exitedChild)
|
|
574
614
|
return;
|
|
575
615
|
this.lastError = this.stderrBuffer || this.lastError;
|
|
616
|
+
if (this.remoteStartTimeTicks && !this.restartQuarantined) {
|
|
617
|
+
const reason = 'WSL runtime owner exited before descendant termination was confirmed';
|
|
618
|
+
this.unconfirmedExit = { generation: this.childGeneration, reason };
|
|
619
|
+
this.restartQuarantined = reason;
|
|
620
|
+
this.lastError = reason;
|
|
621
|
+
}
|
|
576
622
|
const error = new Error(`WSL Agent backend exited (${code ?? 'unknown'}): ${this.lastError || 'no stderr'}`);
|
|
577
623
|
this.detachChild(exitedChild, error);
|
|
578
624
|
}
|
|
@@ -583,6 +629,8 @@ class WslAgentClient {
|
|
|
583
629
|
this.remotePid = 0;
|
|
584
630
|
this.remotePgid = 0;
|
|
585
631
|
this.remoteSessionId = 0;
|
|
632
|
+
this.remoteStartTimeTicks = '';
|
|
633
|
+
this.remoteBootId = '';
|
|
586
634
|
const generation = this.childGeneration;
|
|
587
635
|
for (const [requestId, run] of this.hostToolRuns) {
|
|
588
636
|
if (run.generation !== generation)
|
|
@@ -623,7 +671,11 @@ function normalizeRuntimeIdentity(input) {
|
|
|
623
671
|
const sessionId = Number(input?.sessionId || 0);
|
|
624
672
|
if (![pid, pgid, sessionId].every(value => Number.isSafeInteger(value) && value > 1))
|
|
625
673
|
return null;
|
|
626
|
-
|
|
674
|
+
const startTimeTicks = String(input?.startTimeTicks || '');
|
|
675
|
+
const bootId = String(input?.bootId || '').toLowerCase();
|
|
676
|
+
if (!/^\d+$/.test(startTimeTicks) || !/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(bootId))
|
|
677
|
+
return null;
|
|
678
|
+
return { pid, pgid, sessionId, startTimeTicks, bootId };
|
|
627
679
|
}
|
|
628
680
|
function shellQuote(value) {
|
|
629
681
|
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
@@ -96,6 +96,10 @@ export type WslAgentRequest = {
|
|
|
96
96
|
conversationId?: string;
|
|
97
97
|
target?: ConversationRuntimeTarget;
|
|
98
98
|
workspace?: WslAgentWorkspace | null;
|
|
99
|
+
options?: {
|
|
100
|
+
window?: number;
|
|
101
|
+
before?: number;
|
|
102
|
+
};
|
|
99
103
|
};
|
|
100
104
|
} | {
|
|
101
105
|
id: string;
|
|
@@ -230,6 +234,9 @@ export type WslAgentRequest = {
|
|
|
230
234
|
} | {
|
|
231
235
|
id: string;
|
|
232
236
|
method: 'shutdown';
|
|
237
|
+
params?: {
|
|
238
|
+
supervisorTerminates?: boolean;
|
|
239
|
+
};
|
|
233
240
|
};
|
|
234
241
|
export type WslAgentResponse = {
|
|
235
242
|
id: string;
|
|
@@ -7,7 +7,10 @@ export interface WslTargetRuntimeClient {
|
|
|
7
7
|
subscribe(listener: (event: AgentWorkEvent) => void): () => void;
|
|
8
8
|
setHostToolHandler(handler: WslHostToolHandler | null): void;
|
|
9
9
|
prompt(params: WslAgentPromptRequest): Promise<WslAgentPromptResult>;
|
|
10
|
-
snapshotTarget(target: ConversationRuntimeTarget
|
|
10
|
+
snapshotTarget(target: ConversationRuntimeTarget, options?: {
|
|
11
|
+
window?: number;
|
|
12
|
+
before?: number;
|
|
13
|
+
}): Promise<Record<string, unknown>>;
|
|
11
14
|
rewind(target: ConversationRuntimeTarget, messageIndex: number): Promise<WslConversationRewindResult>;
|
|
12
15
|
requestStop(target: ConversationRuntimeTarget, runId?: string): Promise<WslAgentStopResult>;
|
|
13
16
|
enqueueGuide(target: ConversationRuntimeTarget, envelope: ConversationInputEnvelope): Promise<GuideReceipt>;
|
|
@@ -71,7 +74,10 @@ export declare class WslAgentRuntimePool {
|
|
|
71
74
|
setHostToolHandler(handler: WslHostToolHandler | null): void;
|
|
72
75
|
prompt(params: WslAgentPromptRequest): Promise<WslAgentPromptResult>;
|
|
73
76
|
prewarm(target: ConversationRuntimeTarget): Promise<void>;
|
|
74
|
-
snapshot(target: ConversationRuntimeTarget
|
|
77
|
+
snapshot(target: ConversationRuntimeTarget, options?: {
|
|
78
|
+
window?: number;
|
|
79
|
+
before?: number;
|
|
80
|
+
}): Promise<Record<string, unknown>>;
|
|
75
81
|
rewind(target: ConversationRuntimeTarget, messageIndex: number): Promise<WslConversationRewindResult>;
|
|
76
82
|
requestStop(target: ConversationRuntimeTarget, runId?: string): Promise<WslPoolStopResult>;
|
|
77
83
|
enqueueGuide(envelope: ConversationInputEnvelope): Promise<GuideReceipt>;
|