@bermudi/pi-delegate 0.1.18 → 0.1.20
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 +64 -15
- package/agents.ts +1 -1
- package/assistant-preview.ts +31 -0
- package/browser-state.ts +250 -0
- package/browser.ts +334 -0
- package/concurrency.ts +7 -0
- package/delegate.ts +8 -0
- package/dispatch.ts +512 -163
- package/extension.ts +65 -28
- package/format.ts +27 -9
- package/host.ts +1 -1
- package/isolated-workspace.ts +154 -8
- package/lifecycle.ts +34 -20
- package/manual.ts +21 -8
- package/package.json +2 -1
- package/parent-context.ts +1 -1
- package/pause.ts +81 -0
- package/pool.ts +492 -428
- package/render-branches.ts +19 -5
- package/render-result.ts +7 -0
- package/runner.ts +55 -1
- package/runtime.ts +36 -0
- package/schema.ts +29 -18
- package/status.ts +38 -11
- package/task-resolution.ts +12 -9
- package/test-harness.ts +81 -0
- package/ticket-format.ts +29 -7
- package/tickets.ts +724 -576
- package/types.ts +33 -0
- package/workspace.ts +58 -27
package/README.md
CHANGED
|
@@ -35,17 +35,16 @@ delegate({
|
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
Parent extension/MCP tools are not copied, and project instructions are rebuilt
|
|
38
|
-
for the task's `cwd`. Omit `agent` when you want an
|
|
39
|
-
normal
|
|
38
|
+
for the task's `cwd`. Omit `agent` when you want an inline task using delegate's
|
|
39
|
+
normal defaults instead.
|
|
40
40
|
|
|
41
41
|
The other built-ins are:
|
|
42
42
|
|
|
43
43
|
- `scout` — read-only investigation with `read`, `grep`, `find`, and `ls`.
|
|
44
44
|
- `coder` — implementation and verification with `read`, `write`, `edit`, and
|
|
45
45
|
`bash` in the shared workspace.
|
|
46
|
-
- `reviewer` — review with `read` and `bash
|
|
47
|
-
|
|
48
|
-
`sessionId`.
|
|
46
|
+
- `reviewer` — review with `read` and `bash` in the shared workspace by default.
|
|
47
|
+
Set `workspace: "scratch"` when its project changes should be discarded.
|
|
49
48
|
|
|
50
49
|
### Shared-write safety
|
|
51
50
|
|
|
@@ -64,7 +63,7 @@ visible batch-level warning while it is active.
|
|
|
64
63
|
A same-named Markdown file can override any built-in (first definition wins
|
|
65
64
|
across `.pi/agents/`, `~/.pi/agent/agents/`, `~/.agents/`, `.claude/agents/`,
|
|
66
65
|
`~/.claude/agents/`). A prompt-only override keeps the built-in's tools and
|
|
67
|
-
workspace — `scout` stays read-only and `reviewer` stays
|
|
66
|
+
workspace — `scout` stays read-only and `reviewer` stays shared unless the
|
|
68
67
|
file explicitly sets `tools` or `workspace`. Fresh built-ins inherit the
|
|
69
68
|
parent's exact model object and thinking level; an explicit `model`/`thinking`
|
|
70
69
|
in the Markdown file replaces that inheritance. Task fields always win. For
|
|
@@ -106,7 +105,7 @@ host filesystem.
|
|
|
106
105
|
|
|
107
106
|
### Git-native isolated writers
|
|
108
107
|
|
|
109
|
-
Use `workspace: "isolated"` for
|
|
108
|
+
Use `workspace: "isolated"` for sync or async one-shot coding tasks that should
|
|
110
109
|
run in parallel without sharing a working tree:
|
|
111
110
|
|
|
112
111
|
```ts
|
|
@@ -128,10 +127,12 @@ still matches the baseline.
|
|
|
128
127
|
|
|
129
128
|
A clean reconciliation reports `applied_unverified`: textual merging does not
|
|
130
129
|
prove the code builds or tests pass, so the result includes a suggested
|
|
131
|
-
verification call.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
130
|
+
verification call. Async isolated tasks prepare after returning their ticket and
|
|
131
|
+
settle only after reconciliation. Cancellation never applies unfinished work; a
|
|
132
|
+
completed proposal cancelled before source application is retained as a private
|
|
133
|
+
ref and full patch. Isolated mode requires Git, rejects session reuse, disables
|
|
134
|
+
whole-task retries, and rejects repositories with submodules. It is separation,
|
|
135
|
+
not confinement: absolute-path writes can still reach the host.
|
|
135
136
|
|
|
136
137
|
### Token accounting
|
|
137
138
|
|
|
@@ -150,7 +151,7 @@ Async tickets keep running after the parent's turn settles, and pi renders an
|
|
|
150
151
|
idle session — so delegate adds three signals:
|
|
151
152
|
|
|
152
153
|
- **Footer status** — while any ticket is active, the footer shows
|
|
153
|
-
`⏳ 2 subagents · t5042v19`, updated live as subagents start and finish.
|
|
154
|
+
`⏳ 2 subagents · t5042v19 · /subagents`, updated live as subagents start and finish.
|
|
154
155
|
- **Settle warning** — the first time a turn settles with a ticket still
|
|
155
156
|
active, a warning notification names the ticket and reminds you that
|
|
156
157
|
quitting aborts it. Once per ticket; the footer carries it from there.
|
|
@@ -163,6 +164,54 @@ the mitigation there; on quit, delegate also prints a trace line to the
|
|
|
163
164
|
terminal naming the aborted tickets and agents, and on `/reload` it shows a
|
|
164
165
|
warning notification.
|
|
165
166
|
|
|
167
|
+
### Live subagent browser
|
|
168
|
+
|
|
169
|
+
Press **Ctrl+Shift+B** from the editor, or run **`/subagents`**, to open a live
|
|
170
|
+
browser without sending a model request. It shows both sync tasks and async
|
|
171
|
+
tickets, including retained completed work. Your draft is left untouched.
|
|
172
|
+
|
|
173
|
+
| Key | Action |
|
|
174
|
+
| --- | --- |
|
|
175
|
+
| ↑ / ↓ | Select an agent |
|
|
176
|
+
| Tab / ← / → | Switch between tool activity and assistant responses |
|
|
177
|
+
| PgUp / PgDn | Scroll the selected view |
|
|
178
|
+
| Home / End | Jump to oldest retained text / follow live output |
|
|
179
|
+
| p | Pause or resume the selected agent's **whole async ticket** |
|
|
180
|
+
| Esc | Close and return to your draft |
|
|
181
|
+
|
|
182
|
+
The view shows running commands, tool results, edited file paths, queue
|
|
183
|
+
dependencies, errors, token counts and activity age. Pause controls use the
|
|
184
|
+
same cooperative boundary described below; they do not freeze subprocesses.
|
|
185
|
+
Down in the editor keeps its normal behavior rather than intercepting menus
|
|
186
|
+
or replacing a custom editor.
|
|
187
|
+
|
|
188
|
+
This is a bounded live preview, not a transcript archive: assistant text keeps
|
|
189
|
+
the current attempt's last 32K characters (no thinking blocks); tool activity
|
|
190
|
+
shows up to 100 recent calls with output tails. The browser retains the last
|
|
191
|
+
20 completed sync calls. Async results remain available until the existing
|
|
192
|
+
ticket cleanup removes them (eligible after 30 minutes). Nothing is recovered
|
|
193
|
+
from transcripts after a restart. A refresh timer runs only while the browser
|
|
194
|
+
is open and is cleared on close or shutdown. This feature requires Pi's TUI,
|
|
195
|
+
not print/JSON/RPC mode.
|
|
196
|
+
|
|
197
|
+
### Pause and resume
|
|
198
|
+
|
|
199
|
+
Pause an async ticket with `delegate({ ticketAction: "pause", ticket: "<id>" })`;
|
|
200
|
+
continue it with `ticketAction: "resume"`. Both return immediately.
|
|
201
|
+
|
|
202
|
+
Each current model response and its tool calls finish, then the agent waits
|
|
203
|
+
before its next model request. Queued tasks do not start. **Pausing** means
|
|
204
|
+
work is reaching that boundary; **paused** means it has reached it. A task
|
|
205
|
+
that finishes naturally may complete instead. Resume keeps the same live
|
|
206
|
+
conversations, including scratch workspaces.
|
|
207
|
+
|
|
208
|
+
Pause is not cancellation, rollback, or a process freeze. Background commands
|
|
209
|
+
can keep running, and files may be unfinished. In-progress isolated preparation
|
|
210
|
+
or application finishes before pausing. Sessions, concurrency slots and
|
|
211
|
+
workspace reservations remain held. Inactivity checks stop while parked, but
|
|
212
|
+
explicit wall-clock deadlines keep counting. Wait does not resume work; cancel
|
|
213
|
+
still works. Pauses do not survive Pi exit or reload.
|
|
214
|
+
|
|
166
215
|
### Stall detection and cancellation
|
|
167
216
|
|
|
168
217
|
`stallTimeoutMs` is an inactivity watchdog, not a hard execution deadline. When
|
|
@@ -216,8 +265,8 @@ over an installed extension.
|
|
|
216
265
|
and workspace, and an explicit `model`/`thinking` replaces parent inheritance.
|
|
217
266
|
For `default`, the `delegate.json` override maps are ignored (task fields still
|
|
218
267
|
win); for the other built-ins, those maps win field-by-field over frontmatter.
|
|
219
|
-
- **
|
|
220
|
-
named Markdown agent profile. In current output this is labeled `
|
|
268
|
+
- **Inline subagent** — A subagent created from inline task fields instead of a
|
|
269
|
+
named Markdown agent profile. In current output this is labeled `inline`.
|
|
221
270
|
- **Inline task** — The task object itself when its configuration is supplied
|
|
222
271
|
directly in the delegate call. Prefer this term over “inline agent” when
|
|
223
272
|
talking about the API shape.
|
|
@@ -230,7 +279,7 @@ over an installed extension.
|
|
|
230
279
|
- **Resumed subagent** — A subagent rehydrated from a previous session `.jsonl`
|
|
231
280
|
via `resumeFrom`. It can also be pooled by providing a `sessionId`.
|
|
232
281
|
- **Async ticket** — A background execution handle returned when top-level
|
|
233
|
-
`async: true` is used. Poll, wait, or cancel tickets with top-level
|
|
282
|
+
`async: true` is used. Poll, wait, pause, resume, or cancel tickets with top-level
|
|
234
283
|
`ticketAction: "poll"`, `ticketAction: "wait"` (blocks until the ticket settles;
|
|
235
284
|
optional `timeoutMs`), or `ticketAction: "cancel"`.
|
|
236
285
|
- **Skill** — A `SKILL.md` instruction bundle injected into the subagent system
|
package/agents.ts
CHANGED
|
@@ -120,7 +120,7 @@ export const BUILTIN_AGENT_CONFIGS: Readonly<Record<string, AgentConfig>> = {
|
|
|
120
120
|
[DEFAULT_AGENT_NAME]: {
|
|
121
121
|
name: DEFAULT_AGENT_NAME,
|
|
122
122
|
description:
|
|
123
|
-
"General-purpose subagent mirroring the live parent model, thinking level, native tools, and base prompt. Prefer it for general work; pick a specialist
|
|
123
|
+
"General-purpose subagent mirroring the live parent model, thinking level, native tools, and base prompt. Prefer it for general work; pick a specialist when its role fits.",
|
|
124
124
|
tools: DEFAULT_TOOLS,
|
|
125
125
|
systemPrompt: "",
|
|
126
126
|
builtin: true,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Human-facing, bounded text preview. Never includes thinking blocks or reads
|
|
2
|
+
* transcripts from disk. Updates replace the current response, not append
|
|
3
|
+
* streaming snapshots; completed responses remain available for scrollback. */
|
|
4
|
+
export class AssistantPreview {
|
|
5
|
+
private completed = "";
|
|
6
|
+
private current = "";
|
|
7
|
+
static readonly limit = 32_768;
|
|
8
|
+
|
|
9
|
+
update(text: string): void {
|
|
10
|
+
// Empty synthetic provider failures must not erase a streamed response.
|
|
11
|
+
if (text) this.current = text.slice(-AssistantPreview.limit);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
finish(text: string): void {
|
|
15
|
+
this.update(text);
|
|
16
|
+
if (this.current) {
|
|
17
|
+
this.completed = [this.completed, this.current]
|
|
18
|
+
.filter(Boolean)
|
|
19
|
+
.join("\n\n")
|
|
20
|
+
.slice(-AssistantPreview.limit);
|
|
21
|
+
this.current = "";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get text(): string {
|
|
26
|
+
return [this.completed, this.current]
|
|
27
|
+
.filter(Boolean)
|
|
28
|
+
.join("\n\n")
|
|
29
|
+
.slice(-AssistantPreview.limit);
|
|
30
|
+
}
|
|
31
|
+
}
|
package/browser-state.ts
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import type { DelegateRuntime } from "./runtime.ts";
|
|
2
|
+
import type {
|
|
3
|
+
AsyncTicket,
|
|
4
|
+
DelegateDetails,
|
|
5
|
+
TaskProgress,
|
|
6
|
+
ToolActivity,
|
|
7
|
+
} from "./types.ts";
|
|
8
|
+
|
|
9
|
+
interface SyncDetails {
|
|
10
|
+
tasks: { prompt?: string }[];
|
|
11
|
+
progress: TaskProgress[];
|
|
12
|
+
results: { output?: string; error?: string }[];
|
|
13
|
+
serializedNotice?: string;
|
|
14
|
+
dispatchWarning?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Finished sync calls must not pin entire tool arguments/results after the
|
|
18
|
+
* parent compacts. Keep only primitive display arguments and bounded text. */
|
|
19
|
+
function compactProgress(p: TaskProgress): TaskProgress {
|
|
20
|
+
let budget = 65_536;
|
|
21
|
+
const activities: ToolActivity[] = [];
|
|
22
|
+
const take = (text: string): string => {
|
|
23
|
+
const limit = Math.min(4096, budget);
|
|
24
|
+
if (limit <= 0) return "";
|
|
25
|
+
const value = text.length > limit ? `…${text.slice(-limit)}` : text;
|
|
26
|
+
budget -= value.length;
|
|
27
|
+
return value;
|
|
28
|
+
};
|
|
29
|
+
for (const tool of p.activities.slice(-100).reverse()) {
|
|
30
|
+
if (budget <= 0) break;
|
|
31
|
+
const args: Record<string, unknown> = {};
|
|
32
|
+
for (const key of [
|
|
33
|
+
"path",
|
|
34
|
+
"file_path",
|
|
35
|
+
"command",
|
|
36
|
+
"pattern",
|
|
37
|
+
"query",
|
|
38
|
+
"url",
|
|
39
|
+
"task",
|
|
40
|
+
"prompt",
|
|
41
|
+
"offset",
|
|
42
|
+
"limit",
|
|
43
|
+
]) {
|
|
44
|
+
const value = tool.args[key];
|
|
45
|
+
if (typeof value === "string" && budget > 0) args[key] = take(value);
|
|
46
|
+
else if (typeof value === "number") args[key] = value;
|
|
47
|
+
}
|
|
48
|
+
const output = tool.result
|
|
49
|
+
? tool.result.content
|
|
50
|
+
.filter((part) => part.type === "text")
|
|
51
|
+
.slice(-20)
|
|
52
|
+
.map((part) => part.text?.slice(-4096) ?? "")
|
|
53
|
+
.join("\n")
|
|
54
|
+
: (tool.liveOutput ?? "");
|
|
55
|
+
const text = budget > 0 ? take(output) : "";
|
|
56
|
+
activities.push({
|
|
57
|
+
id: tool.id,
|
|
58
|
+
name: tool.name,
|
|
59
|
+
args,
|
|
60
|
+
startTime: tool.startTime,
|
|
61
|
+
endTime: tool.endTime,
|
|
62
|
+
result: tool.result
|
|
63
|
+
? { isError: tool.result.isError, content: [{ type: "text", text }] }
|
|
64
|
+
: undefined,
|
|
65
|
+
liveOutput: tool.result ? undefined : text,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
...p,
|
|
70
|
+
assistantPreview: p.assistantPreview?.slice(-32_768),
|
|
71
|
+
error: p.error?.slice(0, 4096),
|
|
72
|
+
activities: activities.reverse(),
|
|
73
|
+
warnings: [
|
|
74
|
+
...(p.warnings ?? []).slice(0, 10).map((s) => s.slice(0, 4096)),
|
|
75
|
+
...(activities.length < p.activities.length
|
|
76
|
+
? ["Earlier tool activity omitted from retained sync preview."]
|
|
77
|
+
: []),
|
|
78
|
+
],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function compactDetails(details: SyncDetails): SyncDetails {
|
|
83
|
+
return {
|
|
84
|
+
tasks: details.tasks.map((task) => ({
|
|
85
|
+
prompt: task.prompt?.slice(0, 4096),
|
|
86
|
+
})),
|
|
87
|
+
progress: details.progress.map(compactProgress),
|
|
88
|
+
results: details.results.map((result) => ({
|
|
89
|
+
output: result.output?.slice(-32_768),
|
|
90
|
+
error: result.error?.slice(0, 4096),
|
|
91
|
+
})),
|
|
92
|
+
serializedNotice: details.serializedNotice?.slice(0, 4096),
|
|
93
|
+
dispatchWarning: details.dispatchWarning?.slice(0, 4096),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
interface SyncRun {
|
|
98
|
+
details: SyncDetails;
|
|
99
|
+
finished: boolean;
|
|
100
|
+
created: number;
|
|
101
|
+
error?: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** UI-only history. Active sync runs are never evicted; retain the last twenty
|
|
105
|
+
* finished calls. Async history stays owned by the existing ticket registry. */
|
|
106
|
+
export class BrowserHistory {
|
|
107
|
+
readonly runs = new Map<string, SyncRun>();
|
|
108
|
+
generation = 0;
|
|
109
|
+
|
|
110
|
+
reset(): void {
|
|
111
|
+
this.generation++;
|
|
112
|
+
this.runs.clear();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
update(
|
|
116
|
+
id: string,
|
|
117
|
+
details: DelegateDetails | undefined,
|
|
118
|
+
finished: boolean,
|
|
119
|
+
generation = this.generation,
|
|
120
|
+
): void {
|
|
121
|
+
if (generation !== this.generation) return;
|
|
122
|
+
if (!details?.progress.length) {
|
|
123
|
+
if (finished)
|
|
124
|
+
this.fail(
|
|
125
|
+
id,
|
|
126
|
+
"Dispatch ended before tasks could start; see the delegate result.",
|
|
127
|
+
);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const created = this.runs.get(id)?.created ?? Date.now();
|
|
131
|
+
this.runs.delete(id);
|
|
132
|
+
this.runs.set(id, {
|
|
133
|
+
details: finished ? compactDetails(details) : details,
|
|
134
|
+
finished,
|
|
135
|
+
created,
|
|
136
|
+
});
|
|
137
|
+
this.prune();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
fail(id: string, error: unknown, generation = this.generation): void {
|
|
141
|
+
if (generation !== this.generation) return;
|
|
142
|
+
const run = this.runs.get(id);
|
|
143
|
+
if (!run) return;
|
|
144
|
+
run.finished = true;
|
|
145
|
+
run.details = compactDetails(run.details);
|
|
146
|
+
run.error = (error instanceof Error ? error.message : String(error)).slice(
|
|
147
|
+
0,
|
|
148
|
+
4096,
|
|
149
|
+
);
|
|
150
|
+
this.runs.delete(id);
|
|
151
|
+
this.runs.set(id, run);
|
|
152
|
+
this.prune();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private prune(): void {
|
|
156
|
+
const finished = [...this.runs].filter(([, run]) => run.finished);
|
|
157
|
+
for (const [id] of finished.slice(0, -20)) this.runs.delete(id);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface BrowserRow {
|
|
162
|
+
key: string;
|
|
163
|
+
batch: string;
|
|
164
|
+
created: number;
|
|
165
|
+
progress: TaskProgress;
|
|
166
|
+
siblings: TaskProgress[];
|
|
167
|
+
prompt: string;
|
|
168
|
+
output?: string;
|
|
169
|
+
error?: string;
|
|
170
|
+
ticket?: AsyncTicket;
|
|
171
|
+
finished: boolean;
|
|
172
|
+
notice?: string;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function browserRows(
|
|
176
|
+
runtime: DelegateRuntime,
|
|
177
|
+
history: BrowserHistory,
|
|
178
|
+
): BrowserRow[] {
|
|
179
|
+
const rows: BrowserRow[] = [];
|
|
180
|
+
for (const ticket of runtime.tickets.values()) {
|
|
181
|
+
for (const progress of ticket.progress) {
|
|
182
|
+
const result = ticket.results[progress.index];
|
|
183
|
+
rows.push({
|
|
184
|
+
key: `${ticket.id}:${progress.index}`,
|
|
185
|
+
batch: ticket.id,
|
|
186
|
+
created: ticket.created,
|
|
187
|
+
progress,
|
|
188
|
+
siblings: ticket.progress,
|
|
189
|
+
prompt: ticket.tasks[progress.index]?.prompt ?? progress.task,
|
|
190
|
+
output: result?.output,
|
|
191
|
+
error: result?.error ?? ticket.error,
|
|
192
|
+
ticket,
|
|
193
|
+
finished: ticket.status !== "running" && ticket.status !== "cancelling",
|
|
194
|
+
notice: [ticket.serializedNotice, ticket.dispatchWarning]
|
|
195
|
+
.filter(Boolean)
|
|
196
|
+
.join("\n"),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
for (const [id, run] of history.runs) {
|
|
201
|
+
for (const progress of run.details.progress) {
|
|
202
|
+
const result = run.details.results[progress.index];
|
|
203
|
+
rows.push({
|
|
204
|
+
key: `sync:${id}:${progress.index}`,
|
|
205
|
+
batch: `sync ${id.slice(-8)}`,
|
|
206
|
+
created: run.created,
|
|
207
|
+
progress,
|
|
208
|
+
siblings: run.details.progress,
|
|
209
|
+
prompt: run.details.tasks[progress.index]?.prompt ?? progress.task,
|
|
210
|
+
output: result?.output,
|
|
211
|
+
error: result?.error ?? run.error,
|
|
212
|
+
finished: run.finished,
|
|
213
|
+
notice: [run.details.serializedNotice, run.details.dispatchWarning]
|
|
214
|
+
.filter(Boolean)
|
|
215
|
+
.join("\n"),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return rows.sort(
|
|
220
|
+
(a, b) =>
|
|
221
|
+
Number(a.finished) - Number(b.finished) ||
|
|
222
|
+
b.created - a.created ||
|
|
223
|
+
a.batch.localeCompare(b.batch) ||
|
|
224
|
+
a.progress.index - b.progress.index,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function browserRowStatus(row: BrowserRow): string {
|
|
229
|
+
const p = row.progress;
|
|
230
|
+
if (p.incomplete) return "incomplete — worker may still be active";
|
|
231
|
+
if (p.status === "done") return "done";
|
|
232
|
+
if (p.status === "failed") return "failed";
|
|
233
|
+
if (row.ticket?.status === "cancelling") return "cancelling";
|
|
234
|
+
if (row.finished)
|
|
235
|
+
return row.error ? "failed" : (row.ticket?.status ?? "finished");
|
|
236
|
+
if (p.paused) return "paused";
|
|
237
|
+
if (p.status === "pending") {
|
|
238
|
+
if (row.ticket?.pause && row.ticket.pause.state !== "running")
|
|
239
|
+
return "queued — ticket paused";
|
|
240
|
+
const before =
|
|
241
|
+
p.waitingFor === undefined ? undefined : row.siblings[p.waitingFor];
|
|
242
|
+
if (before && before.status !== "done" && before.status !== "failed")
|
|
243
|
+
return `queued — waiting for ${before.id ?? `task ${before.index + 1}`}`;
|
|
244
|
+
return "queued — waiting for preparation or capacity";
|
|
245
|
+
}
|
|
246
|
+
const phase = p.activity ?? "starting";
|
|
247
|
+
return row.ticket?.pause?.state === "pausing"
|
|
248
|
+
? `${phase} — pausing after turn`
|
|
249
|
+
: phase;
|
|
250
|
+
}
|