pi-crew 0.1.43 → 0.1.45
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/CHANGELOG.md +27 -0
- package/agents/analyst.md +11 -11
- package/agents/critic.md +11 -11
- package/agents/executor.md +11 -11
- package/agents/explorer.md +11 -11
- package/agents/planner.md +11 -11
- package/agents/reviewer.md +11 -11
- package/agents/security-reviewer.md +11 -11
- package/agents/test-engineer.md +11 -11
- package/agents/verifier.md +11 -11
- package/agents/writer.md +11 -11
- package/docs/refactor-tasks-phase3.md +394 -394
- package/docs/refactor-tasks-phase4.md +564 -564
- package/docs/refactor-tasks-phase5.md +402 -402
- package/docs/refactor-tasks-phase6.md +662 -662
- package/docs/research-extension-examples.md +297 -297
- package/docs/research-extension-system.md +324 -324
- package/docs/research-optimization-plan.md +548 -548
- package/docs/research-phase10-distillation.md +199 -0
- package/docs/research-phase11-distillation.md +201 -0
- package/docs/research-pi-coding-agent.md +357 -357
- package/docs/research-source-pi-crew-reference.md +174 -174
- package/docs/runtime-flow.md +148 -148
- package/docs/source-runtime-refactor-map.md +83 -83
- package/index.ts +6 -6
- package/package.json +1 -1
- package/src/agents/agent-serializer.ts +34 -34
- package/src/agents/discover-agents.ts +1 -0
- package/src/config/config.ts +19 -0
- package/src/extension/cross-extension-rpc.ts +82 -82
- package/src/extension/register.ts +134 -8
- package/src/extension/registration/commands.ts +18 -2
- package/src/extension/registration/compaction-guard.ts +125 -125
- package/src/extension/registration/subagent-tools.ts +148 -148
- package/src/extension/registration/team-tool.ts +27 -8
- package/src/extension/run-bundle-schema.ts +89 -89
- package/src/extension/run-index.ts +19 -0
- package/src/extension/run-maintenance.ts +43 -43
- package/src/extension/team-tool/api.ts +1 -1
- package/src/extension/team-tool/cancel.ts +79 -4
- package/src/extension/team-tool/context.ts +2 -0
- package/src/extension/team-tool/handle-settings.ts +188 -188
- package/src/extension/team-tool/inspect.ts +41 -41
- package/src/extension/team-tool/lifecycle-actions.ts +79 -79
- package/src/extension/team-tool/plan.ts +19 -19
- package/src/extension/team-tool/respond.ts +84 -0
- package/src/extension/team-tool/run.ts +3 -2
- package/src/extension/team-tool/status.ts +7 -1
- package/src/extension/team-tool-types.ts +4 -0
- package/src/extension/team-tool.ts +2 -0
- package/src/i18n.ts +184 -184
- package/src/observability/event-to-metric.ts +6 -0
- package/src/observability/exporters/otlp-exporter.ts +77 -77
- package/src/prompt/prompt-runtime.ts +72 -72
- package/src/runtime/agent-control.ts +63 -63
- package/src/runtime/agent-memory.ts +72 -72
- package/src/runtime/agent-observability.ts +114 -114
- package/src/runtime/async-marker.ts +26 -26
- package/src/runtime/attention-events.ts +28 -28
- package/src/runtime/background-runner.ts +53 -53
- package/src/runtime/child-pi.ts +444 -444
- package/src/runtime/completion-guard.ts +87 -0
- package/src/runtime/crash-recovery.ts +30 -0
- package/src/runtime/crew-agent-records.ts +8 -0
- package/src/runtime/crew-agent-runtime.ts +2 -1
- package/src/runtime/delivery-coordinator.ts +154 -0
- package/src/runtime/direct-run.ts +35 -35
- package/src/runtime/foreground-control.ts +82 -82
- package/src/runtime/green-contract.ts +46 -46
- package/src/runtime/group-join.ts +106 -106
- package/src/runtime/heartbeat-gradient.ts +28 -28
- package/src/runtime/heartbeat-watcher.ts +124 -124
- package/src/runtime/live-agent-control.ts +87 -87
- package/src/runtime/live-agent-manager.ts +85 -85
- package/src/runtime/live-control-realtime.ts +36 -36
- package/src/runtime/live-session-runtime.ts +305 -305
- package/src/runtime/model-fallback.ts +5 -2
- package/src/runtime/overflow-recovery.ts +176 -0
- package/src/runtime/parallel-research.ts +44 -44
- package/src/runtime/pi-json-output.ts +111 -111
- package/src/runtime/policy-engine.ts +79 -79
- package/src/runtime/process-status.ts +1 -1
- package/src/runtime/progress-event-coalescer.ts +43 -43
- package/src/runtime/recovery-recipes.ts +74 -74
- package/src/runtime/retry-executor.ts +64 -64
- package/src/runtime/role-permission.ts +39 -39
- package/src/runtime/session-resources.ts +25 -0
- package/src/runtime/session-snapshot.ts +59 -0
- package/src/runtime/session-usage.ts +79 -79
- package/src/runtime/sidechain-output.ts +29 -29
- package/src/runtime/stale-reconciler.ts +199 -0
- package/src/runtime/supervisor-contact.ts +59 -0
- package/src/runtime/task-display.ts +38 -38
- package/src/runtime/task-output-context.ts +127 -127
- package/src/runtime/task-runner/live-executor.ts +101 -101
- package/src/runtime/task-runner/progress.ts +119 -119
- package/src/runtime/task-runner/result-utils.ts +14 -14
- package/src/runtime/task-runner/state-helpers.ts +22 -22
- package/src/runtime/task-runner.ts +14 -0
- package/src/runtime/team-runner.ts +19 -8
- package/src/runtime/worker-heartbeat.ts +21 -21
- package/src/runtime/worker-startup.ts +57 -57
- package/src/schema/config-schema.ts +1 -0
- package/src/schema/team-tool-schema.ts +6 -1
- package/src/state/contracts.ts +6 -2
- package/src/state/state-store.ts +43 -0
- package/src/state/task-claims.ts +44 -44
- package/src/state/types.ts +2 -0
- package/src/state/usage.ts +29 -29
- package/src/subagents/async-entry.ts +1 -1
- package/src/subagents/index.ts +3 -3
- package/src/subagents/live/control.ts +1 -1
- package/src/subagents/live/manager.ts +1 -1
- package/src/subagents/live/realtime.ts +1 -1
- package/src/subagents/live/session-runtime.ts +1 -1
- package/src/subagents/manager.ts +1 -1
- package/src/subagents/spawn.ts +1 -1
- package/src/teams/team-serializer.ts +38 -38
- package/src/types/diff.d.ts +18 -18
- package/src/ui/crew-footer.ts +101 -101
- package/src/ui/crew-select-list.ts +111 -111
- package/src/ui/crew-widget.ts +10 -5
- package/src/ui/dashboard-panes/mailbox-pane.ts +2 -1
- package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
- package/src/ui/dynamic-border.ts +25 -25
- package/src/ui/layout-primitives.ts +106 -106
- package/src/ui/loaders.ts +158 -158
- package/src/ui/powerbar-publisher.ts +4 -4
- package/src/ui/render-diff.ts +119 -119
- package/src/ui/render-scheduler.ts +143 -143
- package/src/ui/run-snapshot-cache.ts +310 -17
- package/src/ui/snapshot-types.ts +5 -0
- package/src/ui/spinner.ts +17 -17
- package/src/ui/status-colors.ts +58 -54
- package/src/ui/syntax-highlight.ts +116 -116
- package/src/utils/atomic-write.ts +33 -0
- package/src/utils/completion-dedupe.ts +63 -63
- package/src/utils/frontmatter.ts +68 -68
- package/src/utils/git.ts +262 -262
- package/src/utils/ids.ts +12 -12
- package/src/utils/names.ts +27 -27
- package/src/utils/redaction.ts +44 -44
- package/src/utils/safe-paths.ts +47 -47
- package/src/utils/sleep.ts +32 -32
- package/src/workflows/validate-workflow.ts +40 -40
- package/src/worktree/branch-freshness.ts +45 -45
- package/teams/default.team.md +12 -12
- package/teams/fast-fix.team.md +11 -11
- package/teams/implementation.team.md +18 -18
- package/teams/parallel-research.team.md +14 -14
- package/teams/research.team.md +11 -11
- package/teams/review.team.md +12 -12
- package/workflows/default.workflow.md +29 -29
- package/workflows/fast-fix.workflow.md +22 -22
- package/workflows/implementation.workflow.md +38 -38
- package/workflows/parallel-research.workflow.md +46 -46
- package/workflows/research.workflow.md +22 -22
- package/workflows/review.workflow.md +30 -30
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import { readCrewAgents, agentsPath, agentOutputPath } from "../runtime/crew-agent-records.ts";
|
|
4
|
+
import { readCrewAgents, readCrewAgentsAsync, agentsPath, agentOutputPath } from "../runtime/crew-agent-records.ts";
|
|
5
5
|
import type { CrewAgentRecord } from "../runtime/crew-agent-runtime.ts";
|
|
6
6
|
import { isActiveRunStatus } from "../runtime/process-status.ts";
|
|
7
7
|
import type { TeamEvent } from "../state/event-log.ts";
|
|
8
8
|
import type { MailboxMessageStatus } from "../state/mailbox.ts";
|
|
9
|
-
import { loadRunManifestById } from "../state/state-store.ts";
|
|
9
|
+
import { loadRunManifestById, loadRunManifestByIdAsync } from "../state/state-store.ts";
|
|
10
10
|
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
11
|
-
import type { RunSnapshotCache, RunUiGroupJoin, RunUiMailbox, RunUiProgress, RunUiSnapshot, RunUiUsage } from "./snapshot-types.ts";
|
|
11
|
+
import type { RunSnapshotCache as RunSnapshotCacheBase, RunUiGroupJoin, RunUiMailbox, RunUiProgress, RunUiSnapshot, RunUiUsage } from "./snapshot-types.ts";
|
|
12
|
+
|
|
13
|
+
export interface RunSnapshotCache extends RunSnapshotCacheBase {
|
|
14
|
+
preloadStale(runId: string): Promise<RunUiSnapshot | undefined>;
|
|
15
|
+
preloadAllStale(runIds: string[]): Promise<void>;
|
|
16
|
+
}
|
|
12
17
|
|
|
13
18
|
const DEFAULT_TTL_MS = 500;
|
|
14
19
|
const DEFAULT_MAX_ENTRIES = 24;
|
|
@@ -60,6 +65,16 @@ function stampFile(filePath: string | undefined): FileStamp {
|
|
|
60
65
|
}
|
|
61
66
|
}
|
|
62
67
|
|
|
68
|
+
async function stampFileAsync(filePath: string | undefined): Promise<FileStamp> {
|
|
69
|
+
if (!filePath) return zeroStamp();
|
|
70
|
+
try {
|
|
71
|
+
const stat = await fs.promises.stat(filePath);
|
|
72
|
+
return { mtimeMs: stat.mtimeMs, size: stat.size };
|
|
73
|
+
} catch {
|
|
74
|
+
return zeroStamp();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
63
78
|
function combineStamps(stamps: FileStamp[]): FileStamp {
|
|
64
79
|
return stamps.reduce((acc, stamp) => ({ mtimeMs: Math.max(acc.mtimeMs, stamp.mtimeMs), size: acc.size + stamp.size }), zeroStamp());
|
|
65
80
|
}
|
|
@@ -84,6 +99,26 @@ function mailboxStamp(manifest: TeamRunManifest): FileStamp {
|
|
|
84
99
|
return combineStamps(stamps);
|
|
85
100
|
}
|
|
86
101
|
|
|
102
|
+
async function mailboxStampAsync(manifest: TeamRunManifest): Promise<FileStamp> {
|
|
103
|
+
const root = path.join(manifest.stateRoot, "mailbox");
|
|
104
|
+
const stamps: FileStamp[] = [
|
|
105
|
+
await stampFileAsync(path.join(root, "inbox.jsonl")),
|
|
106
|
+
await stampFileAsync(path.join(root, "outbox.jsonl")),
|
|
107
|
+
await stampFileAsync(path.join(root, "delivery.json")),
|
|
108
|
+
];
|
|
109
|
+
const tasksRoot = path.join(root, "tasks");
|
|
110
|
+
try {
|
|
111
|
+
for (const entry of await fs.promises.readdir(tasksRoot, { withFileTypes: true })) {
|
|
112
|
+
if (!entry.isDirectory()) continue;
|
|
113
|
+
stamps.push(await stampFileAsync(path.join(tasksRoot, entry.name, "inbox.jsonl")));
|
|
114
|
+
stamps.push(await stampFileAsync(path.join(tasksRoot, entry.name, "outbox.jsonl")));
|
|
115
|
+
}
|
|
116
|
+
} catch {
|
|
117
|
+
// No task mailbox yet.
|
|
118
|
+
}
|
|
119
|
+
return combineStamps(stamps);
|
|
120
|
+
}
|
|
121
|
+
|
|
87
122
|
function safeAgentOutputPath(manifest: TeamRunManifest, agent: CrewAgentRecord): string | undefined {
|
|
88
123
|
try {
|
|
89
124
|
return agentOutputPath(manifest, agent.taskId);
|
|
@@ -96,6 +131,10 @@ function outputStamp(manifest: TeamRunManifest, agents: CrewAgentRecord[]): File
|
|
|
96
131
|
return combineStamps(agents.map((agent) => stampFile(safeAgentOutputPath(manifest, agent))));
|
|
97
132
|
}
|
|
98
133
|
|
|
134
|
+
async function outputStampAsync(manifest: TeamRunManifest, agents: CrewAgentRecord[]): Promise<FileStamp> {
|
|
135
|
+
return combineStamps(await Promise.all(agents.map((agent) => stampFileAsync(safeAgentOutputPath(manifest, agent)))));
|
|
136
|
+
}
|
|
137
|
+
|
|
99
138
|
function sameStamp(a: FileStamp, b: FileStamp): boolean {
|
|
100
139
|
return a.mtimeMs === b.mtimeMs && a.size === b.size;
|
|
101
140
|
}
|
|
@@ -141,6 +180,29 @@ function tailJsonlLines<T>(filePath: string, limit: number, parse: (line: string
|
|
|
141
180
|
}
|
|
142
181
|
}
|
|
143
182
|
|
|
183
|
+
/** Async tail-read JSONL lines from a file, returning parsed objects (limited). */
|
|
184
|
+
async function tailJsonlLinesAsync<T>(filePath: string, limit: number, parse: (line: string) => T | undefined): Promise<T[]> {
|
|
185
|
+
if (limit <= 0) return [];
|
|
186
|
+
try {
|
|
187
|
+
const stat = await fs.promises.stat(filePath);
|
|
188
|
+
const bytesToRead = Math.min(stat.size, MAX_TAIL_BYTES);
|
|
189
|
+
const handle = await fs.promises.open(filePath, "r");
|
|
190
|
+
try {
|
|
191
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
192
|
+
await handle.read(buffer, 0, bytesToRead, stat.size - bytesToRead);
|
|
193
|
+
const lines = buffer.toString("utf-8").split(/\r?\n/).filter(Boolean);
|
|
194
|
+
return lines.flatMap((line) => {
|
|
195
|
+
const item = parse(line);
|
|
196
|
+
return item ? [item] : [];
|
|
197
|
+
}).slice(-limit);
|
|
198
|
+
} finally {
|
|
199
|
+
await handle.close();
|
|
200
|
+
}
|
|
201
|
+
} catch {
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
144
206
|
function safeRecentEvents(eventsPath: string, limit: number): TeamEvent[] {
|
|
145
207
|
return tailJsonlLines(eventsPath, limit, (line) => {
|
|
146
208
|
try {
|
|
@@ -152,6 +214,17 @@ function safeRecentEvents(eventsPath: string, limit: number): TeamEvent[] {
|
|
|
152
214
|
});
|
|
153
215
|
}
|
|
154
216
|
|
|
217
|
+
async function safeRecentEventsAsync(eventsPath: string, limit: number): Promise<TeamEvent[]> {
|
|
218
|
+
return tailJsonlLinesAsync(eventsPath, limit, (line) => {
|
|
219
|
+
try {
|
|
220
|
+
const parsed = JSON.parse(line) as unknown;
|
|
221
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as TeamEvent) : undefined;
|
|
222
|
+
} catch {
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
155
228
|
function tailLines(filePath: string, limit: number): string[] {
|
|
156
229
|
if (limit <= 0) return [];
|
|
157
230
|
try {
|
|
@@ -170,6 +243,24 @@ function tailLines(filePath: string, limit: number): string[] {
|
|
|
170
243
|
}
|
|
171
244
|
}
|
|
172
245
|
|
|
246
|
+
async function tailLinesAsync(filePath: string, limit: number): Promise<string[]> {
|
|
247
|
+
if (limit <= 0) return [];
|
|
248
|
+
try {
|
|
249
|
+
const stat = await fs.promises.stat(filePath);
|
|
250
|
+
const bytesToRead = Math.min(stat.size, MAX_TAIL_BYTES);
|
|
251
|
+
const handle = await fs.promises.open(filePath, "r");
|
|
252
|
+
try {
|
|
253
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
254
|
+
await handle.read(buffer, 0, bytesToRead, stat.size - bytesToRead);
|
|
255
|
+
return buffer.toString("utf-8").split(/\r?\n/).filter(Boolean).slice(-limit);
|
|
256
|
+
} finally {
|
|
257
|
+
await handle.close();
|
|
258
|
+
}
|
|
259
|
+
} catch {
|
|
260
|
+
return [];
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
173
264
|
function recentOutputLines(manifest: TeamRunManifest, agents: CrewAgentRecord[], limit: number): string[] {
|
|
174
265
|
const fromProgress = agents.flatMap((agent) => agent.progress?.recentOutput ?? []);
|
|
175
266
|
const fromFiles = agents.flatMap((agent) => {
|
|
@@ -179,14 +270,28 @@ function recentOutputLines(manifest: TeamRunManifest, agents: CrewAgentRecord[],
|
|
|
179
270
|
return [...fromProgress, ...fromFiles].map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean).slice(-limit);
|
|
180
271
|
}
|
|
181
272
|
|
|
273
|
+
async function recentOutputLinesAsync(manifest: TeamRunManifest, agents: CrewAgentRecord[], limit: number): Promise<string[]> {
|
|
274
|
+
const fromProgress = agents.flatMap((agent) => agent.progress?.recentOutput ?? []);
|
|
275
|
+
const fromFilesArrays = await Promise.all(agents.map((agent) => {
|
|
276
|
+
const outputPath = safeAgentOutputPath(manifest, agent);
|
|
277
|
+
return outputPath ? tailLinesAsync(outputPath, limit) : Promise.resolve([]);
|
|
278
|
+
}));
|
|
279
|
+
const fromFiles = fromFilesArrays.flat();
|
|
280
|
+
return [...fromProgress, ...fromFiles].map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean).slice(-limit);
|
|
281
|
+
}
|
|
282
|
+
|
|
182
283
|
function progressFromTasks(tasks: TeamTaskState[]): RunUiProgress {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
284
|
+
const progress: RunUiProgress = { total: tasks.length, completed: 0, running: 0, failed: 0, queued: 0, waiting: 0, cancelled: 0, skipped: 0 };
|
|
285
|
+
for (const task of tasks) {
|
|
286
|
+
if (task.status === "completed") progress.completed += 1;
|
|
287
|
+
else if (task.status === "running") progress.running += 1;
|
|
288
|
+
else if (task.status === "failed") progress.failed += 1;
|
|
289
|
+
else if (task.status === "queued") progress.queued += 1;
|
|
290
|
+
else if (task.status === "waiting") progress.waiting = (progress.waiting ?? 0) + 1;
|
|
291
|
+
else if (task.status === "cancelled") progress.cancelled = (progress.cancelled ?? 0) + 1;
|
|
292
|
+
else if (task.status === "skipped") progress.skipped = (progress.skipped ?? 0) + 1;
|
|
293
|
+
}
|
|
294
|
+
return progress;
|
|
190
295
|
}
|
|
191
296
|
|
|
192
297
|
function usageFrom(tasks: TeamTaskState[], agents: CrewAgentRecord[]): RunUiUsage {
|
|
@@ -223,6 +328,21 @@ function readDeliveryMessages(filePath: string): Record<string, MailboxMessageSt
|
|
|
223
328
|
}
|
|
224
329
|
}
|
|
225
330
|
|
|
331
|
+
async function readDeliveryMessagesAsync(filePath: string): Promise<Record<string, MailboxMessageStatus>> {
|
|
332
|
+
try {
|
|
333
|
+
const content = await fs.promises.readFile(filePath, "utf-8");
|
|
334
|
+
const parsed = JSON.parse(content) as unknown;
|
|
335
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
336
|
+
const messages = (parsed as { messages?: unknown }).messages;
|
|
337
|
+
if (!messages || typeof messages !== "object" || Array.isArray(messages)) return {};
|
|
338
|
+
const output: Record<string, MailboxMessageStatus> = {};
|
|
339
|
+
for (const [id, status] of Object.entries(messages)) if (isMailboxStatus(status)) output[id] = status;
|
|
340
|
+
return output;
|
|
341
|
+
} catch {
|
|
342
|
+
return {};
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
226
346
|
function readGroupJoinMailbox(filePath: string, delivery: Record<string, MailboxMessageStatus>): RunUiGroupJoin[] {
|
|
227
347
|
return tailJsonlLines(filePath, MAX_TAIL_LINES, (line) => {
|
|
228
348
|
try {
|
|
@@ -238,7 +358,43 @@ function readGroupJoinMailbox(filePath: string, delivery: Record<string, Mailbox
|
|
|
238
358
|
});
|
|
239
359
|
}
|
|
240
360
|
|
|
241
|
-
function
|
|
361
|
+
async function readGroupJoinMailboxAsync(filePath: string, delivery: Record<string, MailboxMessageStatus>): Promise<RunUiGroupJoin[]> {
|
|
362
|
+
return tailJsonlLinesAsync(filePath, MAX_TAIL_LINES, (line) => {
|
|
363
|
+
try {
|
|
364
|
+
const parsed = JSON.parse(line) as unknown;
|
|
365
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
|
|
366
|
+
const message = parsed as { id?: unknown; data?: unknown };
|
|
367
|
+
const data = message.data && typeof message.data === "object" && !Array.isArray(message.data) ? message.data as Record<string, unknown> : undefined;
|
|
368
|
+
if (typeof message.id !== "string" || data?.kind !== "group_join" || typeof data.requestId !== "string") return undefined;
|
|
369
|
+
return { requestId: data.requestId, messageId: message.id, partial: data.partial === true, ack: delivery[message.id] === "acknowledged" ? "acknowledged" as const : "pending" as const };
|
|
370
|
+
} catch {
|
|
371
|
+
return undefined;
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
interface MailboxCount {
|
|
377
|
+
count: number;
|
|
378
|
+
approximate: boolean;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function tailApproximate(filePath: string): boolean {
|
|
382
|
+
try {
|
|
383
|
+
return fs.statSync(filePath).size > MAX_TAIL_BYTES;
|
|
384
|
+
} catch {
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async function tailApproximateAsync(filePath: string): Promise<boolean> {
|
|
390
|
+
try {
|
|
391
|
+
return (await fs.promises.stat(filePath)).size > MAX_TAIL_BYTES;
|
|
392
|
+
} catch {
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function readMailboxCounts(filePath: string, delivery: Record<string, MailboxMessageStatus>): MailboxCount {
|
|
242
398
|
const items = tailJsonlLines(filePath, MAX_TAIL_LINES, (line) => {
|
|
243
399
|
try {
|
|
244
400
|
const parsed = JSON.parse(line) as unknown;
|
|
@@ -250,7 +406,22 @@ function readMailboxCounts(filePath: string, delivery: Record<string, MailboxMes
|
|
|
250
406
|
return 0;
|
|
251
407
|
}
|
|
252
408
|
}) as number[];
|
|
253
|
-
return items.reduce((sum, val) => sum + val, 0);
|
|
409
|
+
return { count: items.reduce((sum, val) => sum + val, 0), approximate: tailApproximate(filePath) };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async function readMailboxCountsAsync(filePath: string, delivery: Record<string, MailboxMessageStatus>): Promise<MailboxCount> {
|
|
413
|
+
const items = await tailJsonlLinesAsync(filePath, MAX_TAIL_LINES, (line) => {
|
|
414
|
+
try {
|
|
415
|
+
const parsed = JSON.parse(line) as unknown;
|
|
416
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return 0;
|
|
417
|
+
const message = parsed as { id?: unknown; status?: unknown };
|
|
418
|
+
if (typeof message.id !== "string" || !isMailboxStatus(message.status)) return 0;
|
|
419
|
+
return message.status !== "acknowledged" && delivery[message.id] !== "acknowledged" ? 1 : 0;
|
|
420
|
+
} catch {
|
|
421
|
+
return 0;
|
|
422
|
+
}
|
|
423
|
+
}) as number[];
|
|
424
|
+
return { count: items.reduce((sum, val) => sum + val, 0), approximate: await tailApproximateAsync(filePath) };
|
|
254
425
|
}
|
|
255
426
|
|
|
256
427
|
function groupJoinsFrom(manifest: TeamRunManifest): RunUiGroupJoin[] {
|
|
@@ -259,23 +430,52 @@ function groupJoinsFrom(manifest: TeamRunManifest): RunUiGroupJoin[] {
|
|
|
259
430
|
return readGroupJoinMailbox(path.join(root, "outbox.jsonl"), delivery).slice(-5);
|
|
260
431
|
}
|
|
261
432
|
|
|
433
|
+
async function groupJoinsFromAsync(manifest: TeamRunManifest): Promise<RunUiGroupJoin[]> {
|
|
434
|
+
const root = path.join(manifest.stateRoot, "mailbox");
|
|
435
|
+
const delivery = await readDeliveryMessagesAsync(path.join(root, "delivery.json"));
|
|
436
|
+
return (await readGroupJoinMailboxAsync(path.join(root, "outbox.jsonl"), delivery)).slice(-5);
|
|
437
|
+
}
|
|
438
|
+
|
|
262
439
|
function mailboxFrom(manifest: TeamRunManifest, agents: CrewAgentRecord[]): RunUiMailbox {
|
|
263
440
|
const root = path.join(manifest.stateRoot, "mailbox");
|
|
264
441
|
const delivery = readDeliveryMessages(path.join(root, "delivery.json"));
|
|
265
|
-
let
|
|
266
|
-
let
|
|
442
|
+
let inbox = readMailboxCounts(path.join(root, "inbox.jsonl"), delivery);
|
|
443
|
+
let outbox = readMailboxCounts(path.join(root, "outbox.jsonl"), delivery);
|
|
267
444
|
const tasksRoot = path.join(root, "tasks");
|
|
268
445
|
try {
|
|
269
446
|
for (const entry of fs.readdirSync(tasksRoot, { withFileTypes: true })) {
|
|
270
447
|
if (!entry.isDirectory()) continue;
|
|
271
|
-
|
|
272
|
-
|
|
448
|
+
const taskInbox = readMailboxCounts(path.join(tasksRoot, entry.name, "inbox.jsonl"), delivery);
|
|
449
|
+
const taskOutbox = readMailboxCounts(path.join(tasksRoot, entry.name, "outbox.jsonl"), delivery);
|
|
450
|
+
inbox = { count: inbox.count + taskInbox.count, approximate: inbox.approximate || taskInbox.approximate };
|
|
451
|
+
outbox = { count: outbox.count + taskOutbox.count, approximate: outbox.approximate || taskOutbox.approximate };
|
|
273
452
|
}
|
|
274
453
|
} catch {
|
|
275
454
|
// No task mailboxes yet.
|
|
276
455
|
}
|
|
277
456
|
const attentionAgents = agents.filter((agent) => agent.progress?.activityState === "needs_attention").length;
|
|
278
|
-
return { inboxUnread, outboxPending, needsAttention:
|
|
457
|
+
return { inboxUnread: inbox.count, outboxPending: outbox.count, needsAttention: inbox.count + attentionAgents, approximate: inbox.approximate || outbox.approximate };
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async function mailboxFromAsync(manifest: TeamRunManifest, agents: CrewAgentRecord[]): Promise<RunUiMailbox> {
|
|
461
|
+
const root = path.join(manifest.stateRoot, "mailbox");
|
|
462
|
+
const delivery = await readDeliveryMessagesAsync(path.join(root, "delivery.json"));
|
|
463
|
+
let inbox = await readMailboxCountsAsync(path.join(root, "inbox.jsonl"), delivery);
|
|
464
|
+
let outbox = await readMailboxCountsAsync(path.join(root, "outbox.jsonl"), delivery);
|
|
465
|
+
const tasksRoot = path.join(root, "tasks");
|
|
466
|
+
try {
|
|
467
|
+
for (const entry of await fs.promises.readdir(tasksRoot, { withFileTypes: true })) {
|
|
468
|
+
if (!entry.isDirectory()) continue;
|
|
469
|
+
const taskInbox = await readMailboxCountsAsync(path.join(tasksRoot, entry.name, "inbox.jsonl"), delivery);
|
|
470
|
+
const taskOutbox = await readMailboxCountsAsync(path.join(tasksRoot, entry.name, "outbox.jsonl"), delivery);
|
|
471
|
+
inbox = { count: inbox.count + taskInbox.count, approximate: inbox.approximate || taskInbox.approximate };
|
|
472
|
+
outbox = { count: outbox.count + taskOutbox.count, approximate: outbox.approximate || taskOutbox.approximate };
|
|
473
|
+
}
|
|
474
|
+
} catch {
|
|
475
|
+
// No task mailboxes yet.
|
|
476
|
+
}
|
|
477
|
+
const attentionAgents = agents.filter((agent) => agent.progress?.activityState === "needs_attention").length;
|
|
478
|
+
return { inboxUnread: inbox.count, outboxPending: outbox.count, needsAttention: inbox.count + attentionAgents, approximate: inbox.approximate || outbox.approximate };
|
|
279
479
|
}
|
|
280
480
|
|
|
281
481
|
function signatureFor(input: Omit<RunUiSnapshot, "signature" | "fetchedAt">, stamps: SnapshotStamps): string {
|
|
@@ -311,6 +511,18 @@ function stampsFor(manifest: TeamRunManifest, agents: CrewAgentRecord[]): Snapsh
|
|
|
311
511
|
};
|
|
312
512
|
}
|
|
313
513
|
|
|
514
|
+
async function stampsForAsync(manifest: TeamRunManifest, agents: CrewAgentRecord[]): Promise<SnapshotStamps> {
|
|
515
|
+
const [manifestStamp, tasksStamp, agentsStamp, eventsStamp, mailbox, output] = await Promise.all([
|
|
516
|
+
stampFileAsync(path.join(manifest.stateRoot, "manifest.json")),
|
|
517
|
+
stampFileAsync(manifest.tasksPath),
|
|
518
|
+
stampFileAsync(agentsPath(manifest)),
|
|
519
|
+
stampFileAsync(manifest.eventsPath),
|
|
520
|
+
mailboxStampAsync(manifest),
|
|
521
|
+
outputStampAsync(manifest, agents),
|
|
522
|
+
]);
|
|
523
|
+
return { manifest: manifestStamp, tasks: tasksStamp, agents: agentsStamp, events: eventsStamp, mailbox, output };
|
|
524
|
+
}
|
|
525
|
+
|
|
314
526
|
export function createRunSnapshotCache(cwd: string, options: RunSnapshotCacheOptions = {}): RunSnapshotCache {
|
|
315
527
|
const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
316
528
|
const maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
@@ -377,6 +589,51 @@ export function createRunSnapshotCache(cwd: string, options: RunSnapshotCacheOpt
|
|
|
377
589
|
return { snapshot, stamps, loadedAtMs: snapshot.fetchedAt, lastAccessMs: snapshot.fetchedAt };
|
|
378
590
|
}
|
|
379
591
|
|
|
592
|
+
async function buildAsync(runId: string, previous?: CacheEntry): Promise<CacheEntry> {
|
|
593
|
+
let loaded: Awaited<ReturnType<typeof loadRunManifestByIdAsync>>;
|
|
594
|
+
try {
|
|
595
|
+
loaded = await loadRunManifestByIdAsync(cwd, runId);
|
|
596
|
+
} catch {
|
|
597
|
+
if (previous) return previous;
|
|
598
|
+
throw new Error(`Run '${runId}' could not be parsed.`);
|
|
599
|
+
}
|
|
600
|
+
if (!loaded) {
|
|
601
|
+
if (previous) return previous;
|
|
602
|
+
throw new Error(`Run '${runId}' not found.`);
|
|
603
|
+
}
|
|
604
|
+
let tasks: TeamTaskState[];
|
|
605
|
+
let agents: CrewAgentRecord[];
|
|
606
|
+
try {
|
|
607
|
+
tasks = loaded.tasks;
|
|
608
|
+
agents = await readCrewAgentsAsync(loaded.manifest);
|
|
609
|
+
} catch {
|
|
610
|
+
if (previous) return previous;
|
|
611
|
+
throw new Error(`Run '${runId}' could not be parsed.`);
|
|
612
|
+
}
|
|
613
|
+
const [mailbox, groupJoins, recentEvents, recentOutput] = await Promise.all([
|
|
614
|
+
mailboxFromAsync(loaded.manifest, agents),
|
|
615
|
+
groupJoinsFromAsync(loaded.manifest),
|
|
616
|
+
safeRecentEventsAsync(loaded.manifest.eventsPath, recentEventsLimit),
|
|
617
|
+
recentOutputLinesAsync(loaded.manifest, agents, recentOutputLimit),
|
|
618
|
+
]);
|
|
619
|
+
const base = {
|
|
620
|
+
runId: loaded.manifest.runId,
|
|
621
|
+
cwd: loaded.manifest.cwd,
|
|
622
|
+
manifest: loaded.manifest,
|
|
623
|
+
tasks,
|
|
624
|
+
agents,
|
|
625
|
+
progress: progressFromTasks(tasks),
|
|
626
|
+
usage: usageFrom(tasks, agents),
|
|
627
|
+
mailbox,
|
|
628
|
+
groupJoins,
|
|
629
|
+
recentEvents,
|
|
630
|
+
recentOutputLines: recentOutput,
|
|
631
|
+
};
|
|
632
|
+
const stamps = await stampsForAsync(loaded.manifest, agents);
|
|
633
|
+
const snapshot: RunUiSnapshot = { ...base, fetchedAt: Date.now(), signature: signatureFor(base, stamps) };
|
|
634
|
+
return { snapshot, stamps, loadedAtMs: snapshot.fetchedAt, lastAccessMs: snapshot.fetchedAt };
|
|
635
|
+
}
|
|
636
|
+
|
|
380
637
|
function currentStamps(previous: CacheEntry): SnapshotStamps {
|
|
381
638
|
const manifest = previous.snapshot.manifest;
|
|
382
639
|
return {
|
|
@@ -389,6 +646,40 @@ export function createRunSnapshotCache(cwd: string, options: RunSnapshotCacheOpt
|
|
|
389
646
|
};
|
|
390
647
|
}
|
|
391
648
|
|
|
649
|
+
async function currentStampsAsync(previous: CacheEntry): Promise<SnapshotStamps> {
|
|
650
|
+
return stampsForAsync(previous.snapshot.manifest, previous.snapshot.agents);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
async function preloadStale(runId: string): Promise<RunUiSnapshot | undefined> {
|
|
654
|
+
const previous = entries.get(runId);
|
|
655
|
+
const now = Date.now();
|
|
656
|
+
// Fresh enough? Return immediately
|
|
657
|
+
if (previous && now - previous.loadedAtMs < ttlMs) {
|
|
658
|
+
return touch(runId, previous);
|
|
659
|
+
}
|
|
660
|
+
// Check stamps async
|
|
661
|
+
if (previous) {
|
|
662
|
+
const stamps = await currentStampsAsync(previous);
|
|
663
|
+
if (sameStamps(stamps, previous.stamps)) {
|
|
664
|
+
previous.loadedAtMs = now;
|
|
665
|
+
return touch(runId, previous);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
// Full async build
|
|
669
|
+
const entry = await buildAsync(runId, previous);
|
|
670
|
+
entries.set(runId, entry);
|
|
671
|
+
evictIfNeeded();
|
|
672
|
+
return entry.snapshot;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
async function preloadAllStale(runIds: string[]): Promise<void> {
|
|
676
|
+
const batchSize = 4;
|
|
677
|
+
for (let i = 0; i < runIds.length; i += batchSize) {
|
|
678
|
+
const batch = runIds.slice(i, i + batchSize);
|
|
679
|
+
await Promise.all(batch.map((id) => preloadStale(id)));
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
392
683
|
return {
|
|
393
684
|
get(runId: string): RunUiSnapshot | undefined {
|
|
394
685
|
const entry = entries.get(runId);
|
|
@@ -410,6 +701,8 @@ export function createRunSnapshotCache(cwd: string, options: RunSnapshotCacheOpt
|
|
|
410
701
|
if (sameStamps(stamps, previous.stamps)) return touch(runId, previous);
|
|
411
702
|
return this.refresh(runId);
|
|
412
703
|
},
|
|
704
|
+
preloadStale,
|
|
705
|
+
preloadAllStale,
|
|
413
706
|
invalidate(runId?: string): void {
|
|
414
707
|
if (runId) entries.delete(runId);
|
|
415
708
|
else entries.clear();
|
package/src/ui/snapshot-types.ts
CHANGED
|
@@ -8,6 +8,9 @@ export interface RunUiProgress {
|
|
|
8
8
|
running: number;
|
|
9
9
|
failed: number;
|
|
10
10
|
queued: number;
|
|
11
|
+
waiting?: number;
|
|
12
|
+
cancelled?: number;
|
|
13
|
+
skipped?: number;
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
export interface RunUiUsage {
|
|
@@ -20,6 +23,8 @@ export interface RunUiMailbox {
|
|
|
20
23
|
inboxUnread: number;
|
|
21
24
|
outboxPending: number;
|
|
22
25
|
needsAttention: number;
|
|
26
|
+
/** True when counts come from bounded tail reads and older messages may be omitted. */
|
|
27
|
+
approximate?: boolean;
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
export interface RunUiGroupJoin {
|
package/src/ui/spinner.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
export const SUBAGENT_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const;
|
|
2
|
-
export const SUBAGENT_SPINNER_FRAME_MS = 160;
|
|
3
|
-
|
|
4
|
-
export function spinnerBucket(now = Date.now(), frameMs = SUBAGENT_SPINNER_FRAME_MS): number {
|
|
5
|
-
return Math.floor(now / Math.max(1, frameMs));
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
function hashKey(key: string): number {
|
|
9
|
-
let hash = 0;
|
|
10
|
-
for (let index = 0; index < key.length; index += 1) hash = (hash * 31 + key.charCodeAt(index)) >>> 0;
|
|
11
|
-
return hash;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function spinnerFrame(key = "", now = Date.now()): string {
|
|
15
|
-
const offset = key ? hashKey(key) % SUBAGENT_SPINNER_FRAMES.length : 0;
|
|
16
|
-
return SUBAGENT_SPINNER_FRAMES[(spinnerBucket(now) + offset) % SUBAGENT_SPINNER_FRAMES.length] ?? SUBAGENT_SPINNER_FRAMES[0];
|
|
17
|
-
}
|
|
1
|
+
export const SUBAGENT_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const;
|
|
2
|
+
export const SUBAGENT_SPINNER_FRAME_MS = 160;
|
|
3
|
+
|
|
4
|
+
export function spinnerBucket(now = Date.now(), frameMs = SUBAGENT_SPINNER_FRAME_MS): number {
|
|
5
|
+
return Math.floor(now / Math.max(1, frameMs));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function hashKey(key: string): number {
|
|
9
|
+
let hash = 0;
|
|
10
|
+
for (let index = 0; index < key.length; index += 1) hash = (hash * 31 + key.charCodeAt(index)) >>> 0;
|
|
11
|
+
return hash;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function spinnerFrame(key = "", now = Date.now()): string {
|
|
15
|
+
const offset = key ? hashKey(key) % SUBAGENT_SPINNER_FRAMES.length : 0;
|
|
16
|
+
return SUBAGENT_SPINNER_FRAMES[(spinnerBucket(now) + offset) % SUBAGENT_SPINNER_FRAMES.length] ?? SUBAGENT_SPINNER_FRAMES[0];
|
|
17
|
+
}
|
package/src/ui/status-colors.ts
CHANGED
|
@@ -1,54 +1,58 @@
|
|
|
1
|
-
import type { CrewTheme, CrewThemeColor } from "./theme-adapter.ts";
|
|
2
|
-
|
|
3
|
-
export type RunStatus = "queued" | "running" | "completed" | "failed" | "cancelled" | "stopped" | "blocked" | (string & {});
|
|
4
|
-
|
|
5
|
-
export function colorForStatus(status: RunStatus): CrewThemeColor {
|
|
6
|
-
switch (status) {
|
|
7
|
-
case "running":
|
|
8
|
-
return "accent";
|
|
9
|
-
case "
|
|
10
|
-
return "
|
|
11
|
-
case "
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
case "
|
|
15
|
-
|
|
16
|
-
case "
|
|
17
|
-
|
|
18
|
-
case "
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
case "
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
case "
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
case "
|
|
36
|
-
return
|
|
37
|
-
case "
|
|
38
|
-
return
|
|
39
|
-
case "
|
|
40
|
-
return "
|
|
41
|
-
|
|
42
|
-
return "
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return
|
|
54
|
-
}
|
|
1
|
+
import type { CrewTheme, CrewThemeColor } from "./theme-adapter.ts";
|
|
2
|
+
|
|
3
|
+
export type RunStatus = "queued" | "running" | "completed" | "failed" | "cancelled" | "stopped" | "blocked" | (string & {});
|
|
4
|
+
|
|
5
|
+
export function colorForStatus(status: RunStatus): CrewThemeColor {
|
|
6
|
+
switch (status) {
|
|
7
|
+
case "running":
|
|
8
|
+
return "accent";
|
|
9
|
+
case "waiting":
|
|
10
|
+
return "muted";
|
|
11
|
+
case "completed":
|
|
12
|
+
return "success";
|
|
13
|
+
case "failed":
|
|
14
|
+
case "stale":
|
|
15
|
+
return "error";
|
|
16
|
+
case "cancelled":
|
|
17
|
+
case "blocked":
|
|
18
|
+
case "stopped":
|
|
19
|
+
return "warning";
|
|
20
|
+
case "queued":
|
|
21
|
+
default:
|
|
22
|
+
return "dim";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function iconForStatus(status: RunStatus, options?: { runningGlyph?: string }): string {
|
|
27
|
+
const glyph = options?.runningGlyph ?? "▶";
|
|
28
|
+
switch (status) {
|
|
29
|
+
case "completed":
|
|
30
|
+
return "✓";
|
|
31
|
+
case "failed":
|
|
32
|
+
case "stale":
|
|
33
|
+
return "✗";
|
|
34
|
+
case "cancelled":
|
|
35
|
+
case "stopped":
|
|
36
|
+
return "■";
|
|
37
|
+
case "running":
|
|
38
|
+
return glyph;
|
|
39
|
+
case "waiting":
|
|
40
|
+
return "⏳";
|
|
41
|
+
case "queued":
|
|
42
|
+
return "◦";
|
|
43
|
+
case "blocked":
|
|
44
|
+
return "⏸";
|
|
45
|
+
default:
|
|
46
|
+
return "·";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function colorForActivity(activityState: string | undefined): CrewThemeColor {
|
|
51
|
+
if (activityState === "needs_attention") return "warning";
|
|
52
|
+
if (activityState === "stale") return "error";
|
|
53
|
+
return "dim";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function applyStatusColor(theme: CrewTheme, status: RunStatus, text: string): string {
|
|
57
|
+
return theme.fg(colorForStatus(status), text);
|
|
58
|
+
}
|