@bermudi/pi-delegate 0.1.14 → 0.1.15
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/dispatch.ts +1 -0
- package/format.ts +28 -1
- package/lifecycle.ts +4 -0
- package/package.json +1 -1
- package/render-branches.ts +16 -5
- package/task-resolution.ts +16 -2
- package/ticket-format.ts +7 -6
- package/tickets.ts +3 -1
- package/types.ts +15 -0
package/dispatch.ts
CHANGED
|
@@ -112,6 +112,7 @@ export function initProgress(resolved: ResolvedTask[]): TaskProgress[] {
|
|
|
112
112
|
id: t.id,
|
|
113
113
|
index: i,
|
|
114
114
|
agent: t.agentName,
|
|
115
|
+
resumedFrom: t.resumeFromDisplay,
|
|
115
116
|
task: trunc(sanitizeTerminalLine(t.prompt || t.sessionAction || ""), 50),
|
|
116
117
|
status: "pending" as const,
|
|
117
118
|
durationMs: 0,
|
package/format.ts
CHANGED
|
@@ -200,6 +200,33 @@ export function formatTaskId(id: string | undefined): string {
|
|
|
200
200
|
return safeId ? ` #${safeId}` : "";
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Short identity tag for a resumed transcript, derived from its session file
|
|
205
|
+
* path (pi names sessions `<timestamp>_<uuid>.jsonl`, so the UUID prefix is
|
|
206
|
+
* the stable part). Best-effort display identity only — never parsed back.
|
|
207
|
+
*/
|
|
208
|
+
export function formatResumeTag(resumeFrom: string): string {
|
|
209
|
+
const stem = sanitizeTerminalLine(resumeFrom).replace(/\.jsonl$/i, "");
|
|
210
|
+
const base = stem.split("/").pop() ?? stem;
|
|
211
|
+
const unique = base.includes("_") ? (base.split("_").pop() ?? base) : base;
|
|
212
|
+
return (unique.slice(0, 8) || "resumed").trim();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Plain-text revival marker for a progress row that continued an earlier
|
|
217
|
+
* transcript. Empty when the row is not a resume, or when the agent name
|
|
218
|
+
* already carries the resume identity (`resume:<tag>`, set at task resolution
|
|
219
|
+
* for omitted-agent resumes) — the marker must not duplicate it.
|
|
220
|
+
*/
|
|
221
|
+
export function resumeMarker(p: {
|
|
222
|
+
agent: string;
|
|
223
|
+
resumedFrom?: string;
|
|
224
|
+
}): string {
|
|
225
|
+
return p.resumedFrom && p.agent !== `resume:${p.resumedFrom}`
|
|
226
|
+
? ` ↻${p.resumedFrom}`
|
|
227
|
+
: "";
|
|
228
|
+
}
|
|
229
|
+
|
|
203
230
|
/**
|
|
204
231
|
* Extract a single-line preview of agent output for collapsed final display.
|
|
205
232
|
*
|
|
@@ -463,7 +490,7 @@ export function formatCompletedTask(
|
|
|
463
490
|
// `|| task.sessionAction` covers action-only tasks (close/list/...) where prompt is
|
|
464
491
|
// empty. Async prompt tasks always set prompt, so this is a no-op there.
|
|
465
492
|
parts.push(
|
|
466
|
-
`=== ${result.agent}${formatTaskId(result.id ?? task.id)}: ${trunc(task.prompt || task.sessionAction || "", 80)} ===`,
|
|
493
|
+
`=== ${result.agent}${resumeMarker(result)}${formatTaskId(result.id ?? task.id)}: ${trunc(task.prompt || task.sessionAction || "", 80)} ===`,
|
|
467
494
|
);
|
|
468
495
|
if (task.warnings?.length) {
|
|
469
496
|
for (const w of task.warnings) parts.push(`[WARNING: ${w}]`);
|
package/lifecycle.ts
CHANGED
|
@@ -118,6 +118,7 @@ function failTask(
|
|
|
118
118
|
return {
|
|
119
119
|
id: task.id,
|
|
120
120
|
agent: task.agentName,
|
|
121
|
+
resumedFrom: task.resumeFromDisplay,
|
|
121
122
|
output: "",
|
|
122
123
|
error,
|
|
123
124
|
failureKind,
|
|
@@ -200,6 +201,7 @@ function completeSessionAction(
|
|
|
200
201
|
return {
|
|
201
202
|
id: task.id,
|
|
202
203
|
agent: task.agentName,
|
|
204
|
+
resumedFrom: task.resumeFromDisplay,
|
|
203
205
|
output,
|
|
204
206
|
durationMs: elapsedMs ?? 0,
|
|
205
207
|
tokens: 0,
|
|
@@ -1237,6 +1239,7 @@ function deadlineExceededResult(
|
|
|
1237
1239
|
return {
|
|
1238
1240
|
id: task.id,
|
|
1239
1241
|
agent: task.agentName,
|
|
1242
|
+
resumedFrom: task.resumeFromDisplay,
|
|
1240
1243
|
output: prior?.output ?? "",
|
|
1241
1244
|
error: formatDeadlineExceededError(budgetMs),
|
|
1242
1245
|
failureKind: "deadline_exceeded",
|
|
@@ -1566,6 +1569,7 @@ async function runTaskAttempt(
|
|
|
1566
1569
|
return propagateSessionQuarantine(r, {
|
|
1567
1570
|
id: task.id,
|
|
1568
1571
|
agent: task.agentName,
|
|
1572
|
+
resumedFrom: task.resumeFromDisplay,
|
|
1569
1573
|
output: r.output,
|
|
1570
1574
|
error: r.error,
|
|
1571
1575
|
// Classify the failure: the runner sets `stalled` for the
|
package/package.json
CHANGED
package/render-branches.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
previewOutputLine,
|
|
15
15
|
taskTokenLabel,
|
|
16
16
|
waitingLabel,
|
|
17
|
+
resumeMarker,
|
|
17
18
|
} from "./format.ts";
|
|
18
19
|
import {
|
|
19
20
|
resolveCarriageReturn,
|
|
@@ -128,6 +129,11 @@ export function renderPartialBranch(ctx: BranchCtx, h: RenderHelpers): void {
|
|
|
128
129
|
const task = sanitizeTerminalLine(p.task);
|
|
129
130
|
const taskId = formatTaskId(p.id);
|
|
130
131
|
const taskIdTag = taskId ? theme.fg("accent", taskId) : "";
|
|
132
|
+
// Revival marker: a resumed row must never read as a fresh spawn. Empty
|
|
133
|
+
// when the identity already carries the resume label (omitted-agent
|
|
134
|
+
// resumes resolve to `resume:<tag>` at task resolution).
|
|
135
|
+
const resumeMarkRaw = resumeMarker(p);
|
|
136
|
+
const resumeMark = resumeMarkRaw ? theme.fg("warning", resumeMarkRaw) : "";
|
|
131
137
|
const runParts: string[] = [];
|
|
132
138
|
if (p.toolUses > 0)
|
|
133
139
|
runParts.push(`${p.toolUses} tool${p.toolUses > 1 ? "s" : ""}`);
|
|
@@ -137,7 +143,7 @@ export function renderPartialBranch(ctx: BranchCtx, h: RenderHelpers): void {
|
|
|
137
143
|
case "done":
|
|
138
144
|
lines.push(
|
|
139
145
|
truncLine(
|
|
140
|
-
`${tree(i, total)} ${theme.fg("success", "✓")} ${theme.bold(agent)}${taskIdTag}${theme.fg("muted", ` — ${task}`)}${expanded ? `${modelLabel(p)}${statJoin([fmtDuration(p.durationMs), taskTokenLabel(p)])}` : ""}`,
|
|
146
|
+
`${tree(i, total)} ${theme.fg("success", "✓")} ${theme.bold(agent)}${resumeMark}${taskIdTag}${theme.fg("muted", ` — ${task}`)}${expanded ? `${modelLabel(p)}${statJoin([fmtDuration(p.durationMs), taskTokenLabel(p)])}` : ""}`,
|
|
141
147
|
w,
|
|
142
148
|
),
|
|
143
149
|
);
|
|
@@ -158,7 +164,7 @@ export function renderPartialBranch(ctx: BranchCtx, h: RenderHelpers): void {
|
|
|
158
164
|
case "failed":
|
|
159
165
|
lines.push(
|
|
160
166
|
truncLine(
|
|
161
|
-
`${tree(i, total)} ${theme.fg("error", "✗")} ${theme.bold(agent)}${taskIdTag}${theme.fg("muted", ` — ${task}`)}${expanded ? modelLabel(p) : ""}${p.error ? theme.fg("error", ` · ${sanitizeTerminalLine(p.error)}`) : ""}`,
|
|
167
|
+
`${tree(i, total)} ${theme.fg("error", "✗")} ${theme.bold(agent)}${resumeMark}${taskIdTag}${theme.fg("muted", ` — ${task}`)}${expanded ? modelLabel(p) : ""}${p.error ? theme.fg("error", ` · ${sanitizeTerminalLine(p.error)}`) : ""}`,
|
|
162
168
|
w,
|
|
163
169
|
),
|
|
164
170
|
);
|
|
@@ -192,7 +198,7 @@ export function renderPartialBranch(ctx: BranchCtx, h: RenderHelpers): void {
|
|
|
192
198
|
const glyph = theme.fg("warning", spinnerFrame());
|
|
193
199
|
lines.push(
|
|
194
200
|
truncLine(
|
|
195
|
-
`${tree(i, total)} ${glyph} ${theme.bold(agent)}${taskIdTag}${theme.fg("muted", ` — ${task}`)}${expanded ? `${modelLabel(p)}${statJoin(runParts)}` : ""}${issueTag}${theme.fg("dim", ageTag)}`,
|
|
201
|
+
`${tree(i, total)} ${glyph} ${theme.bold(agent)}${resumeMark}${taskIdTag}${theme.fg("muted", ` — ${task}`)}${expanded ? `${modelLabel(p)}${statJoin(runParts)}` : ""}${issueTag}${theme.fg("dim", ageTag)}`,
|
|
196
202
|
w,
|
|
197
203
|
),
|
|
198
204
|
);
|
|
@@ -271,7 +277,7 @@ export function renderPartialBranch(ctx: BranchCtx, h: RenderHelpers): void {
|
|
|
271
277
|
);
|
|
272
278
|
lines.push(
|
|
273
279
|
truncLine(
|
|
274
|
-
`${tree(i, total)} ${theme.fg("muted", "○")} ${theme.bold(agent)}${taskIdTag}${theme.fg("muted", ` — ${task}`)}${expanded ? modelLabel(p) : ""}${queuedTag}`,
|
|
280
|
+
`${tree(i, total)} ${theme.fg("muted", "○")} ${theme.bold(agent)}${resumeMark}${taskIdTag}${theme.fg("muted", ` — ${task}`)}${expanded ? modelLabel(p) : ""}${queuedTag}`,
|
|
275
281
|
w,
|
|
276
282
|
),
|
|
277
283
|
);
|
|
@@ -431,6 +437,11 @@ export function renderFinalBranch(ctx: BranchCtx, h: RenderHelpers): void {
|
|
|
431
437
|
const taskId = formatTaskId(p.id);
|
|
432
438
|
const taskIdTag = taskId ? theme.fg("accent", taskId) : "";
|
|
433
439
|
const taskIdWidth = taskId.length;
|
|
440
|
+
// Revival marker: a resumed row must never read as a fresh spawn. Empty
|
|
441
|
+
// when the identity already carries the resume label (omitted-agent
|
|
442
|
+
// resumes resolve to `resume:<tag>` at task resolution).
|
|
443
|
+
const resumeMarkRaw = resumeMarker(p);
|
|
444
|
+
const resumeMark = resumeMarkRaw ? theme.fg("warning", resumeMarkRaw) : "";
|
|
434
445
|
const previewBudget = Math.max(1, w - 30 - taskIdWidth);
|
|
435
446
|
const taskPreview = theme.fg("muted", ` — ${trunc(task, previewBudget)}`);
|
|
436
447
|
const isLive =
|
|
@@ -445,7 +456,7 @@ export function renderFinalBranch(ctx: BranchCtx, h: RenderHelpers): void {
|
|
|
445
456
|
: "";
|
|
446
457
|
lines.push(
|
|
447
458
|
truncLine(
|
|
448
|
-
`${tree(i, total)} ${icon} ${theme.bold(agent)}${taskIdTag}${taskPreview}${expanded ? modelLabel(p) : ""}${isLive ? liveTail : cancelledTail || (expanded ? statJoin([fmtDuration(p.durationMs), taskTokenLabel(p)]) : "")}`,
|
|
459
|
+
`${tree(i, total)} ${icon} ${theme.bold(agent)}${resumeMark}${taskIdTag}${taskPreview}${expanded ? modelLabel(p) : ""}${isLive ? liveTail : cancelledTail || (expanded ? statJoin([fmtDuration(p.durationMs), taskTokenLabel(p)]) : "")}`,
|
|
449
460
|
w,
|
|
450
461
|
),
|
|
451
462
|
);
|
package/task-resolution.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
isSessionIdQuarantined,
|
|
19
19
|
} from "./session-quarantine.ts";
|
|
20
20
|
import { BUILTIN_AGENT_CONFIGS, buildSubagentSystemPrompt } from "./agents.ts";
|
|
21
|
+
import { formatResumeTag } from "./format.ts";
|
|
21
22
|
import { buildParentTranscript } from "./parent-context.ts";
|
|
22
23
|
import { findAvailableAlternative, resolveModelRequest } from "./model.ts";
|
|
23
24
|
import {
|
|
@@ -591,6 +592,14 @@ export function resolveTasks(
|
|
|
591
592
|
requestedSystemPrompt = systemPrompt;
|
|
592
593
|
}
|
|
593
594
|
|
|
595
|
+
// Freeze the display tag from the caller's path *before* lifecycle
|
|
596
|
+
// canonicalizes `resumeFrom` for locking/acquisition. A symlink whose
|
|
597
|
+
// basename differs from its target would otherwise make the settled row's
|
|
598
|
+
// `resumedFrom` disagree with the live progress row and `agentName`,
|
|
599
|
+
// defeating `resumeMarker`'s no-duplication rule.
|
|
600
|
+
const resumeFromDisplay = t.resumeFrom
|
|
601
|
+
? formatResumeTag(t.resumeFrom)
|
|
602
|
+
: undefined;
|
|
594
603
|
return {
|
|
595
604
|
...t,
|
|
596
605
|
id: t.id,
|
|
@@ -604,8 +613,13 @@ export function resolveTasks(
|
|
|
604
613
|
// display code treats "" and absent alike (`t.prompt || …`).
|
|
605
614
|
prompt: prompt ?? "",
|
|
606
615
|
// Keep the built-in selector visible in progress/results. Omitted-agent
|
|
607
|
-
// inline tasks retain the established `ad-hoc` label and config namespace
|
|
608
|
-
|
|
616
|
+
// inline tasks retain the established `ad-hoc` label and config namespace
|
|
617
|
+
// — except resumes: a continued transcript is not a fresh ad-hoc spawn,
|
|
618
|
+
// so it carries the resumed-transcript identity instead.
|
|
619
|
+
agentName:
|
|
620
|
+
agent?.name ??
|
|
621
|
+
(resumeFromDisplay ? `resume:${resumeFromDisplay}` : "ad-hoc"),
|
|
622
|
+
resumeFromDisplay,
|
|
609
623
|
warnings,
|
|
610
624
|
reuseIntent: {
|
|
611
625
|
model: requestedModel,
|
package/ticket-format.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
relativeTouchedSummary,
|
|
11
11
|
findTouchedOverlaps,
|
|
12
12
|
formatTouchedOverlapWarning,
|
|
13
|
+
resumeMarker,
|
|
13
14
|
} from "./format.ts";
|
|
14
15
|
import { renderOutputForPoll } from "./spill.ts";
|
|
15
16
|
import { getOutputSpillTail, getOutputSpillThreshold } from "./config.ts";
|
|
@@ -93,12 +94,12 @@ export function formatInFlightTaskLine(p: TaskProgress): string {
|
|
|
93
94
|
if (p.tokens > 0) parts.push(`${fmtTokens(p.tokens)} tokens`);
|
|
94
95
|
const age = getActivityAge(p.lastActivityAt);
|
|
95
96
|
if (age) parts.push(age);
|
|
96
|
-
return `⏳ ${p.agent}${formatTaskId(p.id)} · ${parts.join(" · ")}`;
|
|
97
|
+
return `⏳ ${p.agent}${resumeMarker(p)}${formatTaskId(p.id)} · ${parts.join(" · ")}`;
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
/** Queued-task line shared by poll snapshots and cancel previews. */
|
|
100
101
|
export function formatQueuedTaskLine(p: TaskProgress): string {
|
|
101
|
-
return `○ ${p.agent}${formatTaskId(p.id)} · waiting…`;
|
|
102
|
+
return `○ ${p.agent}${resumeMarker(p)}${formatTaskId(p.id)} · waiting…`;
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
function appendTouchedMeta(
|
|
@@ -123,7 +124,7 @@ function formatSettledPollLines(
|
|
|
123
124
|
const thresholdChars = getOutputSpillThreshold(ticket.config);
|
|
124
125
|
if (!failed) {
|
|
125
126
|
const lines = [
|
|
126
|
-
`✓ ${result.agent}${formatTaskId(result.id)} · ${meta.join(" · ")}`,
|
|
127
|
+
`✓ ${result.agent}${resumeMarker(result)}${formatTaskId(result.id)} · ${meta.join(" · ")}`,
|
|
127
128
|
];
|
|
128
129
|
if (result.output && result.output !== "(no output)") {
|
|
129
130
|
lines.push(
|
|
@@ -134,7 +135,7 @@ function formatSettledPollLines(
|
|
|
134
135
|
}
|
|
135
136
|
const errorText = result.error ?? "unknown error";
|
|
136
137
|
const lines = [
|
|
137
|
-
`✗ ${result.agent}${formatTaskId(result.id)} · ${errorText} · ${meta.join(" · ")}`,
|
|
138
|
+
`✗ ${result.agent}${resumeMarker(result)}${formatTaskId(result.id)} · ${errorText} · ${meta.join(" · ")}`,
|
|
138
139
|
];
|
|
139
140
|
if (result.sessionFile)
|
|
140
141
|
lines.push(` session: ${shortenPath(result.sessionFile)}`);
|
|
@@ -273,9 +274,9 @@ export function formatCancelPreview(ticket: AsyncTicket): string {
|
|
|
273
274
|
|
|
274
275
|
for (const p of ticket.progress) {
|
|
275
276
|
if (p.status === "done") {
|
|
276
|
-
lines.push(`✓ ${p.agent}${formatTaskId(p.id)} · completed`);
|
|
277
|
+
lines.push(`✓ ${p.agent}${resumeMarker(p)}${formatTaskId(p.id)} · completed`);
|
|
277
278
|
} else if (p.status === "failed") {
|
|
278
|
-
lines.push(`✗ ${p.agent}${formatTaskId(p.id)} · ${p.error ?? "failed"}`);
|
|
279
|
+
lines.push(`✗ ${p.agent}${resumeMarker(p)}${formatTaskId(p.id)} · ${p.error ?? "failed"}`);
|
|
279
280
|
} else if (p.status === "running") {
|
|
280
281
|
lines.push(formatInFlightTaskLine(p));
|
|
281
282
|
} else {
|
package/tickets.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
fmtDuration,
|
|
12
12
|
formatCompletedTask,
|
|
13
13
|
formatTaskId,
|
|
14
|
+
resumeMarker,
|
|
14
15
|
trunc,
|
|
15
16
|
findTouchedOverlaps,
|
|
16
17
|
formatTouchedOverlapWarning,
|
|
@@ -268,7 +269,7 @@ export function formatCompletedTicket(
|
|
|
268
269
|
const t = ticket.resolved[i]!;
|
|
269
270
|
if (!r) {
|
|
270
271
|
parts.push(
|
|
271
|
-
`=== ${t.agentName}${formatTaskId(t.id)}: ${trunc(t.prompt || "", 80)} ===`,
|
|
272
|
+
`=== ${t.agentName}${resumeMarker(ticket.progress[i]!)}${formatTaskId(t.id)}: ${trunc(t.prompt || "", 80)} ===`,
|
|
272
273
|
);
|
|
273
274
|
parts.push(`[${pendingLabelFor(i)}]`);
|
|
274
275
|
continue;
|
|
@@ -323,6 +324,7 @@ function pendingResultPlaceholder(task: ResolvedTask | undefined): TaskResult {
|
|
|
323
324
|
return {
|
|
324
325
|
id: task?.id,
|
|
325
326
|
agent: task?.agentName ?? "unknown",
|
|
327
|
+
resumedFrom: task?.resumeFromDisplay,
|
|
326
328
|
output: "",
|
|
327
329
|
durationMs: 0,
|
|
328
330
|
tokens: 0,
|
package/types.ts
CHANGED
|
@@ -148,6 +148,14 @@ export interface ResolvedTask {
|
|
|
148
148
|
sessionId?: string;
|
|
149
149
|
sessionAction?: SessionAction;
|
|
150
150
|
resumeFrom?: string;
|
|
151
|
+
/** Display tag (`formatResumeTag`) of the *caller's* `resumeFrom` path, frozen
|
|
152
|
+
* once at task resolution. `resumeFrom` itself is later replaced by the
|
|
153
|
+
* canonical transcript path for locking/acquisition/quarantine, which can
|
|
154
|
+
* have a different basename (symlink alias vs target). Settled-result and
|
|
155
|
+
* progress `resumedFrom` tags must read this field — never re-derive from the
|
|
156
|
+
* now-canonical `resumeFrom` — so the live and settled rows agree and
|
|
157
|
+
* `resumeMarker`'s no-duplication rule holds. */
|
|
158
|
+
resumeFromDisplay?: string;
|
|
151
159
|
/** Hard wall-clock budget in milliseconds, measured from task start. */
|
|
152
160
|
deadlineMs?: number;
|
|
153
161
|
agentName: string;
|
|
@@ -228,6 +236,9 @@ export interface TaskProgress {
|
|
|
228
236
|
id?: string;
|
|
229
237
|
index: number;
|
|
230
238
|
agent: string;
|
|
239
|
+
/** Short tag (via `formatResumeTag`) of the transcript this task continued
|
|
240
|
+
* via `resumeFrom`, if any. Lets renderers mark the row as a revival. */
|
|
241
|
+
resumedFrom?: string;
|
|
231
242
|
task: string;
|
|
232
243
|
status: "pending" | "running" | "done" | "failed";
|
|
233
244
|
durationMs: number;
|
|
@@ -267,6 +278,10 @@ export interface DelegateDetails {
|
|
|
267
278
|
export interface TaskResult {
|
|
268
279
|
id?: string;
|
|
269
280
|
agent: string;
|
|
281
|
+
/** Short tag (via `formatResumeTag`) of the transcript this task continued
|
|
282
|
+
* via `resumeFrom`, if any. Lets settled-result renderers mark the row as
|
|
283
|
+
* a revival — mirrors `TaskProgress.resumedFrom`. */
|
|
284
|
+
resumedFrom?: string;
|
|
270
285
|
output: string;
|
|
271
286
|
error?: string;
|
|
272
287
|
/** Stable machine-readable failure reason; error remains human-facing. */
|