@shanepadgett/tau-agent 0.15.0 → 0.17.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/docs/subagents.md +25 -6
- package/extensions/attention/README.md +1 -0
- package/extensions/attention/index.ts +38 -0
- package/extensions/context/README.md +13 -3
- package/extensions/context/definitions.ts +3 -15
- package/extensions/context/evidence.ts +516 -0
- package/extensions/context/index.ts +114 -116
- package/extensions/context/panel.ts +60 -0
- package/extensions/context/settings.ts +30 -1
- package/extensions/context/sync.ts +171 -686
- package/extensions/context/validation.ts +4 -1
- package/extensions/context/write-scope.ts +109 -0
- package/extensions/context-pruning/README.md +22 -0
- package/extensions/context-pruning/file-evidence.ts +265 -0
- package/extensions/context-pruning/index.ts +379 -0
- package/extensions/context-pruning/projection.ts +108 -0
- package/extensions/context-pruning/prune.ts +346 -0
- package/extensions/context-pruning/render.ts +179 -0
- package/extensions/context-pruning/settings.ts +41 -0
- package/extensions/explore/README.md +3 -1
- package/extensions/explore/autoread.ts +85 -22
- package/extensions/explore/full-file-knowledge.ts +234 -0
- package/extensions/explore/index.ts +2 -3
- package/extensions/explore/read-cache.ts +150 -86
- package/extensions/explore/read-snapshots.ts +17 -4
- package/extensions/explore/read.ts +57 -38
- package/extensions/footer/index.ts +62 -60
- package/extensions/patch/README.md +1 -1
- package/extensions/patch/index.ts +19 -5
- package/extensions/run-summary/index.ts +5 -5
- package/extensions/silent-command-runner/README.md +1 -1
- package/extensions/silent-command-runner/index.ts +45 -28
- package/extensions/soul/prompt.ts +3 -1
- package/extensions/subagent/README.md +21 -5
- package/extensions/subagent/agents/context-sync.md +90 -0
- package/extensions/subagent/agents/{generalist.md → dormant/generalist.md} +6 -0
- package/extensions/subagent/agents/{scout.md → dormant/scout.md} +6 -0
- package/extensions/subagent/agents/review.md +48 -0
- package/extensions/subagent/agents/web-research.md +6 -0
- package/extensions/subagent/agents.ts +15 -2
- package/extensions/subagent/cmux-dashboard.ts +454 -0
- package/extensions/subagent/index.ts +181 -234
- package/extensions/subagent/render.ts +1 -1
- package/extensions/subagent/resume.ts +78 -0
- package/extensions/subagent/run.ts +213 -118
- package/extensions/subagent/runtime.ts +856 -0
- package/extensions/subagent/session-resource.ts +169 -0
- package/extensions/tau-help/help.md +7 -3
- package/extensions/turn-budget/index.ts +8 -36
- package/package.json +2 -2
- package/schemas/tau.schema.json +48 -1
- package/shared/context-pruning-state.ts +364 -0
- package/shared/events.ts +18 -0
- package/shared/model-fallback/index.ts +21 -10
- package/shared/model-fallback/types.ts +5 -3
- package/shared/settings/load.ts +78 -1
- package/shared/tool-row-state.ts +21 -1
|
@@ -8,6 +8,7 @@ export interface AgentDefinition {
|
|
|
8
8
|
name: string;
|
|
9
9
|
description: string;
|
|
10
10
|
tools: string[];
|
|
11
|
+
names: string[];
|
|
11
12
|
model?: string;
|
|
12
13
|
thinking?: ThinkingLevel;
|
|
13
14
|
prompt: string;
|
|
@@ -64,7 +65,7 @@ function parseDefinition(
|
|
|
64
65
|
const name = typeof rawName === "string" && rawName.trim() ? rawName.trim() : fallbackName;
|
|
65
66
|
const reasons: string[] = [];
|
|
66
67
|
for (const field of fields) {
|
|
67
|
-
if (!new Set(["name", "description", "tools", "model", "thinking"]).has(field))
|
|
68
|
+
if (!new Set(["name", "description", "tools", "names", "model", "thinking"]).has(field))
|
|
68
69
|
reasons.push(`unsupported field "${field}"`);
|
|
69
70
|
}
|
|
70
71
|
if (typeof rawName !== "string" || !rawName.trim()) reasons.push("name must be a non-empty string");
|
|
@@ -81,6 +82,17 @@ function parseDefinition(
|
|
|
81
82
|
if (new Set(tools).size !== tools.length) reasons.push("tools must be unique");
|
|
82
83
|
if (tools.includes("subagent")) reasons.push("tool subagent is forbidden");
|
|
83
84
|
}
|
|
85
|
+
const rawNames = parsed.frontmatter.names;
|
|
86
|
+
if (rawNames !== undefined) {
|
|
87
|
+
if (!Array.isArray(rawNames) || rawNames.length === 0) reasons.push("names must be a non-empty array");
|
|
88
|
+
else {
|
|
89
|
+
const names = rawNames
|
|
90
|
+
.filter((item): item is string => typeof item === "string" && item.trim().length > 0)
|
|
91
|
+
.map((item) => item.trim());
|
|
92
|
+
if (names.length !== rawNames.length) reasons.push("names must contain non-empty strings");
|
|
93
|
+
if (new Set(names).size !== names.length) reasons.push("names must be unique");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
84
96
|
if (!parsed.body.trim()) reasons.push("prompt body must be non-empty");
|
|
85
97
|
const rawModel = parsed.frontmatter.model;
|
|
86
98
|
if (rawModel !== undefined && (typeof rawModel !== "string" || !/^[^/\s]+\/[^/\s]+$/.test(rawModel.trim())))
|
|
@@ -95,6 +107,7 @@ function parseDefinition(
|
|
|
95
107
|
name,
|
|
96
108
|
description: (rawDescription as string).trim(),
|
|
97
109
|
tools: (rawTools as string[]).map((tool) => tool.trim()),
|
|
110
|
+
names: Array.isArray(rawNames) ? (rawNames as string[]).map((item) => item.trim()) : [name],
|
|
98
111
|
model: typeof rawModel === "string" ? rawModel.trim() : undefined,
|
|
99
112
|
thinking: rawThinking as ThinkingLevel | undefined,
|
|
100
113
|
prompt: parsed.body.trim(),
|
|
@@ -125,7 +138,7 @@ async function loadScope(
|
|
|
125
138
|
if (!required) return new Map();
|
|
126
139
|
const reason = error instanceof Error ? error.message : "directory unavailable";
|
|
127
140
|
return new Map([
|
|
128
|
-
["
|
|
141
|
+
["review", [{ path: directory, name: "review", reason: `packaged agents unavailable: ${reason}` }]],
|
|
129
142
|
[
|
|
130
143
|
"web-research",
|
|
131
144
|
[{ path: directory, name: "web-research", reason: `packaged agents unavailable: ${reason}` }],
|
|
@@ -0,0 +1,454 @@
|
|
|
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
|
+
export function formatDashboardMarkdown(snapshots: readonly SubagentInvocationSnapshot[]): string {
|
|
75
|
+
const ordered = [...snapshots].sort(
|
|
76
|
+
(a, b) => a.startedAt - b.startedAt || a.invocationId.localeCompare(b.invocationId),
|
|
77
|
+
);
|
|
78
|
+
const activeCount = ordered.filter(
|
|
79
|
+
(snapshot) => snapshot.status !== "completed" && snapshot.status !== "failed" && snapshot.status !== "aborted",
|
|
80
|
+
).length;
|
|
81
|
+
const lines = [
|
|
82
|
+
"# Subagent dashboard",
|
|
83
|
+
"",
|
|
84
|
+
`Active: ${activeCount} · Completed: ${ordered.length - activeCount}`,
|
|
85
|
+
"",
|
|
86
|
+
];
|
|
87
|
+
if (ordered.length === 0) {
|
|
88
|
+
lines.push("_No active invocations._", "");
|
|
89
|
+
return lines.join("\n");
|
|
90
|
+
}
|
|
91
|
+
for (const details of ordered) {
|
|
92
|
+
const identity = `${details.displayName} (${details.agent})`;
|
|
93
|
+
lines.push(
|
|
94
|
+
`## ${identity}`,
|
|
95
|
+
"",
|
|
96
|
+
`\`${details.status}${details.phase ? ` · ${details.phase}` : ""}\` · inv ${details.invocationId} · tools ${details.toolCalls} · ${(details.durationMs / 1000).toFixed(1)}s`,
|
|
97
|
+
`model ${details.model} · thinking ${details.thinkingLevel}`,
|
|
98
|
+
"",
|
|
99
|
+
"### Task",
|
|
100
|
+
"",
|
|
101
|
+
cap(details.task, 1200) || "_(empty)_",
|
|
102
|
+
);
|
|
103
|
+
if (details.currentActivity) {
|
|
104
|
+
lines.push("", "### Current", "", cap(details.currentActivity, 400));
|
|
105
|
+
}
|
|
106
|
+
if (details.actions.length > 0) {
|
|
107
|
+
lines.push("", "### Actions", "");
|
|
108
|
+
for (const action of details.actions.slice(-12)) {
|
|
109
|
+
const mark = action.error ? "!" : "·";
|
|
110
|
+
lines.push(`- ${mark} ${cap(action.summary, 200)}`);
|
|
111
|
+
}
|
|
112
|
+
if (details.omittedActions > 0) {
|
|
113
|
+
lines.push(`- _${details.omittedActions} earlier actions omitted_`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (details.response) {
|
|
117
|
+
lines.push("", "### Response", "", cap(details.response, 3000));
|
|
118
|
+
}
|
|
119
|
+
if (details.error) {
|
|
120
|
+
lines.push("", "### Error", "", cap(details.error, 800));
|
|
121
|
+
}
|
|
122
|
+
lines.push("");
|
|
123
|
+
}
|
|
124
|
+
return lines.join("\n");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function parseOpenResult(stdout: string): { workspaceId?: string; surfaceId?: string } {
|
|
128
|
+
const parsed = JSON.parse(stdout) as {
|
|
129
|
+
surface_id?: string;
|
|
130
|
+
surface_ref?: string;
|
|
131
|
+
workspace_id?: string;
|
|
132
|
+
workspace_ref?: string;
|
|
133
|
+
result?: {
|
|
134
|
+
surface_id?: string;
|
|
135
|
+
surface_ref?: string;
|
|
136
|
+
workspace_id?: string;
|
|
137
|
+
workspace_ref?: string;
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
const body = parsed.result ?? parsed;
|
|
141
|
+
const surfaceId =
|
|
142
|
+
typeof body.surface_id === "string" && body.surface_id
|
|
143
|
+
? body.surface_id
|
|
144
|
+
: typeof body.surface_ref === "string" && body.surface_ref
|
|
145
|
+
? body.surface_ref
|
|
146
|
+
: undefined;
|
|
147
|
+
const workspaceId =
|
|
148
|
+
typeof body.workspace_id === "string" && body.workspace_id
|
|
149
|
+
? body.workspace_id
|
|
150
|
+
: typeof body.workspace_ref === "string" && body.workspace_ref
|
|
151
|
+
? body.workspace_ref
|
|
152
|
+
: undefined;
|
|
153
|
+
return { surfaceId, workspaceId };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function surfaceGone(result: ExecResult): boolean {
|
|
157
|
+
return /not found|unknown surface|no such/i.test(`${result.stderr}\n${result.stdout}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* One session-owned Markdown surface for all active subagent invocations.
|
|
162
|
+
* Never blocks the child scheduler — all work is fire-and-forget from the runtime.
|
|
163
|
+
*/
|
|
164
|
+
export function createCmuxDashboard(options: CmuxDashboardOptions): CmuxDashboard {
|
|
165
|
+
const exec = options.exec;
|
|
166
|
+
const clock = options.clock ?? defaultClock();
|
|
167
|
+
const env = options.env ?? process.env;
|
|
168
|
+
const notify = options.notify;
|
|
169
|
+
const canOpen = options.canOpen ?? (() => true);
|
|
170
|
+
|
|
171
|
+
const parentWorkspaceId = env.CMUX_WORKSPACE_ID;
|
|
172
|
+
const parentSurfaceId = env.CMUX_SURFACE_ID;
|
|
173
|
+
const envAvailable = Boolean(parentWorkspaceId && parentSurfaceId);
|
|
174
|
+
|
|
175
|
+
let interactive = false;
|
|
176
|
+
let disabled = !envAvailable;
|
|
177
|
+
let shuttingDown = false;
|
|
178
|
+
let shutdownPromise: Promise<DashboardOrphan[]> | undefined;
|
|
179
|
+
let notifiedFailure = false;
|
|
180
|
+
let owned: OwnedSurface | undefined;
|
|
181
|
+
/** Ambiguous opens: keep dir tracked until shutdown. */
|
|
182
|
+
const retainedDirs = new Set<RetainedDirectory>();
|
|
183
|
+
let openPromise: Promise<void> | undefined;
|
|
184
|
+
let closePromise: Promise<void> | undefined;
|
|
185
|
+
let closeTimer: unknown;
|
|
186
|
+
let writeTimer: unknown;
|
|
187
|
+
let writeChain = Promise.resolve();
|
|
188
|
+
let opChain = Promise.resolve();
|
|
189
|
+
const active = new Map<string, SubagentInvocationSnapshot>();
|
|
190
|
+
const hasLiveInvocations = (): boolean =>
|
|
191
|
+
[...active.values()].some(
|
|
192
|
+
(snapshot) => snapshot.status !== "completed" && snapshot.status !== "failed" && snapshot.status !== "aborted",
|
|
193
|
+
);
|
|
194
|
+
const removeTerminalInvocations = (): void => {
|
|
195
|
+
for (const [invocationId, snapshot] of active) {
|
|
196
|
+
if (snapshot.status === "completed" || snapshot.status === "failed" || snapshot.status === "aborted") {
|
|
197
|
+
active.delete(invocationId);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const enqueueOp = (fn: () => Promise<void>): Promise<void> => {
|
|
203
|
+
const next = opChain.then(fn, fn);
|
|
204
|
+
opChain = next.then(
|
|
205
|
+
() => undefined,
|
|
206
|
+
() => undefined,
|
|
207
|
+
);
|
|
208
|
+
return next;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const cmuxRpc = async (method: string, params: Record<string, unknown>): Promise<ExecResult> => {
|
|
212
|
+
return exec("cmux", ["--json", "--id-format", "both", "rpc", method, JSON.stringify(params)], {
|
|
213
|
+
timeout: CMUX_TIMEOUT_MS,
|
|
214
|
+
});
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const writeAtomic = async (path: string, content: string): Promise<void> => {
|
|
218
|
+
const temp = `${path}.${process.pid}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
219
|
+
await writeFile(temp, content, { encoding: "utf8", mode: 0o600 });
|
|
220
|
+
await rename(temp, path);
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const flushWrite = (): Promise<void> => {
|
|
224
|
+
const path = owned?.path;
|
|
225
|
+
if (!path) return Promise.resolve();
|
|
226
|
+
const content = formatDashboardMarkdown([...active.values()]);
|
|
227
|
+
writeChain = writeChain.then(() => writeAtomic(path, content)).catch(() => undefined);
|
|
228
|
+
return writeChain;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const scheduleWrite = (immediate: boolean): void => {
|
|
232
|
+
if (!owned || shuttingDown) return;
|
|
233
|
+
if (immediate) {
|
|
234
|
+
if (writeTimer !== undefined) {
|
|
235
|
+
clock.clearTimeout(writeTimer);
|
|
236
|
+
writeTimer = undefined;
|
|
237
|
+
}
|
|
238
|
+
void flushWrite();
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (writeTimer !== undefined) return;
|
|
242
|
+
writeTimer = clock.setTimeout(() => {
|
|
243
|
+
writeTimer = undefined;
|
|
244
|
+
void flushWrite();
|
|
245
|
+
}, WRITE_DEBOUNCE_MS);
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const cancelCloseTimer = (): void => {
|
|
249
|
+
if (closeTimer === undefined) return;
|
|
250
|
+
clock.clearTimeout(closeTimer);
|
|
251
|
+
closeTimer = undefined;
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const failOnce = (message: string): void => {
|
|
255
|
+
disabled = true;
|
|
256
|
+
if (notifiedFailure) return;
|
|
257
|
+
notifiedFailure = true;
|
|
258
|
+
notify?.(message);
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const retainDirectory = (directory: string, path: string): void => {
|
|
262
|
+
retainedDirs.add({ directory, path });
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const ensureOpen = (): void => {
|
|
266
|
+
if (disabled || shuttingDown || !interactive || !envAvailable || !parentWorkspaceId || !parentSurfaceId) return;
|
|
267
|
+
if (!canOpen()) return;
|
|
268
|
+
if (owned || openPromise || closePromise) return;
|
|
269
|
+
openPromise = enqueueOp(async () => {
|
|
270
|
+
if (disabled || shuttingDown || owned || closePromise) return;
|
|
271
|
+
let directory: string | undefined;
|
|
272
|
+
let path: string | undefined;
|
|
273
|
+
let openAttempted = false;
|
|
274
|
+
try {
|
|
275
|
+
directory = await mkdtemp(join(tmpdir(), "tau-subagent-dashboard-"));
|
|
276
|
+
path = join(directory, DASHBOARD_FILENAME);
|
|
277
|
+
await writeAtomic(path, formatDashboardMarkdown([...active.values()]));
|
|
278
|
+
openAttempted = true;
|
|
279
|
+
const result = await cmuxRpc("markdown.open", {
|
|
280
|
+
path,
|
|
281
|
+
workspace_id: parentWorkspaceId,
|
|
282
|
+
surface_id: parentSurfaceId,
|
|
283
|
+
direction: "right",
|
|
284
|
+
focus: false,
|
|
285
|
+
});
|
|
286
|
+
if (result.code !== 0 || result.killed) {
|
|
287
|
+
// Surface may still exist after timeout/kill. Keep directory owned.
|
|
288
|
+
retainDirectory(directory, path);
|
|
289
|
+
directory = undefined;
|
|
290
|
+
failOnce("Subagent cmux dashboard unavailable; continuing without live pane.");
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
let parsed: { surfaceId?: string; workspaceId?: string };
|
|
294
|
+
try {
|
|
295
|
+
parsed = parseOpenResult(result.stdout);
|
|
296
|
+
} catch {
|
|
297
|
+
retainDirectory(directory, path);
|
|
298
|
+
directory = undefined;
|
|
299
|
+
failOnce("Subagent cmux dashboard returned malformed open output; disabling for this session.");
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (!parsed.surfaceId) {
|
|
303
|
+
retainDirectory(directory, path);
|
|
304
|
+
directory = undefined;
|
|
305
|
+
failOnce("Subagent cmux dashboard open missing surface id; disabling for this session.");
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
owned = {
|
|
309
|
+
workspaceId: parsed.workspaceId ?? parentWorkspaceId,
|
|
310
|
+
surfaceId: parsed.surfaceId,
|
|
311
|
+
directory,
|
|
312
|
+
path,
|
|
313
|
+
};
|
|
314
|
+
directory = undefined;
|
|
315
|
+
await flushWrite();
|
|
316
|
+
if (!hasLiveInvocations()) scheduleCloseIfIdle();
|
|
317
|
+
} catch {
|
|
318
|
+
if (openAttempted && directory && path) retainDirectory(directory, path);
|
|
319
|
+
else if (directory) await rm(directory, { recursive: true, force: true }).catch(() => undefined);
|
|
320
|
+
failOnce("Subagent cmux dashboard failed to open; continuing without live pane.");
|
|
321
|
+
} finally {
|
|
322
|
+
openPromise = undefined;
|
|
323
|
+
// A close may have been requested while opening; or new work arrived after a prior close race.
|
|
324
|
+
if (!shuttingDown && hasLiveInvocations() && !owned && !disabled) ensureOpen();
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const closeOwned = async (): Promise<void> => {
|
|
330
|
+
cancelCloseTimer();
|
|
331
|
+
if (writeTimer !== undefined) {
|
|
332
|
+
clock.clearTimeout(writeTimer);
|
|
333
|
+
writeTimer = undefined;
|
|
334
|
+
}
|
|
335
|
+
const current = owned;
|
|
336
|
+
if (!current) return;
|
|
337
|
+
// Recheck cohort immediately before RPC.
|
|
338
|
+
if (hasLiveInvocations()) return;
|
|
339
|
+
await writeChain.catch(() => undefined);
|
|
340
|
+
if (hasLiveInvocations()) return;
|
|
341
|
+
const result = await cmuxRpc("surface.close", {
|
|
342
|
+
workspace_id: current.workspaceId,
|
|
343
|
+
surface_id: current.surfaceId,
|
|
344
|
+
}).catch(() => ({ stdout: "", stderr: "close failed", code: 1, killed: false }));
|
|
345
|
+
if (hasLiveInvocations()) {
|
|
346
|
+
// New work arrived during close RPC.
|
|
347
|
+
const closed = result.code === 0 && !result.killed;
|
|
348
|
+
if (closed || surfaceGone(result)) {
|
|
349
|
+
owned = undefined;
|
|
350
|
+
removeTerminalInvocations();
|
|
351
|
+
await rm(current.directory, { recursive: true, force: true }).catch(() => undefined);
|
|
352
|
+
ensureOpen();
|
|
353
|
+
}
|
|
354
|
+
// Unknown failure: keep ownership so the live surface still has a file.
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
const closed = result.code === 0 && !result.killed;
|
|
358
|
+
if (closed || surfaceGone(result)) {
|
|
359
|
+
owned = undefined;
|
|
360
|
+
active.clear();
|
|
361
|
+
await rm(current.directory, { recursive: true, force: true }).catch(() => undefined);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
// Unknown failure: keep ownership and backing file.
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
const scheduleCloseIfIdle = (): void => {
|
|
368
|
+
if (shuttingDown || hasLiveInvocations() || !owned || closePromise) return;
|
|
369
|
+
cancelCloseTimer();
|
|
370
|
+
closeTimer = clock.setTimeout(() => {
|
|
371
|
+
closeTimer = undefined;
|
|
372
|
+
if (shuttingDown || hasLiveInvocations() || closePromise) return;
|
|
373
|
+
closePromise = enqueueOp(async () => {
|
|
374
|
+
try {
|
|
375
|
+
if (shuttingDown || hasLiveInvocations()) return;
|
|
376
|
+
await closeOwned();
|
|
377
|
+
} finally {
|
|
378
|
+
closePromise = undefined;
|
|
379
|
+
if (!shuttingDown && hasLiveInvocations() && !owned && !disabled) ensureOpen();
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
}, CLOSE_DELAY_MS);
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
setInteractive(enabled: boolean) {
|
|
387
|
+
if (shuttingDown) return;
|
|
388
|
+
interactive = enabled;
|
|
389
|
+
},
|
|
390
|
+
onSnapshot(snapshot: SubagentInvocationSnapshot) {
|
|
391
|
+
if (disabled || shuttingDown || !interactive) return;
|
|
392
|
+
const terminal =
|
|
393
|
+
snapshot.status === "completed" || snapshot.status === "failed" || snapshot.status === "aborted";
|
|
394
|
+
cancelCloseTimer();
|
|
395
|
+
if (terminal) {
|
|
396
|
+
// Keep completed work visible while the rest of the cohort runs.
|
|
397
|
+
active.set(snapshot.invocationId, snapshot);
|
|
398
|
+
ensureOpen();
|
|
399
|
+
void flushWrite().then(() => {
|
|
400
|
+
if (shuttingDown) return;
|
|
401
|
+
if (!hasLiveInvocations()) scheduleCloseIfIdle();
|
|
402
|
+
});
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
active.set(snapshot.invocationId, snapshot);
|
|
406
|
+
if (closePromise) {
|
|
407
|
+
// Close in flight — reopen path runs when close finishes if still active.
|
|
408
|
+
} else {
|
|
409
|
+
ensureOpen();
|
|
410
|
+
}
|
|
411
|
+
scheduleWrite(snapshot.status === "starting" || snapshot.status === "running");
|
|
412
|
+
},
|
|
413
|
+
async shutdown() {
|
|
414
|
+
if (shutdownPromise) return shutdownPromise;
|
|
415
|
+
shuttingDown = true;
|
|
416
|
+
disabled = true;
|
|
417
|
+
interactive = false;
|
|
418
|
+
cancelCloseTimer();
|
|
419
|
+
if (writeTimer !== undefined) {
|
|
420
|
+
clock.clearTimeout(writeTimer);
|
|
421
|
+
writeTimer = undefined;
|
|
422
|
+
}
|
|
423
|
+
active.clear();
|
|
424
|
+
shutdownPromise = (async () => {
|
|
425
|
+
const orphans: DashboardOrphan[] = [];
|
|
426
|
+
if (openPromise) await openPromise.catch(() => undefined);
|
|
427
|
+
if (closePromise) await closePromise.catch(() => undefined);
|
|
428
|
+
await enqueueOp(async () => {
|
|
429
|
+
if (owned) {
|
|
430
|
+
await flushWrite();
|
|
431
|
+
const current = owned;
|
|
432
|
+
const result = await cmuxRpc("surface.close", {
|
|
433
|
+
workspace_id: current.workspaceId,
|
|
434
|
+
surface_id: current.surfaceId,
|
|
435
|
+
}).catch(() => ({ stdout: "", stderr: "close failed", code: 1, killed: false }));
|
|
436
|
+
const closed = result.code === 0 && !result.killed;
|
|
437
|
+
if (closed || surfaceGone(result)) {
|
|
438
|
+
owned = undefined;
|
|
439
|
+
await rm(current.directory, { recursive: true, force: true }).catch(() => undefined);
|
|
440
|
+
} else {
|
|
441
|
+
// Transfer ownership out so extension can retry later.
|
|
442
|
+
orphans.push({ ...current });
|
|
443
|
+
owned = undefined;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
for (const item of retainedDirs) orphans.push({ directory: item.directory, path: item.path });
|
|
448
|
+
retainedDirs.clear();
|
|
449
|
+
return orphans;
|
|
450
|
+
})();
|
|
451
|
+
return shutdownPromise;
|
|
452
|
+
},
|
|
453
|
+
};
|
|
454
|
+
}
|