pum-agent 0.2.2-beta.1 → 0.2.4-beta.1
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 +4 -3
- package/package.json +1 -1
- package/src/app.tsx +33 -19
- package/src/check-mutation.ts +6 -1
- package/src/help-popup.tsx +6 -1
- package/src/message-cache.ts +41 -10
- package/src/sandbox/windows.ts +11 -8
- package/src/stash-batch.ts +3 -1
- package/src/subagents/manager.ts +41 -6
- package/src/theme.ts +9 -9
- package/src/transcript.tsx +14 -3
package/README.md
CHANGED
|
@@ -138,6 +138,7 @@ Set `PUM_DIR` to override PUM's complete configuration and data directory. Run `
|
|
|
138
138
|
| `Ctrl+L` | Open the agent transcript selector |
|
|
139
139
|
| `Shift+Tab` / `Ctrl+Shift+Tab` | Cycle through agent transcripts |
|
|
140
140
|
| `Ctrl+H` | Open session history when the terminal reports the key distinctly |
|
|
141
|
+
| `Ctrl+End` | Scroll to the end of the selected transcript |
|
|
141
142
|
| `Ctrl+P` | Open settings |
|
|
142
143
|
| `Ctrl+T` | Open supervised external triggers |
|
|
143
144
|
| `Esc` twice | Cancel the selected working agent |
|
|
@@ -177,7 +178,7 @@ PUM runs up to 10 active subagents by default. Configure a limit from 1 through
|
|
|
177
178
|
- Its own transcript, draft, usage data, and cancellation state
|
|
178
179
|
- Tools for progress messages and a single final completion report
|
|
179
180
|
|
|
180
|
-
Select a range of stashed prompts and press `Enter`. The main agent can group related work and run independent groups in parallel. Successful managed merges remove the completed worktree and branch. A parent cannot finish, merge, or be removed until every retained descendant closes deepest-first.
|
|
181
|
+
Select a range of stashed prompts and press `Enter`. The main agent can group related work and run independent groups in parallel. A managed merge requires both authoritative `completed` status and a persisted completion notice. Idle settlement is not completion. Successful managed merges remove the completed worktree and branch. A parent cannot finish, merge, or be removed until every retained descendant closes deepest-first.
|
|
181
182
|
|
|
182
183
|
Use `Ctrl+L` to select an agent transcript. Input then goes to that agent. Finished or interrupted agents remain available until PUM merges or removes them.
|
|
183
184
|
|
|
@@ -185,7 +186,7 @@ The public `spawn_subagent` tool accepts `preview: true`. PUM then shows the exa
|
|
|
185
186
|
|
|
186
187
|
Press `↑` on an empty single-line prompt to recall the newest queued user-authored message for the selected transcript. PUM removes the message from the authoritative queue before restoring its text. PUM does not recall inter-agent, trigger, lifecycle, cache, delivered, or image-bearing messages.
|
|
187
188
|
|
|
188
|
-
Idle notices report settled work cycles to the direct spawner. They are not completion notices. PUM
|
|
189
|
+
Idle notices report settled work cycles to the direct spawner. They are not completion notices. PUM acknowledges completion delivery only after the notice enters the parent session. Persisted completion intent and stable message identifiers let interrupted delivery resume without duplicate completion messages.
|
|
189
190
|
|
|
190
191
|
## Tools and safeguards
|
|
191
192
|
|
|
@@ -201,7 +202,7 @@ Main and managed child agents can list and read the current workspace message ca
|
|
|
201
202
|
|
|
202
203
|
Agents can add entries. An agent can delete only entries created by that exact agent. User-created and legacy entries remain user-owned.
|
|
203
204
|
|
|
204
|
-
The `message_cache_send` tool accepts stable entry IDs. Single entries use the selected agent delivery path. Multiple entries use main-agent worktree orchestration.
|
|
205
|
+
The `message_cache_send` tool accepts stable entry IDs. Single entries use the selected agent delivery path. Multiple entries use main-agent worktree orchestration. PUM reserves selected entries during delivery and marks them executed only after delivery succeeds. Failed main or child delivery leaves the entries pending.
|
|
205
206
|
|
|
206
207
|
### External triggers
|
|
207
208
|
|
package/package.json
CHANGED
package/src/app.tsx
CHANGED
|
@@ -486,6 +486,7 @@ export function App({
|
|
|
486
486
|
), [modelRuntime, modelId, modelQuery, loginPage]);
|
|
487
487
|
|
|
488
488
|
const inputRef = useRef<TextareaRenderable>(null);
|
|
489
|
+
const transcriptScrollRef = useRef<ScrollBoxRenderable>(null);
|
|
489
490
|
const questionnaireInputRef = useRef<TextareaRenderable>(null);
|
|
490
491
|
const spawnPreviewInputRef = useRef<TextareaRenderable>(null);
|
|
491
492
|
const settingsOpenRef = useRef(settingsOpen);
|
|
@@ -1361,12 +1362,12 @@ export function App({
|
|
|
1361
1362
|
return true;
|
|
1362
1363
|
};
|
|
1363
1364
|
|
|
1364
|
-
const deliverMainPrompt = (
|
|
1365
|
+
const deliverMainPrompt = async (
|
|
1365
1366
|
promptText: string,
|
|
1366
1367
|
displayText: string,
|
|
1367
1368
|
images: ReturnType<typeof imageContent>[] = [],
|
|
1368
1369
|
recallable = images.length === 0,
|
|
1369
|
-
) => {
|
|
1370
|
+
): Promise<void> => {
|
|
1370
1371
|
const userLine: Extract<Line, { kind: "text" }> = {
|
|
1371
1372
|
kind: "text",
|
|
1372
1373
|
role: "user",
|
|
@@ -1383,20 +1384,26 @@ export function App({
|
|
|
1383
1384
|
recallable,
|
|
1384
1385
|
};
|
|
1385
1386
|
addPending(pending);
|
|
1386
|
-
|
|
1387
|
+
try {
|
|
1388
|
+
await withSearchRoute(session.sessionId, () => session.steer(promptText, images));
|
|
1389
|
+
} catch (error) {
|
|
1387
1390
|
dropPending(pending.id);
|
|
1388
1391
|
append({ kind: "text", role: "error", text: String(error) });
|
|
1389
|
-
|
|
1392
|
+
throw error;
|
|
1393
|
+
}
|
|
1390
1394
|
return;
|
|
1391
1395
|
}
|
|
1392
1396
|
|
|
1393
1397
|
append(userLine);
|
|
1394
1398
|
inFlight.current = promptText;
|
|
1395
1399
|
setWorking(true);
|
|
1396
|
-
|
|
1400
|
+
try {
|
|
1401
|
+
await withSearchRoute(session.sessionId, () => session.prompt(promptText, { images }));
|
|
1402
|
+
} catch (error) {
|
|
1397
1403
|
append({ kind: "text", role: "error", text: String(error) });
|
|
1398
1404
|
setWorking(false);
|
|
1399
|
-
|
|
1405
|
+
throw error;
|
|
1406
|
+
}
|
|
1400
1407
|
};
|
|
1401
1408
|
|
|
1402
1409
|
const submitPrompt = (value?: string, stashIndex?: number) => {
|
|
@@ -1443,7 +1450,7 @@ export function App({
|
|
|
1443
1450
|
histCursor.current = null;
|
|
1444
1451
|
draft.current = "";
|
|
1445
1452
|
setSelectedStash(-1);
|
|
1446
|
-
deliverMainPrompt(promptText, displayText, images);
|
|
1453
|
+
void deliverMainPrompt(promptText, displayText, images).catch(() => {});
|
|
1447
1454
|
};
|
|
1448
1455
|
|
|
1449
1456
|
const cachedBatchDisplay = (prompts: readonly string[]): string => [
|
|
@@ -1477,7 +1484,7 @@ export function App({
|
|
|
1477
1484
|
setStash(next);
|
|
1478
1485
|
refreshHistoryAfterStashMutation();
|
|
1479
1486
|
resetAfterCacheExecution();
|
|
1480
|
-
deliverMainPrompt(buildStashBatchPrompt(prompts), cachedBatchDisplay(prompts), [], false);
|
|
1487
|
+
void deliverMainPrompt(buildStashBatchPrompt(prompts), cachedBatchDisplay(prompts), [], false).catch(() => {});
|
|
1481
1488
|
};
|
|
1482
1489
|
|
|
1483
1490
|
useEffect(() => {
|
|
@@ -1493,24 +1500,22 @@ export function App({
|
|
|
1493
1500
|
const detach = messageCacheController.bindExecutor(
|
|
1494
1501
|
session.sessionId,
|
|
1495
1502
|
async (request: MessageCacheSendRequest): Promise<MessageCacheSendResult> => {
|
|
1496
|
-
const
|
|
1497
|
-
stashRef.current = state.stash;
|
|
1498
|
-
setStash(state.stash);
|
|
1499
|
-
history.current = state.history;
|
|
1500
|
-
resetAfterCacheExecution(
|
|
1501
|
-
request.requester.kind === "subagent" ? request.requester.id : null,
|
|
1502
|
-
);
|
|
1503
|
-
const prompts = entries.map((entry) => entry.text);
|
|
1503
|
+
const prompts = request.entries.map((entry) => entry.text);
|
|
1504
1504
|
if (prompts.length > 1) {
|
|
1505
|
-
deliverMainPrompt(buildStashBatchPrompt(prompts), cachedBatchDisplay(prompts), [], false);
|
|
1505
|
+
await deliverMainPrompt(buildStashBatchPrompt(prompts), cachedBatchDisplay(prompts), [], false);
|
|
1506
|
+
resetAfterCacheExecution(
|
|
1507
|
+
request.requester.kind === "subagent" ? request.requester.id : null,
|
|
1508
|
+
);
|
|
1506
1509
|
return { count: prompts.length, route: "main" };
|
|
1507
1510
|
}
|
|
1508
1511
|
const prompt = prompts[0]!;
|
|
1509
1512
|
if (request.requester.kind === "subagent") {
|
|
1510
1513
|
await subagentManager.sendUserMessage(request.requester.id, prompt, [], prompt, false);
|
|
1514
|
+
resetAfterCacheExecution(request.requester.id);
|
|
1511
1515
|
return { count: 1, route: "subagent" };
|
|
1512
1516
|
}
|
|
1513
|
-
deliverMainPrompt(prompt, prompt, [], false);
|
|
1517
|
+
await deliverMainPrompt(prompt, prompt, [], false);
|
|
1518
|
+
resetAfterCacheExecution(null);
|
|
1514
1519
|
return { count: 1, route: "main" };
|
|
1515
1520
|
},
|
|
1516
1521
|
);
|
|
@@ -1989,6 +1994,13 @@ export function App({
|
|
|
1989
1994
|
return;
|
|
1990
1995
|
}
|
|
1991
1996
|
|
|
1997
|
+
if (key.ctrl && key.name === "end") {
|
|
1998
|
+
key.stopPropagation();
|
|
1999
|
+
const transcriptScroll = transcriptScrollRef.current;
|
|
2000
|
+
if (transcriptScroll) transcriptScroll.scrollTop = transcriptScroll.scrollHeight;
|
|
2001
|
+
return;
|
|
2002
|
+
}
|
|
2003
|
+
|
|
1992
2004
|
const isAgentCycle =
|
|
1993
2005
|
(key.name === "tab" && key.shift) ||
|
|
1994
2006
|
key.name === "backtab" ||
|
|
@@ -2290,6 +2302,8 @@ export function App({
|
|
|
2290
2302
|
/>
|
|
2291
2303
|
<scrollbox
|
|
2292
2304
|
key={activeAgentId ?? "main"}
|
|
2305
|
+
ref={transcriptScrollRef}
|
|
2306
|
+
id="transcript-scrollbox"
|
|
2293
2307
|
style={{ flexGrow: 1, paddingLeft: 1, paddingRight: 1 }}
|
|
2294
2308
|
stickyScroll
|
|
2295
2309
|
stickyStart="bottom"
|
|
@@ -2430,7 +2444,7 @@ export function App({
|
|
|
2430
2444
|
textColor={theme.fg}
|
|
2431
2445
|
cursorColor={theme.accent}
|
|
2432
2446
|
selectionBg={theme.selectionBg}
|
|
2433
|
-
wrapMode="
|
|
2447
|
+
wrapMode="word"
|
|
2434
2448
|
scrollMargin={1}
|
|
2435
2449
|
focused={!settingsOpen && !helpOpen && !historyOpen && !agentSelectorOpen && !triggersOpen && !loginOpen && !questionnaire && !spawnPreview && !checkApproval}
|
|
2436
2450
|
onContentChange={handleTextareaChange}
|
package/src/check-mutation.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { generateUnifiedPatch } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { lstat, readFile, realpath } from "node:fs/promises";
|
|
4
|
-
import { basename, dirname, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { basename, dirname, parse, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { previewApplyPatch } from "./apply-patch";
|
|
6
6
|
import type { CheckedToolName } from "./check-approvals";
|
|
7
7
|
import {
|
|
@@ -85,6 +85,11 @@ async function validateEditPath(
|
|
|
85
85
|
const roots = await Promise.all([projectRoot, ...allowedPaths].map((path) => realpath(path)));
|
|
86
86
|
const absolute = resolve(projectRoot, inputPath);
|
|
87
87
|
const sortedRoots = roots.sort((first, second) => second.length - first.length);
|
|
88
|
+
if (process.platform === "win32" && windowsAbsolute(inputPath)) {
|
|
89
|
+
const targetRoot = parse(absolute).root.toLowerCase();
|
|
90
|
+
const hasAllowedVolume = sortedRoots.some((candidate) => parse(candidate).root.toLowerCase() === targetRoot);
|
|
91
|
+
if (!hasAllowedVolume) throw new Error(`Edit path is outside the allowed Check mode paths: ${inputPath}`);
|
|
92
|
+
}
|
|
88
93
|
let root = sortedRoots.find((candidate) => isPathInsideOrSame(candidate, absolute));
|
|
89
94
|
if (!root) {
|
|
90
95
|
let targetIdentity: string;
|
package/src/help-popup.tsx
CHANGED
|
@@ -66,6 +66,7 @@ export const HELP_GROUPS: HelpGroup[] = [
|
|
|
66
66
|
controls: [
|
|
67
67
|
["Ctrl+P", "Open Settings"],
|
|
68
68
|
["Ctrl+T", "Open external triggers"],
|
|
69
|
+
["Ctrl+End", "Scroll transcript to the end"],
|
|
69
70
|
["/ in Settings", "Focus settings search"],
|
|
70
71
|
["Esc", "Close; twice to cancel work"],
|
|
71
72
|
["Ctrl+C", "Clear; twice quits"],
|
|
@@ -154,8 +155,12 @@ export function helpPageSize(terminalHeight: number): number {
|
|
|
154
155
|
export function maxHelpScrollOffset(terminalHeight: number): number {
|
|
155
156
|
const lines = helpLines(terminalHeight);
|
|
156
157
|
const raw = Math.max(0, lines.length - helpPageSize(terminalHeight));
|
|
158
|
+
const headingBeforeFirstControl =
|
|
159
|
+
raw > 0 && lines[raw]?.kind === "control" && lines[raw - 1]?.kind === "heading"
|
|
160
|
+
? raw - 1
|
|
161
|
+
: raw;
|
|
157
162
|
const lastHeading = lines.findLastIndex((line) => line.kind === "heading");
|
|
158
|
-
return lastHeading < 0 ?
|
|
163
|
+
return lastHeading < 0 ? headingBeforeFirstControl : Math.min(headingBeforeFirstControl, lastHeading);
|
|
159
164
|
}
|
|
160
165
|
|
|
161
166
|
function HelpLineRow({ line, theme }: { line: HelpLine; theme: Theme }) {
|
package/src/message-cache.ts
CHANGED
|
@@ -18,6 +18,7 @@ export type MessageCacheRequester =
|
|
|
18
18
|
export type MessageCacheSendRequest = {
|
|
19
19
|
requester: MessageCacheRequester;
|
|
20
20
|
ids: string[];
|
|
21
|
+
entries: StashedPrompt[];
|
|
21
22
|
};
|
|
22
23
|
|
|
23
24
|
export type MessageCacheSendResult = {
|
|
@@ -83,6 +84,8 @@ export class MessageCacheController {
|
|
|
83
84
|
private executor?: MessageCacheExecutor;
|
|
84
85
|
private readonly listeners = new Set<() => void>();
|
|
85
86
|
private readonly active = new Set<string>();
|
|
87
|
+
private readonly reservations = new Map<string, symbol>();
|
|
88
|
+
private readonly operations = new Map<symbol, Set<string>>();
|
|
86
89
|
private mainSessionId?: string;
|
|
87
90
|
|
|
88
91
|
constructor(
|
|
@@ -107,12 +110,28 @@ export class MessageCacheController {
|
|
|
107
110
|
if (this.mainSessionId === sessionId) {
|
|
108
111
|
this.mainSessionId = undefined;
|
|
109
112
|
this.active.clear();
|
|
113
|
+
this.reservations.clear();
|
|
114
|
+
this.operations.clear();
|
|
110
115
|
}
|
|
111
116
|
};
|
|
112
117
|
}
|
|
113
118
|
|
|
114
119
|
releaseRequester(requester: Pick<MessageCacheRequester, "kind" | "id">): void {
|
|
115
|
-
|
|
120
|
+
const key = `${requester.kind}:${requester.id}`;
|
|
121
|
+
this.active.delete(key);
|
|
122
|
+
for (const [token, keys] of this.operations) {
|
|
123
|
+
keys.delete(key);
|
|
124
|
+
if (keys.size === 0) this.releaseOperation(token);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private releaseOperation(token: symbol): void {
|
|
129
|
+
const keys = this.operations.get(token);
|
|
130
|
+
if (keys) for (const key of keys) this.active.delete(key);
|
|
131
|
+
this.operations.delete(token);
|
|
132
|
+
for (const [id, owner] of this.reservations) {
|
|
133
|
+
if (owner === token) this.reservations.delete(id);
|
|
134
|
+
}
|
|
116
135
|
}
|
|
117
136
|
|
|
118
137
|
list(): StashedPrompt[] {
|
|
@@ -135,11 +154,12 @@ export class MessageCacheController {
|
|
|
135
154
|
}
|
|
136
155
|
|
|
137
156
|
delete(requester: MessageCacheRequester, id: string): void {
|
|
157
|
+
if (this.reservations.has(id)) throw new Error("The cache entry is reserved by an active send");
|
|
138
158
|
this.store.removeStashById(this.workspaceCwd, id, requester.id);
|
|
139
159
|
this.emit();
|
|
140
160
|
}
|
|
141
161
|
|
|
142
|
-
execute(ids: readonly string[]): { entries: StashedPrompt[]; state: PromptCacheState } {
|
|
162
|
+
private execute(ids: readonly string[]): { entries: StashedPrompt[]; state: PromptCacheState } {
|
|
143
163
|
const result = this.store.executeStashByIds(this.workspaceCwd, ids);
|
|
144
164
|
this.emit();
|
|
145
165
|
return result;
|
|
@@ -150,21 +170,29 @@ export class MessageCacheController {
|
|
|
150
170
|
if (requester.kind === "main" && requester.id !== this.mainSessionId) {
|
|
151
171
|
throw new Error("The requesting main session is no longer active");
|
|
152
172
|
}
|
|
153
|
-
this.read(ids);
|
|
173
|
+
const entries = this.read(ids);
|
|
154
174
|
const requesterKey = `${requester.kind}:${requester.id}`;
|
|
155
175
|
const targetKey = ids.length > 1 || requester.kind === "main"
|
|
156
176
|
? `main:${this.mainSessionId}`
|
|
157
177
|
: requesterKey;
|
|
158
|
-
if (
|
|
178
|
+
if (
|
|
179
|
+
this.active.has(requesterKey)
|
|
180
|
+
|| this.active.has(targetKey)
|
|
181
|
+
|| ids.some((id) => this.reservations.has(id))
|
|
182
|
+
) {
|
|
159
183
|
throw new Error("A message-cache send is already active for this requester or target");
|
|
160
184
|
}
|
|
161
|
-
|
|
162
|
-
|
|
185
|
+
const keys = new Set([requesterKey, targetKey]);
|
|
186
|
+
const token = Symbol("message-cache-send");
|
|
187
|
+
for (const key of keys) this.active.add(key);
|
|
188
|
+
for (const id of ids) this.reservations.set(id, token);
|
|
189
|
+
this.operations.set(token, keys);
|
|
163
190
|
try {
|
|
164
|
-
|
|
191
|
+
const result = await this.executor({ requester, ids: [...ids], entries: [...entries] });
|
|
192
|
+
this.execute(ids);
|
|
193
|
+
return result;
|
|
165
194
|
} catch (error) {
|
|
166
|
-
this.
|
|
167
|
-
this.active.delete(targetKey);
|
|
195
|
+
this.releaseOperation(token);
|
|
168
196
|
throw error;
|
|
169
197
|
}
|
|
170
198
|
}
|
|
@@ -226,10 +254,13 @@ export class MessageCacheController {
|
|
|
226
254
|
pi.registerTool({
|
|
227
255
|
name: "message_cache_send",
|
|
228
256
|
label: "Message Cache Send",
|
|
229
|
-
description: "
|
|
257
|
+
description: "Execute cached messages by stable ID through PUM's authoritative user execution path. This marks entries executed and produces the main-agent coordination path. Order and duplicates are preserved.",
|
|
230
258
|
promptSnippet: "Send one or more cached messages through PUM coordination",
|
|
231
259
|
promptGuidelines: [
|
|
232
260
|
"Use stable IDs from message_cache_list or message_cache_read.",
|
|
261
|
+
"When the user asks to do, run, or execute open or pending cached tasks, call message_cache_send before spawning or assigning work.",
|
|
262
|
+
"Listing entries or reading previews is not execution and does not replace message_cache_send.",
|
|
263
|
+
"After the generated coordination prompt arrives, reuse agents already assigned to those tasks and never create duplicate assignments.",
|
|
233
264
|
"Do not retry a send while the requester or target is still processing a prior cache send.",
|
|
234
265
|
],
|
|
235
266
|
parameters: Type.Object({ ids: IdsSchema }, { additionalProperties: false }),
|
package/src/sandbox/windows.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ChildProcess } from "node:child_process";
|
|
2
|
-
import {
|
|
2
|
+
import { join, win32 } from "node:path";
|
|
3
3
|
import type {
|
|
4
4
|
ContainerConfig,
|
|
5
5
|
PlatformSupport,
|
|
@@ -31,16 +31,19 @@ const REQUIRED_UI_CAPABILITIES = [
|
|
|
31
31
|
|
|
32
32
|
const loadMxcSdk: MxcSdkLoader = async () => {
|
|
33
33
|
// MXC 0.7 resolves `whoami /user` through a shell during module import.
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
const previous = process.env[pathKey];
|
|
34
|
+
// Bun caches the default child-process environment, so changing PATH here
|
|
35
|
+
// does not affect MXC after node:child_process has already been imported.
|
|
37
36
|
const systemRoot = process.env.SystemRoot ?? process.env.WINDIR;
|
|
38
|
-
|
|
37
|
+
const system32 = join(systemRoot ?? "C:\\Windows", "System32");
|
|
38
|
+
const previousCwd = process.cwd();
|
|
39
|
+
const specifier = import.meta.resolve("@microsoft/mxc-sdk");
|
|
39
40
|
try {
|
|
40
|
-
|
|
41
|
+
// Bun can synchronously require this ESM package. Keep the cwd change
|
|
42
|
+
// synchronous so no unrelated work can observe the temporary directory.
|
|
43
|
+
process.chdir(system32);
|
|
44
|
+
return require(specifier) as MxcSdk;
|
|
41
45
|
} finally {
|
|
42
|
-
|
|
43
|
-
else process.env[pathKey] = previous;
|
|
46
|
+
process.chdir(previousCwd);
|
|
44
47
|
}
|
|
45
48
|
};
|
|
46
49
|
|
package/src/stash-batch.ts
CHANGED
|
@@ -9,6 +9,8 @@ export function buildStashBatchPrompt(prompts: string[]): string {
|
|
|
9
9
|
return `Coordinate the following cached tasks with managed worktree subagents.
|
|
10
10
|
|
|
11
11
|
Rules:
|
|
12
|
+
- This prompt is the authoritative execution path created by message_cache_send or the cache selection UI.
|
|
13
|
+
- Reuse agents already assigned to these tasks. Never create duplicate assignments.
|
|
12
14
|
- Count only starting and running subagents as active. Use the configured capacity from the system prompt.
|
|
13
15
|
- Use spawn_subagent for implementation work while capacity is available.
|
|
14
16
|
- At capacity, queue related work to an appropriate running subagent with message_agent.
|
|
@@ -18,7 +20,7 @@ Rules:
|
|
|
18
20
|
- Run independent task groups in parallel.
|
|
19
21
|
- Keep each subagent task complete and self-contained.
|
|
20
22
|
- Track every unfinished task group through completion notifications.
|
|
21
|
-
- Merge each successful subagent
|
|
23
|
+
- Merge each successful subagent only after a completion notice arrives and authoritative status is \`completed\`; idle settlement is not completion.
|
|
22
24
|
- Before merging a managed parent, recursively merge or resolve every retained descendant.
|
|
23
25
|
- Close the deepest descendants first. Every retained status blocks the parent, including completed and failed descendants.
|
|
24
26
|
- Wait to merge only for a concrete dependency, known conflict risk, or required integration order. State that reason explicitly.
|
package/src/subagents/manager.ts
CHANGED
|
@@ -243,6 +243,7 @@ export class SubagentManager {
|
|
|
243
243
|
private readonly messageTimes = new Map<string, number[]>();
|
|
244
244
|
private readonly settlements = new Map<string, SubagentSettlement>();
|
|
245
245
|
private readonly acceptedSettlementMessageIds = new Set<string>();
|
|
246
|
+
private readonly settlementDeliveriesInFlight = new Set<string>();
|
|
246
247
|
|
|
247
248
|
constructor(options: ManagerOptions) {
|
|
248
249
|
this.modelRuntime = options.modelRuntime;
|
|
@@ -303,6 +304,7 @@ export class SubagentManager {
|
|
|
303
304
|
this.messageTimes.clear();
|
|
304
305
|
this.settlements.clear();
|
|
305
306
|
this.acceptedSettlementMessageIds.clear();
|
|
307
|
+
this.settlementDeliveriesInFlight.clear();
|
|
306
308
|
this.mainApi = pi;
|
|
307
309
|
this.mainSessionManager = sessionManager;
|
|
308
310
|
this.mainCwd = cwd;
|
|
@@ -430,6 +432,7 @@ export class SubagentManager {
|
|
|
430
432
|
});
|
|
431
433
|
this.mainApi = undefined;
|
|
432
434
|
this.mainSessionManager = undefined;
|
|
435
|
+
this.settlementDeliveriesInFlight.clear();
|
|
433
436
|
this.records.clear();
|
|
434
437
|
this.emit();
|
|
435
438
|
}
|
|
@@ -1343,6 +1346,22 @@ export class SubagentManager {
|
|
|
1343
1346
|
);
|
|
1344
1347
|
}
|
|
1345
1348
|
|
|
1349
|
+
private assertManagedMergeReady(record: RuntimeRecord): void {
|
|
1350
|
+
if (record.snapshot.status !== "completed") {
|
|
1351
|
+
throw new Error(
|
|
1352
|
+
`Cannot merge ${record.snapshot.name} while its authoritative status is ${record.snapshot.status}. ` +
|
|
1353
|
+
"A managed merge requires status completed after its completion notice arrives. Idle settlement is not completion.",
|
|
1354
|
+
);
|
|
1355
|
+
}
|
|
1356
|
+
const completion = this.settlements.get(this.settlementId(record, "completed"));
|
|
1357
|
+
if (completion?.acknowledgedAt === undefined) {
|
|
1358
|
+
throw new Error(
|
|
1359
|
+
`Cannot merge ${record.snapshot.name} before its completion notice arrives. ` +
|
|
1360
|
+
"Authoritative status completed alone is not sufficient.",
|
|
1361
|
+
);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1346
1365
|
async sendUserMessage(
|
|
1347
1366
|
id: string,
|
|
1348
1367
|
text: string,
|
|
@@ -1646,6 +1665,7 @@ export class SubagentManager {
|
|
|
1646
1665
|
}
|
|
1647
1666
|
|
|
1648
1667
|
private acknowledgeSettlement(settlement: SubagentSettlement): void {
|
|
1668
|
+
this.settlementDeliveriesInFlight.delete(settlement.messageId);
|
|
1649
1669
|
if (settlement.acknowledgedAt !== undefined) return;
|
|
1650
1670
|
settlement.acknowledgedAt = Date.now();
|
|
1651
1671
|
this.acceptedSettlementMessageIds.add(settlement.messageId);
|
|
@@ -1662,6 +1682,14 @@ export class SubagentManager {
|
|
|
1662
1682
|
if (settlement) this.acknowledgeSettlement(settlement);
|
|
1663
1683
|
}
|
|
1664
1684
|
|
|
1685
|
+
private clearSettlementDeliveriesForParent(parentAgentId: string): void {
|
|
1686
|
+
for (const settlement of this.settlements.values()) {
|
|
1687
|
+
if (settlement.parentAgentId === parentAgentId) {
|
|
1688
|
+
this.settlementDeliveriesInFlight.delete(settlement.messageId);
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1665
1693
|
private async deliverSettlement(settlement: SubagentSettlement): Promise<boolean> {
|
|
1666
1694
|
if (settlement.acknowledgedAt !== undefined) return true;
|
|
1667
1695
|
const source = this.records.get(settlement.agentId);
|
|
@@ -1673,6 +1701,7 @@ export class SubagentManager {
|
|
|
1673
1701
|
this.acknowledgeSettlement(settlement);
|
|
1674
1702
|
return true;
|
|
1675
1703
|
}
|
|
1704
|
+
if (this.settlementDeliveriesInFlight.has(settlement.messageId)) return true;
|
|
1676
1705
|
const data: AgentMessageData = {
|
|
1677
1706
|
id: settlement.messageId,
|
|
1678
1707
|
sender,
|
|
@@ -1681,14 +1710,17 @@ export class SubagentManager {
|
|
|
1681
1710
|
at: settlement.createdAt,
|
|
1682
1711
|
kind: settlement.status === "idle" ? "idle" : settlement.status === "completed" ? "completion" : "status",
|
|
1683
1712
|
};
|
|
1713
|
+
this.settlementDeliveriesInFlight.add(settlement.messageId);
|
|
1684
1714
|
const delivered = this.wakeMain({
|
|
1685
1715
|
customType: AGENT_MESSAGE_CUSTOM_TYPE,
|
|
1686
1716
|
content: settlement.content,
|
|
1687
1717
|
display: true,
|
|
1688
1718
|
details: data,
|
|
1689
1719
|
}, settlement.content);
|
|
1690
|
-
if (!delivered)
|
|
1691
|
-
|
|
1720
|
+
if (!delivered) {
|
|
1721
|
+
this.settlementDeliveriesInFlight.delete(settlement.messageId);
|
|
1722
|
+
return false;
|
|
1723
|
+
}
|
|
1692
1724
|
this.emit({ type: "main-line", line: this.agentMessageLine(data) });
|
|
1693
1725
|
return true;
|
|
1694
1726
|
}
|
|
@@ -1702,6 +1734,7 @@ export class SubagentManager {
|
|
|
1702
1734
|
this.acknowledgeSettlement(settlement);
|
|
1703
1735
|
return true;
|
|
1704
1736
|
}
|
|
1737
|
+
if (this.settlementDeliveriesInFlight.has(settlement.messageId)) return true;
|
|
1705
1738
|
const data: AgentMessageData = {
|
|
1706
1739
|
id: settlement.messageId,
|
|
1707
1740
|
sender,
|
|
@@ -1714,6 +1747,7 @@ export class SubagentManager {
|
|
|
1714
1747
|
if (!parent.snapshot.transcript.pending.some((item) => item.id === pending.id)) {
|
|
1715
1748
|
this.addPending(parent, pending);
|
|
1716
1749
|
}
|
|
1750
|
+
this.settlementDeliveriesInFlight.add(settlement.messageId);
|
|
1717
1751
|
try {
|
|
1718
1752
|
withSearchRoute(parent.session.sessionId, () => {
|
|
1719
1753
|
parent.api!.sendMessage(
|
|
@@ -1730,11 +1764,11 @@ export class SubagentManager {
|
|
|
1730
1764
|
);
|
|
1731
1765
|
});
|
|
1732
1766
|
} catch {
|
|
1767
|
+
this.settlementDeliveriesInFlight.delete(settlement.messageId);
|
|
1733
1768
|
this.dropPending(parent, pending.id);
|
|
1734
1769
|
return false;
|
|
1735
1770
|
}
|
|
1736
1771
|
this.updateStatus(parent, "running");
|
|
1737
|
-
this.acknowledgeSettlement(settlement);
|
|
1738
1772
|
return true;
|
|
1739
1773
|
} catch {
|
|
1740
1774
|
return false;
|
|
@@ -1761,6 +1795,7 @@ export class SubagentManager {
|
|
|
1761
1795
|
if (!record) return;
|
|
1762
1796
|
const sessionId = record.session?.sessionId;
|
|
1763
1797
|
if (record.dispose) await record.dispose();
|
|
1798
|
+
this.clearSettlementDeliveriesForParent(record.snapshot.id);
|
|
1764
1799
|
if (sessionId) {
|
|
1765
1800
|
await this.triggerManager?.invalidateAgent(sessionId, record.snapshot.id);
|
|
1766
1801
|
this.emit({
|
|
@@ -1825,10 +1860,10 @@ export class SubagentManager {
|
|
|
1825
1860
|
if (action === "merge") {
|
|
1826
1861
|
return this.withWorktreeLock(async () => {
|
|
1827
1862
|
const managedAgent = this.findRecord(target);
|
|
1828
|
-
if (managedAgent
|
|
1829
|
-
|
|
1863
|
+
if (managedAgent) {
|
|
1864
|
+
this.assertNoRetainedDescendants(managedAgent, "merge");
|
|
1865
|
+
this.assertManagedMergeReady(managedAgent);
|
|
1830
1866
|
}
|
|
1831
|
-
if (managedAgent) this.assertNoRetainedDescendants(managedAgent, "merge");
|
|
1832
1867
|
const record = managedAgent?.snapshot.worktree
|
|
1833
1868
|
?? (await listWorktrees(cwd)).find((item) => item.name === target || item.branch === target);
|
|
1834
1869
|
if (!record) throw new Error(`Unknown worktree: ${target}`);
|
package/src/theme.ts
CHANGED
|
@@ -23,7 +23,7 @@ export type Theme = {
|
|
|
23
23
|
success: string;
|
|
24
24
|
error: string;
|
|
25
25
|
warn: string;
|
|
26
|
-
/**
|
|
26
|
+
/** Orange foreground and existing background for tool calls blocked before execution. */
|
|
27
27
|
rejection: string;
|
|
28
28
|
rejectionBg: string;
|
|
29
29
|
selectionBg: string;
|
|
@@ -59,7 +59,7 @@ const tokyonight: Theme = {
|
|
|
59
59
|
success: "#9ece6a",
|
|
60
60
|
error: "#f7768e",
|
|
61
61
|
warn: "#ff9e64",
|
|
62
|
-
rejection: "#
|
|
62
|
+
rejection: "#ff9e64",
|
|
63
63
|
rejectionBg: "#332b24",
|
|
64
64
|
selectionBg: "#33467c",
|
|
65
65
|
popupBg: "#1f2335",
|
|
@@ -91,7 +91,7 @@ const gruvbox: Theme = {
|
|
|
91
91
|
success: "#b8bb26",
|
|
92
92
|
error: "#fb4934",
|
|
93
93
|
warn: "#fe8019",
|
|
94
|
-
rejection: "#
|
|
94
|
+
rejection: "#fe8019",
|
|
95
95
|
rejectionBg: "#3c3836",
|
|
96
96
|
selectionBg: "#504945",
|
|
97
97
|
popupBg: "#32302f",
|
|
@@ -123,7 +123,7 @@ const catppuccin: Theme = {
|
|
|
123
123
|
success: "#a6e3a1",
|
|
124
124
|
error: "#f38ba8",
|
|
125
125
|
warn: "#fab387",
|
|
126
|
-
rejection: "#
|
|
126
|
+
rejection: "#fab387",
|
|
127
127
|
rejectionBg: "#3b342f",
|
|
128
128
|
selectionBg: "#45475a",
|
|
129
129
|
popupBg: "#181825",
|
|
@@ -155,7 +155,7 @@ const nord: Theme = {
|
|
|
155
155
|
success: "#a3be8c",
|
|
156
156
|
error: "#bf616a",
|
|
157
157
|
warn: "#d08770",
|
|
158
|
-
rejection: "#
|
|
158
|
+
rejection: "#d08770",
|
|
159
159
|
rejectionBg: "#3b3b3b",
|
|
160
160
|
selectionBg: "#4c566a",
|
|
161
161
|
popupBg: "#3b4252",
|
|
@@ -187,7 +187,7 @@ const dracula: Theme = {
|
|
|
187
187
|
success: "#50fa7b",
|
|
188
188
|
error: "#ff5555",
|
|
189
189
|
warn: "#ffb86c",
|
|
190
|
-
rejection: "#
|
|
190
|
+
rejection: "#ffb86c",
|
|
191
191
|
rejectionBg: "#3d3f36",
|
|
192
192
|
selectionBg: "#44475a",
|
|
193
193
|
popupBg: "#21222c",
|
|
@@ -251,7 +251,7 @@ const solarized: Theme = {
|
|
|
251
251
|
success: "#859900",
|
|
252
252
|
error: "#dc322f",
|
|
253
253
|
warn: "#cb4b16",
|
|
254
|
-
rejection: "#
|
|
254
|
+
rejection: "#cb4b16",
|
|
255
255
|
rejectionBg: "#343b35",
|
|
256
256
|
selectionBg: "#0b4b59",
|
|
257
257
|
popupBg: "#073642",
|
|
@@ -283,7 +283,7 @@ const kanagawa: Theme = {
|
|
|
283
283
|
success: "#98bb6c",
|
|
284
284
|
error: "#e46876",
|
|
285
285
|
warn: "#ffa066",
|
|
286
|
-
rejection: "#
|
|
286
|
+
rejection: "#ffa066",
|
|
287
287
|
rejectionBg: "#39352d",
|
|
288
288
|
selectionBg: "#2d4f67",
|
|
289
289
|
popupBg: "#16161d",
|
|
@@ -315,7 +315,7 @@ const githubLight: Theme = {
|
|
|
315
315
|
success: "#1a7f37",
|
|
316
316
|
error: "#cf222e",
|
|
317
317
|
warn: "#bc4c00",
|
|
318
|
-
rejection: "#
|
|
318
|
+
rejection: "#bc4c00",
|
|
319
319
|
rejectionBg: "#fff1c2",
|
|
320
320
|
selectionBg: "#b6d7ff",
|
|
321
321
|
popupBg: "#f6f8fa",
|
package/src/transcript.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { StyledText, fg, type MarkdownRenderable, type SyntaxStyle } from "@opentui/core";
|
|
1
|
+
import { StyledText, bold, fg, type MarkdownRenderable, type SyntaxStyle } from "@opentui/core";
|
|
2
2
|
import type { MarkdownProps } from "@opentui/react";
|
|
3
3
|
import {
|
|
4
4
|
useBlinkingText,
|
|
@@ -353,6 +353,18 @@ export function toolStateGlyph(state: ToolCall["state"]): string {
|
|
|
353
353
|
return "✗";
|
|
354
354
|
}
|
|
355
355
|
|
|
356
|
+
const CHECK_MODE_HARD_BLOCK_PREFIX = "Check mode hard block:";
|
|
357
|
+
|
|
358
|
+
function rejectedDetail(theme: Theme, detail: string): StyledText {
|
|
359
|
+
if (!detail.startsWith(CHECK_MODE_HARD_BLOCK_PREFIX)) {
|
|
360
|
+
return new StyledText([fg(theme.rejection)(detail)]);
|
|
361
|
+
}
|
|
362
|
+
return new StyledText([
|
|
363
|
+
bold(fg(theme.rejection)(CHECK_MODE_HARD_BLOCK_PREFIX)),
|
|
364
|
+
fg(theme.rejection)(detail.slice(CHECK_MODE_HARD_BLOCK_PREFIX.length)),
|
|
365
|
+
]);
|
|
366
|
+
}
|
|
367
|
+
|
|
356
368
|
export function ToolLine({
|
|
357
369
|
theme,
|
|
358
370
|
call,
|
|
@@ -420,8 +432,7 @@ export function ToolLine({
|
|
|
420
432
|
{rejected && call.detail ? (
|
|
421
433
|
<Row glyph={GUTTER} glyphColor={theme.rejection} background={theme.rejectionBg}>
|
|
422
434
|
<text
|
|
423
|
-
content={call.detail}
|
|
424
|
-
fg={theme.rejection}
|
|
435
|
+
content={rejectedDetail(theme, call.detail)}
|
|
425
436
|
selectable
|
|
426
437
|
wrapMode="word"
|
|
427
438
|
style={{ flexGrow: 1, flexShrink: 1, minWidth: 0, width: "100%" }}
|