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.
Files changed (68) hide show
  1. package/dist/conversation-utility-host.bundle.cjs +4565 -2998
  2. package/dist/conversation-utility-host.js +1 -1
  3. package/dist/core/agent.d.ts +108 -16
  4. package/dist/core/agent.js +698 -195
  5. package/dist/core/agentKernel/agent.d.ts +1 -0
  6. package/dist/core/agentKernel/agent.js +9 -0
  7. package/dist/core/agentKernelDiagnostics.d.ts +4 -6
  8. package/dist/core/agentKernelDiagnostics.js +12 -9
  9. package/dist/core/agentKernelRunner.d.ts +5 -0
  10. package/dist/core/agentKernelRunner.js +250 -107
  11. package/dist/core/autoRouter.js +17 -27
  12. package/dist/core/config.d.ts +2 -0
  13. package/dist/core/config.js +7 -1
  14. package/dist/core/conversationCommandState.d.ts +17 -0
  15. package/dist/core/conversationCommandState.js +140 -0
  16. package/dist/core/conversationKernel.d.ts +25 -4
  17. package/dist/core/conversationKernel.js +360 -89
  18. package/dist/core/conversationListEvent.d.ts +6 -0
  19. package/dist/core/conversationListEvent.js +14 -0
  20. package/dist/core/electronUtilityAgentClient.d.ts +4 -1
  21. package/dist/core/electronUtilityAgentClient.js +4 -4
  22. package/dist/core/electronUtilityRuntimePool.d.ts +8 -2
  23. package/dist/core/electronUtilityRuntimePool.js +11 -5
  24. package/dist/core/installUpdate.js +41 -29
  25. package/dist/core/modelResponseHealth.d.ts +17 -0
  26. package/dist/core/modelResponseHealth.js +193 -0
  27. package/dist/core/providerUsageAccounting.d.ts +47 -0
  28. package/dist/core/providerUsageAccounting.js +71 -0
  29. package/dist/core/requestContextEstimate.d.ts +18 -0
  30. package/dist/core/requestContextEstimate.js +54 -0
  31. package/dist/core/subagent.d.ts +89 -5
  32. package/dist/core/subagent.js +264 -41
  33. package/dist/core/subagentCommunication.d.ts +43 -0
  34. package/dist/core/subagentCommunication.js +167 -0
  35. package/dist/core/types.d.ts +34 -1
  36. package/dist/core/utilityAgentProtocol.d.ts +4 -0
  37. package/dist/core/workEventCoalescer.js +4 -1
  38. package/dist/core/wslAgentClient.d.ts +18 -5
  39. package/dist/core/wslAgentClient.js +79 -27
  40. package/dist/core/wslAgentProtocol.d.ts +7 -0
  41. package/dist/core/wslAgentRuntimePool.d.ts +8 -2
  42. package/dist/core/wslAgentRuntimePool.js +19 -6
  43. package/dist/core/wslRuntimeProcessTree.d.ts +10 -0
  44. package/dist/core/wslRuntimeProcessTree.js +104 -0
  45. package/dist/llm/provider.d.ts +5 -12
  46. package/dist/llm/provider.js +150 -269
  47. package/dist/main.js +582 -356
  48. package/dist/preload.js +3 -3
  49. package/dist/providers/chat-completions.adapter.d.ts +2 -0
  50. package/dist/providers/chat-completions.adapter.js +84 -36
  51. package/dist/providers/provider-adapter.d.ts +2 -0
  52. package/dist/providers/provider-events.d.ts +23 -14
  53. package/dist/providers/provider-events.js +148 -43
  54. package/dist/providers/provider-headers.d.ts +7 -3
  55. package/dist/providers/provider-headers.js +109 -39
  56. package/dist/providers/provider-request-compat.d.ts +6 -0
  57. package/dist/providers/provider-request-compat.js +37 -0
  58. package/dist/providers/responses.adapter.d.ts +2 -0
  59. package/dist/providers/responses.adapter.js +192 -126
  60. package/dist/server.d.ts +9 -2
  61. package/dist/server.js +100 -24
  62. package/dist/tools/index.js +17 -1
  63. package/dist/ui/index.html +2515 -693
  64. package/dist/ui/lucide-sprite.svg +7 -0
  65. package/dist/ui/startup.html +6 -6
  66. package/dist/wsl-agent-host.bundle.cjs +4575 -3006
  67. package/dist/wsl-agent-host.js +9 -4
  68. 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
@@ -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;
@@ -78,6 +78,10 @@ export type UtilityAgentRequest = {
78
78
  method: 'snapshot';
79
79
  params: {
80
80
  target: ConversationRuntimeTarget;
81
+ options?: {
82
+ window?: number;
83
+ before?: number;
84
+ };
81
85
  };
82
86
  } | {
83
87
  id: string;
@@ -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
- * Kills one previously verified WSL session/process group and proves that the
25
- * group no longer exists before its caller may launch a replacement. The bash
26
- * helper has its own bounded poll loop; the Windows child runner adds a second
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): Promise<Record<string, unknown>>;
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 abortListener = () => finish({
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?.addEventListener('abort', abortListener, { once: true });
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
- * Kills one previously verified WSL session/process group and proves that the
84
- * group no longer exists before its caller may launch a replacement. The bash
85
- * helper has its own bounded poll loop; the Windows child runner adds a second
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 command = [
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', undefined, 250);
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
- return { pid, pgid, sessionId };
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): Promise<Record<string, unknown>>;
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): Promise<Record<string, unknown>>;
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>;