@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bermudi/pi-delegate",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "description": "Delegate tool for the Pi coding agent.",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -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
  );
@@ -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
- agentName: agent?.name ?? "ad-hoc",
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. */