@shanepadgett/tau-agent 0.42.3 → 0.44.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/README.md +0 -1
- package/docs/context.md +1 -1
- package/docs/extending-tau-agent.md +1 -2
- package/extensions/attention/README.md +1 -1
- package/extensions/cache-diagnostics/index.ts +4 -0
- package/extensions/context/README.md +1 -24
- package/extensions/context/definitions.ts +1 -52
- package/extensions/context/index.ts +1 -150
- package/extensions/context/panel.ts +0 -72
- package/extensions/explore/guidance.ts +9 -1
- package/extensions/footer/README.md +1 -1
- package/extensions/footer/index.ts +3 -25
- package/extensions/qna/choice-question-body.ts +7 -2
- package/extensions/run-summary/README.md +1 -1
- package/extensions/run-summary/index.ts +18 -25
- package/extensions/runtime-context/README.md +1 -1
- package/extensions/runtime-context/context.ts +1 -1
- package/extensions/runtime-context/index.ts +10 -16
- package/extensions/script-runner/index.ts +21 -29
- package/extensions/silent-command-runner/README.md +0 -2
- package/extensions/silent-command-runner/index.ts +12 -41
- package/extensions/soul/README.md +5 -8
- package/extensions/soul/context.ts +115 -0
- package/extensions/soul/index.ts +141 -7
- package/extensions/soul/prompt.ts +47 -102
- package/extensions/soul/state.ts +114 -0
- package/extensions/soul/tools.ts +31 -0
- package/extensions/tau-help/help.md +6 -18
- package/extensions/tau-help/index.ts +10 -4
- package/extensions/tool-approval/README.md +4 -2
- package/extensions/tool-approval/index.ts +141 -53
- package/extensions/tool-approval/panel.ts +153 -0
- package/extensions/tool-loader/README.md +1 -1
- package/extensions/tool-loader/index.ts +91 -22
- package/package.json +2 -2
- package/schemas/tau.schema.json +0 -104
- package/shared/bounded-text-result.ts +0 -1
- package/shared/events.ts +19 -15
- package/shared/isolated-session.ts +1 -2
- package/shared/model-effort.ts +15 -21
- package/shared/prompt-contributions.ts +24 -0
- package/src/index.ts +1 -1
- package/docs/subagents.md +0 -92
- package/extensions/auto-compact/README.md +0 -9
- package/extensions/auto-compact/index.ts +0 -122
- package/extensions/auto-compact/settings.ts +0 -30
- package/extensions/context/settings.ts +0 -62
- package/extensions/context/sync.ts +0 -276
- package/extensions/context/validation.ts +0 -88
- package/extensions/effort/README.md +0 -7
- package/extensions/effort/index.ts +0 -134
- package/extensions/effort/state.ts +0 -18
- package/extensions/qna/inline-editor-row.ts +0 -56
- package/extensions/soul/overseer.ts +0 -273
- package/extensions/soul/settings.ts +0 -34
- package/extensions/subagent/README.md +0 -67
- package/extensions/subagent/agents/context-sync.md +0 -244
- package/extensions/subagent/agents/dormant/generalist.md +0 -33
- package/extensions/subagent/agents/scout.md +0 -104
- package/extensions/subagent/agents/web-research.md +0 -101
- package/extensions/subagent/agents.ts +0 -261
- package/extensions/subagent/cmux-dashboard.ts +0 -495
- package/extensions/subagent/index.ts +0 -425
- package/extensions/subagent/panel.ts +0 -124
- package/extensions/subagent/render.ts +0 -74
- package/extensions/subagent/resume.ts +0 -78
- package/extensions/subagent/run.ts +0 -641
- package/extensions/subagent/runtime.ts +0 -1296
- package/extensions/subagent/session-resource.ts +0 -61
- package/extensions/subagent/settings.ts +0 -18
|
@@ -1,495 +0,0 @@
|
|
|
1
|
-
import { mkdtemp, rename, rm, writeFile } from "node:fs/promises";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import type { ExecOptions, ExecResult } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import type { SubagentInvocationSnapshot } from "./run.ts";
|
|
6
|
-
|
|
7
|
-
const WRITE_DEBOUNCE_MS = 150;
|
|
8
|
-
const CLOSE_DELAY_MS = 2000;
|
|
9
|
-
const CMUX_TIMEOUT_MS = 2500;
|
|
10
|
-
const DASHBOARD_FILENAME = "dashboard.md";
|
|
11
|
-
|
|
12
|
-
export type CmuxExec = (command: string, args: string[], options?: ExecOptions) => Promise<ExecResult>;
|
|
13
|
-
|
|
14
|
-
export interface CmuxClock {
|
|
15
|
-
now(): number;
|
|
16
|
-
setTimeout(handler: () => void, ms: number): unknown;
|
|
17
|
-
clearTimeout(handle: unknown): void;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface CmuxDashboardOptions {
|
|
21
|
-
exec: CmuxExec;
|
|
22
|
-
clock?: CmuxClock;
|
|
23
|
-
env?: NodeJS.ProcessEnv;
|
|
24
|
-
notify?: (message: string) => void;
|
|
25
|
-
/** When false, never open a new surface (unresolved prior orphans). */
|
|
26
|
-
canOpen?: () => boolean;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface DashboardOrphan {
|
|
30
|
-
workspaceId?: string;
|
|
31
|
-
surfaceId?: string;
|
|
32
|
-
directory: string;
|
|
33
|
-
path: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface CmuxDashboard {
|
|
37
|
-
/** Soft enable for interactive TUI sessions. Env still required. */
|
|
38
|
-
setInteractive(enabled: boolean): void;
|
|
39
|
-
onSnapshot(snapshot: SubagentInvocationSnapshot): void;
|
|
40
|
-
/** Close owned surface. Returns orphans that still need ownership/retry. */
|
|
41
|
-
shutdown(): Promise<DashboardOrphan[]>;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
interface OwnedSurface {
|
|
45
|
-
workspaceId: string;
|
|
46
|
-
surfaceId: string;
|
|
47
|
-
directory: string;
|
|
48
|
-
path: string;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Directory retained after ambiguous open until shutdown can best-effort clean. */
|
|
52
|
-
interface RetainedDirectory {
|
|
53
|
-
directory: string;
|
|
54
|
-
path: string;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function defaultClock(): CmuxClock {
|
|
58
|
-
return {
|
|
59
|
-
now: () => Date.now(),
|
|
60
|
-
setTimeout: (handler, ms) => setTimeout(handler, ms),
|
|
61
|
-
clearTimeout: (handle) => {
|
|
62
|
-
clearTimeout(handle as ReturnType<typeof setTimeout>);
|
|
63
|
-
},
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function cap(text: string | undefined, limit: number): string {
|
|
68
|
-
if (!text) return "";
|
|
69
|
-
const normalized = text.replace(/\r\n/g, "\n").trimEnd();
|
|
70
|
-
if (normalized.length <= limit) return normalized;
|
|
71
|
-
return `${normalized.slice(0, limit - 1)}…`;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function tableCell(text: string, limit: number): string {
|
|
75
|
-
const compact = cap(text.replace(/\s+/g, " ").trim(), limit);
|
|
76
|
-
return compact.replaceAll("|", "|").replaceAll("<", "<").replaceAll(">", ">");
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function quote(text: string): string {
|
|
80
|
-
return cap(text, 1200)
|
|
81
|
-
.replaceAll("<", "<")
|
|
82
|
-
.replaceAll(">", ">")
|
|
83
|
-
.split("\n")
|
|
84
|
-
.map((line) => `> ${line}`)
|
|
85
|
-
.join("\n");
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function elapsed(durationMs: number): string {
|
|
89
|
-
if (durationMs < 1000) return "<1s";
|
|
90
|
-
if (durationMs < 60_000) return `${(durationMs / 1000).toFixed(1)}s`;
|
|
91
|
-
const minutes = Math.floor(durationMs / 60_000);
|
|
92
|
-
const seconds = Math.floor((durationMs % 60_000) / 1000);
|
|
93
|
-
return `${minutes}m ${seconds}s`;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function dashboardState(status: SubagentInvocationSnapshot["status"]): string {
|
|
97
|
-
switch (status) {
|
|
98
|
-
case "waiting":
|
|
99
|
-
return "queued";
|
|
100
|
-
case "completed":
|
|
101
|
-
return "done";
|
|
102
|
-
case "aborted":
|
|
103
|
-
return "stopped";
|
|
104
|
-
default:
|
|
105
|
-
return status;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function dashboardCounts(ordered: readonly SubagentInvocationSnapshot[]): string {
|
|
110
|
-
const activeCount = ordered.filter((snapshot) =>
|
|
111
|
-
["waiting", "starting", "running"].includes(snapshot.status),
|
|
112
|
-
).length;
|
|
113
|
-
const doneCount = ordered.filter((snapshot) => snapshot.status === "completed").length;
|
|
114
|
-
const failedCount = ordered.filter((snapshot) => snapshot.status === "failed").length;
|
|
115
|
-
const stoppedCount = ordered.filter((snapshot) => snapshot.status === "aborted").length;
|
|
116
|
-
const counts = [`${activeCount} active`, `${doneCount} done`];
|
|
117
|
-
if (failedCount > 0) counts.push(`${failedCount} failed`);
|
|
118
|
-
if (stoppedCount > 0) counts.push(`${stoppedCount} stopped`);
|
|
119
|
-
return counts.join(" · ");
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function dashboardTableRow(details: SubagentInvocationSnapshot): string {
|
|
123
|
-
const latest = details.actions.at(-1);
|
|
124
|
-
const currentTool = details.currentActivity?.match(/^\S+/)?.[0];
|
|
125
|
-
const lastTool = currentTool ?? latest?.tool ?? "";
|
|
126
|
-
const ctx = typeof details.contextPercent === "number" ? `${details.contextPercent.toFixed(1)}%` : "—";
|
|
127
|
-
return `| ${tableCell(`${details.displayName} (${details.agent})`, 48)} | ${dashboardState(details.status)} | ${tableCell(lastTool, 32) || "—"} | ${details.toolCalls} | $${details.usage.cost.toFixed(4)} | ${ctx} | ${elapsed(details.durationMs)} |`;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function dashboardInputSection(details: SubagentInvocationSnapshot): string[] {
|
|
131
|
-
const lines = [
|
|
132
|
-
`### ${tableCell(details.displayName, 80)}`,
|
|
133
|
-
"",
|
|
134
|
-
tableCell(details.agent, 80),
|
|
135
|
-
"",
|
|
136
|
-
quote(details.task) || "> _(empty)_",
|
|
137
|
-
];
|
|
138
|
-
if (details.files.length > 0) {
|
|
139
|
-
lines.push("", "**Files**", "");
|
|
140
|
-
for (const file of details.files) lines.push(`- ${tableCell(file, 300)}`);
|
|
141
|
-
}
|
|
142
|
-
if (details.error) lines.push("", "**Error**", "", quote(details.error));
|
|
143
|
-
lines.push("");
|
|
144
|
-
return lines;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function formatDashboardMarkdown(snapshots: readonly SubagentInvocationSnapshot[]): string {
|
|
148
|
-
const ordered = [...snapshots].sort(
|
|
149
|
-
(a, b) => a.startedAt - b.startedAt || a.invocationId.localeCompare(b.invocationId),
|
|
150
|
-
);
|
|
151
|
-
const lines = ["# Subagents", "", dashboardCounts(ordered), ""];
|
|
152
|
-
if (ordered.length === 0) {
|
|
153
|
-
lines.push("_No subagents._", "");
|
|
154
|
-
return lines.join("\n");
|
|
155
|
-
}
|
|
156
|
-
lines.push(
|
|
157
|
-
"| Agent | State | Last tool | Calls | Cost | Ctx | Time |",
|
|
158
|
-
"| --- | --- | --- | ---: | ---: | ---: | ---: |",
|
|
159
|
-
...ordered.map(dashboardTableRow),
|
|
160
|
-
"",
|
|
161
|
-
"## Inputs",
|
|
162
|
-
"",
|
|
163
|
-
);
|
|
164
|
-
for (const details of ordered) lines.push(...dashboardInputSection(details));
|
|
165
|
-
return lines.join("\n");
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function parseOpenResult(stdout: string): { workspaceId?: string; surfaceId?: string } {
|
|
169
|
-
const parsed = JSON.parse(stdout) as {
|
|
170
|
-
surface_id?: string;
|
|
171
|
-
surface_ref?: string;
|
|
172
|
-
workspace_id?: string;
|
|
173
|
-
workspace_ref?: string;
|
|
174
|
-
result?: {
|
|
175
|
-
surface_id?: string;
|
|
176
|
-
surface_ref?: string;
|
|
177
|
-
workspace_id?: string;
|
|
178
|
-
workspace_ref?: string;
|
|
179
|
-
};
|
|
180
|
-
};
|
|
181
|
-
const body = parsed.result ?? parsed;
|
|
182
|
-
const surfaceId =
|
|
183
|
-
typeof body.surface_id === "string" && body.surface_id
|
|
184
|
-
? body.surface_id
|
|
185
|
-
: typeof body.surface_ref === "string" && body.surface_ref
|
|
186
|
-
? body.surface_ref
|
|
187
|
-
: undefined;
|
|
188
|
-
const workspaceId =
|
|
189
|
-
typeof body.workspace_id === "string" && body.workspace_id
|
|
190
|
-
? body.workspace_id
|
|
191
|
-
: typeof body.workspace_ref === "string" && body.workspace_ref
|
|
192
|
-
? body.workspace_ref
|
|
193
|
-
: undefined;
|
|
194
|
-
return { surfaceId, workspaceId };
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function surfaceGone(result: ExecResult): boolean {
|
|
198
|
-
return /not found|unknown surface|no such/i.test(`${result.stderr}\n${result.stdout}`);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* One session-owned Markdown surface for all active subagent invocations.
|
|
203
|
-
* Never blocks the child scheduler — all work is fire-and-forget from the runtime.
|
|
204
|
-
*/
|
|
205
|
-
export function createCmuxDashboard(options: CmuxDashboardOptions): CmuxDashboard {
|
|
206
|
-
const exec = options.exec;
|
|
207
|
-
const clock = options.clock ?? defaultClock();
|
|
208
|
-
const env = options.env ?? process.env;
|
|
209
|
-
const notify = options.notify;
|
|
210
|
-
const canOpen = options.canOpen ?? (() => true);
|
|
211
|
-
|
|
212
|
-
const parentWorkspaceId = env.CMUX_WORKSPACE_ID;
|
|
213
|
-
const parentSurfaceId = env.CMUX_SURFACE_ID;
|
|
214
|
-
const envAvailable = Boolean(parentWorkspaceId && parentSurfaceId);
|
|
215
|
-
|
|
216
|
-
let interactive = false;
|
|
217
|
-
let disabled = !envAvailable;
|
|
218
|
-
let shuttingDown = false;
|
|
219
|
-
let shutdownPromise: Promise<DashboardOrphan[]> | undefined;
|
|
220
|
-
let notifiedFailure = false;
|
|
221
|
-
let owned: OwnedSurface | undefined;
|
|
222
|
-
/** Ambiguous opens: keep dir tracked until shutdown. */
|
|
223
|
-
const retainedDirs = new Set<RetainedDirectory>();
|
|
224
|
-
let openPromise: Promise<void> | undefined;
|
|
225
|
-
let closePromise: Promise<void> | undefined;
|
|
226
|
-
let closeTimer: unknown;
|
|
227
|
-
let writeTimer: unknown;
|
|
228
|
-
let writeChain = Promise.resolve();
|
|
229
|
-
let opChain = Promise.resolve();
|
|
230
|
-
const active = new Map<string, SubagentInvocationSnapshot>();
|
|
231
|
-
const hasLiveInvocations = (): boolean =>
|
|
232
|
-
[...active.values()].some(
|
|
233
|
-
(snapshot) => snapshot.status !== "completed" && snapshot.status !== "failed" && snapshot.status !== "aborted",
|
|
234
|
-
);
|
|
235
|
-
const removeTerminalInvocations = (): void => {
|
|
236
|
-
for (const [invocationId, snapshot] of active) {
|
|
237
|
-
if (snapshot.status === "completed" || snapshot.status === "failed" || snapshot.status === "aborted") {
|
|
238
|
-
active.delete(invocationId);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
const enqueueOp = (fn: () => Promise<void>): Promise<void> => {
|
|
244
|
-
const next = opChain.then(fn, fn);
|
|
245
|
-
opChain = next.then(
|
|
246
|
-
() => undefined,
|
|
247
|
-
() => undefined,
|
|
248
|
-
);
|
|
249
|
-
return next;
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
const cmuxRpc = async (method: string, params: Record<string, unknown>): Promise<ExecResult> => {
|
|
253
|
-
return exec("cmux", ["--json", "--id-format", "both", "rpc", method, JSON.stringify(params)], {
|
|
254
|
-
timeout: CMUX_TIMEOUT_MS,
|
|
255
|
-
});
|
|
256
|
-
};
|
|
257
|
-
|
|
258
|
-
const writeAtomic = async (path: string, content: string): Promise<void> => {
|
|
259
|
-
const temp = `${path}.${process.pid}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
260
|
-
await writeFile(temp, content, { encoding: "utf8", mode: 0o600 });
|
|
261
|
-
await rename(temp, path);
|
|
262
|
-
};
|
|
263
|
-
|
|
264
|
-
const flushWrite = (): Promise<void> => {
|
|
265
|
-
const path = owned?.path;
|
|
266
|
-
if (!path) return Promise.resolve();
|
|
267
|
-
const content = formatDashboardMarkdown([...active.values()]);
|
|
268
|
-
writeChain = writeChain.then(() => writeAtomic(path, content)).catch(() => undefined);
|
|
269
|
-
return writeChain;
|
|
270
|
-
};
|
|
271
|
-
|
|
272
|
-
const scheduleWrite = (immediate: boolean): void => {
|
|
273
|
-
if (!owned || shuttingDown) return;
|
|
274
|
-
if (immediate) {
|
|
275
|
-
if (writeTimer !== undefined) {
|
|
276
|
-
clock.clearTimeout(writeTimer);
|
|
277
|
-
writeTimer = undefined;
|
|
278
|
-
}
|
|
279
|
-
void flushWrite();
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
if (writeTimer !== undefined) return;
|
|
283
|
-
writeTimer = clock.setTimeout(() => {
|
|
284
|
-
writeTimer = undefined;
|
|
285
|
-
void flushWrite();
|
|
286
|
-
}, WRITE_DEBOUNCE_MS);
|
|
287
|
-
};
|
|
288
|
-
|
|
289
|
-
const cancelCloseTimer = (): void => {
|
|
290
|
-
if (closeTimer === undefined) return;
|
|
291
|
-
clock.clearTimeout(closeTimer);
|
|
292
|
-
closeTimer = undefined;
|
|
293
|
-
};
|
|
294
|
-
|
|
295
|
-
const failOnce = (message: string): void => {
|
|
296
|
-
disabled = true;
|
|
297
|
-
if (notifiedFailure) return;
|
|
298
|
-
notifiedFailure = true;
|
|
299
|
-
notify?.(message);
|
|
300
|
-
};
|
|
301
|
-
|
|
302
|
-
const retainDirectory = (directory: string, path: string): void => {
|
|
303
|
-
retainedDirs.add({ directory, path });
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
const ensureOpen = (): void => {
|
|
307
|
-
if (disabled || shuttingDown || !interactive || !envAvailable || !parentWorkspaceId || !parentSurfaceId) return;
|
|
308
|
-
if (!canOpen()) return;
|
|
309
|
-
if (owned || openPromise || closePromise) return;
|
|
310
|
-
openPromise = enqueueOp(async () => {
|
|
311
|
-
if (disabled || shuttingDown || owned || closePromise) return;
|
|
312
|
-
let directory: string | undefined;
|
|
313
|
-
let path: string | undefined;
|
|
314
|
-
let openAttempted = false;
|
|
315
|
-
try {
|
|
316
|
-
directory = await mkdtemp(join(tmpdir(), "tau-subagent-dashboard-"));
|
|
317
|
-
path = join(directory, DASHBOARD_FILENAME);
|
|
318
|
-
await writeAtomic(path, formatDashboardMarkdown([...active.values()]));
|
|
319
|
-
openAttempted = true;
|
|
320
|
-
const result = await cmuxRpc("markdown.open", {
|
|
321
|
-
path,
|
|
322
|
-
workspace_id: parentWorkspaceId,
|
|
323
|
-
surface_id: parentSurfaceId,
|
|
324
|
-
direction: "right",
|
|
325
|
-
focus: false,
|
|
326
|
-
});
|
|
327
|
-
if (result.code !== 0 || result.killed) {
|
|
328
|
-
// Surface may still exist after timeout/kill. Keep directory owned.
|
|
329
|
-
retainDirectory(directory, path);
|
|
330
|
-
directory = undefined;
|
|
331
|
-
failOnce("Subagent cmux dashboard unavailable; continuing without live pane.");
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
334
|
-
let parsed: { surfaceId?: string; workspaceId?: string };
|
|
335
|
-
try {
|
|
336
|
-
parsed = parseOpenResult(result.stdout);
|
|
337
|
-
} catch {
|
|
338
|
-
retainDirectory(directory, path);
|
|
339
|
-
directory = undefined;
|
|
340
|
-
failOnce("Subagent cmux dashboard returned malformed open output; disabling for this session.");
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
if (!parsed.surfaceId) {
|
|
344
|
-
retainDirectory(directory, path);
|
|
345
|
-
directory = undefined;
|
|
346
|
-
failOnce("Subagent cmux dashboard open missing surface id; disabling for this session.");
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
349
|
-
owned = {
|
|
350
|
-
workspaceId: parsed.workspaceId ?? parentWorkspaceId,
|
|
351
|
-
surfaceId: parsed.surfaceId,
|
|
352
|
-
directory,
|
|
353
|
-
path,
|
|
354
|
-
};
|
|
355
|
-
directory = undefined;
|
|
356
|
-
await flushWrite();
|
|
357
|
-
if (!hasLiveInvocations()) scheduleCloseIfIdle();
|
|
358
|
-
} catch {
|
|
359
|
-
if (openAttempted && directory && path) retainDirectory(directory, path);
|
|
360
|
-
else if (directory) await rm(directory, { recursive: true, force: true }).catch(() => undefined);
|
|
361
|
-
failOnce("Subagent cmux dashboard failed to open; continuing without live pane.");
|
|
362
|
-
} finally {
|
|
363
|
-
openPromise = undefined;
|
|
364
|
-
// A close may have been requested while opening; or new work arrived after a prior close race.
|
|
365
|
-
if (!shuttingDown && hasLiveInvocations() && !owned && !disabled) ensureOpen();
|
|
366
|
-
}
|
|
367
|
-
});
|
|
368
|
-
};
|
|
369
|
-
|
|
370
|
-
const closeOwned = async (): Promise<void> => {
|
|
371
|
-
cancelCloseTimer();
|
|
372
|
-
if (writeTimer !== undefined) {
|
|
373
|
-
clock.clearTimeout(writeTimer);
|
|
374
|
-
writeTimer = undefined;
|
|
375
|
-
}
|
|
376
|
-
const current = owned;
|
|
377
|
-
if (!current) return;
|
|
378
|
-
// Recheck cohort immediately before RPC.
|
|
379
|
-
if (hasLiveInvocations()) return;
|
|
380
|
-
await writeChain.catch(() => undefined);
|
|
381
|
-
if (hasLiveInvocations()) return;
|
|
382
|
-
const result = await cmuxRpc("surface.close", {
|
|
383
|
-
workspace_id: current.workspaceId,
|
|
384
|
-
surface_id: current.surfaceId,
|
|
385
|
-
}).catch(() => ({ stdout: "", stderr: "close failed", code: 1, killed: false }));
|
|
386
|
-
if (hasLiveInvocations()) {
|
|
387
|
-
// New work arrived during close RPC.
|
|
388
|
-
const closed = result.code === 0 && !result.killed;
|
|
389
|
-
if (closed || surfaceGone(result)) {
|
|
390
|
-
owned = undefined;
|
|
391
|
-
removeTerminalInvocations();
|
|
392
|
-
await rm(current.directory, { recursive: true, force: true }).catch(() => undefined);
|
|
393
|
-
ensureOpen();
|
|
394
|
-
}
|
|
395
|
-
// Unknown failure: keep ownership so the live surface still has a file.
|
|
396
|
-
return;
|
|
397
|
-
}
|
|
398
|
-
const closed = result.code === 0 && !result.killed;
|
|
399
|
-
if (closed || surfaceGone(result)) {
|
|
400
|
-
owned = undefined;
|
|
401
|
-
active.clear();
|
|
402
|
-
await rm(current.directory, { recursive: true, force: true }).catch(() => undefined);
|
|
403
|
-
return;
|
|
404
|
-
}
|
|
405
|
-
// Unknown failure: keep ownership and backing file.
|
|
406
|
-
};
|
|
407
|
-
|
|
408
|
-
const scheduleCloseIfIdle = (): void => {
|
|
409
|
-
if (shuttingDown || hasLiveInvocations() || !owned || closePromise) return;
|
|
410
|
-
cancelCloseTimer();
|
|
411
|
-
closeTimer = clock.setTimeout(() => {
|
|
412
|
-
closeTimer = undefined;
|
|
413
|
-
if (shuttingDown || hasLiveInvocations() || closePromise) return;
|
|
414
|
-
closePromise = enqueueOp(async () => {
|
|
415
|
-
try {
|
|
416
|
-
if (shuttingDown || hasLiveInvocations()) return;
|
|
417
|
-
await closeOwned();
|
|
418
|
-
} finally {
|
|
419
|
-
closePromise = undefined;
|
|
420
|
-
if (!shuttingDown && hasLiveInvocations() && !owned && !disabled) ensureOpen();
|
|
421
|
-
}
|
|
422
|
-
});
|
|
423
|
-
}, CLOSE_DELAY_MS);
|
|
424
|
-
};
|
|
425
|
-
|
|
426
|
-
return {
|
|
427
|
-
setInteractive(enabled: boolean) {
|
|
428
|
-
if (shuttingDown) return;
|
|
429
|
-
interactive = enabled;
|
|
430
|
-
},
|
|
431
|
-
onSnapshot(snapshot: SubagentInvocationSnapshot) {
|
|
432
|
-
if (disabled || shuttingDown || !interactive) return;
|
|
433
|
-
const terminal =
|
|
434
|
-
snapshot.status === "completed" || snapshot.status === "failed" || snapshot.status === "aborted";
|
|
435
|
-
cancelCloseTimer();
|
|
436
|
-
if (terminal) {
|
|
437
|
-
// Keep completed work visible while the rest of the cohort runs.
|
|
438
|
-
active.set(snapshot.invocationId, snapshot);
|
|
439
|
-
ensureOpen();
|
|
440
|
-
void flushWrite().then(() => {
|
|
441
|
-
if (shuttingDown) return;
|
|
442
|
-
if (!hasLiveInvocations()) scheduleCloseIfIdle();
|
|
443
|
-
});
|
|
444
|
-
return;
|
|
445
|
-
}
|
|
446
|
-
active.set(snapshot.invocationId, snapshot);
|
|
447
|
-
if (closePromise) {
|
|
448
|
-
// Close in flight — reopen path runs when close finishes if still active.
|
|
449
|
-
} else {
|
|
450
|
-
ensureOpen();
|
|
451
|
-
}
|
|
452
|
-
scheduleWrite(snapshot.status === "starting" || snapshot.status === "running");
|
|
453
|
-
},
|
|
454
|
-
async shutdown() {
|
|
455
|
-
if (shutdownPromise) return shutdownPromise;
|
|
456
|
-
shuttingDown = true;
|
|
457
|
-
disabled = true;
|
|
458
|
-
interactive = false;
|
|
459
|
-
cancelCloseTimer();
|
|
460
|
-
if (writeTimer !== undefined) {
|
|
461
|
-
clock.clearTimeout(writeTimer);
|
|
462
|
-
writeTimer = undefined;
|
|
463
|
-
}
|
|
464
|
-
active.clear();
|
|
465
|
-
shutdownPromise = (async () => {
|
|
466
|
-
const orphans: DashboardOrphan[] = [];
|
|
467
|
-
if (openPromise) await openPromise.catch(() => undefined);
|
|
468
|
-
if (closePromise) await closePromise.catch(() => undefined);
|
|
469
|
-
await enqueueOp(async () => {
|
|
470
|
-
if (owned) {
|
|
471
|
-
await flushWrite();
|
|
472
|
-
const current = owned;
|
|
473
|
-
const result = await cmuxRpc("surface.close", {
|
|
474
|
-
workspace_id: current.workspaceId,
|
|
475
|
-
surface_id: current.surfaceId,
|
|
476
|
-
}).catch(() => ({ stdout: "", stderr: "close failed", code: 1, killed: false }));
|
|
477
|
-
const closed = result.code === 0 && !result.killed;
|
|
478
|
-
if (closed || surfaceGone(result)) {
|
|
479
|
-
owned = undefined;
|
|
480
|
-
await rm(current.directory, { recursive: true, force: true }).catch(() => undefined);
|
|
481
|
-
} else {
|
|
482
|
-
// Transfer ownership out so extension can retry later.
|
|
483
|
-
orphans.push({ ...current });
|
|
484
|
-
owned = undefined;
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
});
|
|
488
|
-
for (const item of retainedDirs) orphans.push({ directory: item.directory, path: item.path });
|
|
489
|
-
retainedDirs.clear();
|
|
490
|
-
return orphans;
|
|
491
|
-
})();
|
|
492
|
-
return shutdownPromise;
|
|
493
|
-
},
|
|
494
|
-
};
|
|
495
|
-
}
|