pi-crew 0.1.41 → 0.1.44

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.
Files changed (191) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +51 -0
  3. package/agents/analyst.md +11 -11
  4. package/agents/critic.md +11 -11
  5. package/agents/executor.md +11 -11
  6. package/agents/explorer.md +11 -11
  7. package/agents/planner.md +11 -11
  8. package/agents/reviewer.md +11 -11
  9. package/agents/security-reviewer.md +11 -11
  10. package/agents/test-engineer.md +11 -11
  11. package/agents/verifier.md +11 -11
  12. package/agents/writer.md +11 -11
  13. package/docs/refactor-tasks-phase3.md +394 -394
  14. package/docs/refactor-tasks-phase4.md +564 -564
  15. package/docs/refactor-tasks-phase5.md +402 -402
  16. package/docs/refactor-tasks-phase6.md +662 -662
  17. package/docs/research-extension-examples.md +297 -297
  18. package/docs/research-extension-system.md +324 -324
  19. package/docs/research-optimization-plan.md +548 -548
  20. package/docs/research-phase10-distillation.md +199 -0
  21. package/docs/research-phase11-distillation.md +201 -0
  22. package/docs/research-pi-coding-agent.md +357 -357
  23. package/docs/research-source-pi-crew-reference.md +174 -174
  24. package/docs/runtime-flow.md +148 -148
  25. package/docs/source-runtime-refactor-map.md +83 -83
  26. package/index.ts +6 -6
  27. package/package.json +1 -1
  28. package/src/agents/agent-serializer.ts +34 -34
  29. package/src/agents/discover-agents.ts +5 -4
  30. package/src/config/config.ts +28 -4
  31. package/src/extension/cross-extension-rpc.ts +82 -82
  32. package/src/extension/management.ts +37 -8
  33. package/src/extension/notification-router.ts +2 -2
  34. package/src/extension/register.ts +130 -8
  35. package/src/extension/registration/commands.ts +11 -9
  36. package/src/extension/registration/compaction-guard.ts +125 -125
  37. package/src/extension/registration/subagent-tools.ts +28 -19
  38. package/src/extension/registration/team-tool.ts +2 -1
  39. package/src/extension/result-watcher.ts +4 -4
  40. package/src/extension/run-bundle-schema.ts +8 -4
  41. package/src/extension/run-import.ts +4 -0
  42. package/src/extension/run-index.ts +23 -1
  43. package/src/extension/run-maintenance.ts +43 -24
  44. package/src/extension/team-tool/api.ts +2 -2
  45. package/src/extension/team-tool/cancel.ts +76 -4
  46. package/src/extension/team-tool/context.ts +1 -0
  47. package/src/extension/team-tool/doctor.ts +8 -1
  48. package/src/extension/team-tool/handle-settings.ts +188 -0
  49. package/src/extension/team-tool/inspect.ts +41 -41
  50. package/src/extension/team-tool/lifecycle-actions.ts +79 -79
  51. package/src/extension/team-tool/plan.ts +19 -19
  52. package/src/extension/team-tool/respond.ts +67 -0
  53. package/src/extension/team-tool/run.ts +6 -4
  54. package/src/extension/team-tool/status.ts +99 -93
  55. package/src/extension/team-tool-types.ts +4 -0
  56. package/src/extension/team-tool.ts +5 -1
  57. package/src/i18n.ts +184 -0
  58. package/src/observability/correlation.ts +2 -2
  59. package/src/observability/event-to-metric.ts +10 -3
  60. package/src/observability/exporters/adapter.ts +7 -1
  61. package/src/observability/exporters/otlp-exporter.ts +14 -2
  62. package/src/observability/exporters/prometheus-exporter.ts +9 -2
  63. package/src/observability/metric-registry.ts +18 -3
  64. package/src/observability/metric-retention.ts +11 -3
  65. package/src/observability/metric-sink.ts +9 -4
  66. package/src/observability/metrics-primitives.ts +4 -3
  67. package/src/prompt/prompt-runtime.ts +72 -68
  68. package/src/runtime/agent-control.ts +63 -63
  69. package/src/runtime/agent-memory.ts +72 -72
  70. package/src/runtime/agent-observability.ts +114 -114
  71. package/src/runtime/async-marker.ts +26 -26
  72. package/src/runtime/attention-events.ts +28 -23
  73. package/src/runtime/background-runner.ts +53 -53
  74. package/src/runtime/child-pi.ts +4 -4
  75. package/src/runtime/completion-guard.ts +95 -4
  76. package/src/runtime/concurrency.ts +1 -1
  77. package/src/runtime/crash-recovery.ts +32 -1
  78. package/src/runtime/crew-agent-runtime.ts +59 -58
  79. package/src/runtime/deadletter.ts +14 -4
  80. package/src/runtime/delivery-coordinator.ts +143 -0
  81. package/src/runtime/direct-run.ts +35 -35
  82. package/src/runtime/foreground-control.ts +82 -82
  83. package/src/runtime/green-contract.ts +46 -46
  84. package/src/runtime/group-join.ts +106 -106
  85. package/src/runtime/heartbeat-gradient.ts +28 -28
  86. package/src/runtime/heartbeat-watcher.ts +48 -4
  87. package/src/runtime/live-agent-control.ts +87 -87
  88. package/src/runtime/live-agent-manager.ts +85 -85
  89. package/src/runtime/live-control-realtime.ts +36 -36
  90. package/src/runtime/live-session-runtime.ts +305 -305
  91. package/src/runtime/manifest-cache.ts +2 -2
  92. package/src/runtime/model-fallback.ts +272 -261
  93. package/src/runtime/overflow-recovery.ts +157 -0
  94. package/src/runtime/parallel-research.ts +44 -44
  95. package/src/runtime/parallel-utils.ts +1 -1
  96. package/src/runtime/pi-json-output.ts +111 -111
  97. package/src/runtime/policy-engine.ts +79 -78
  98. package/src/runtime/post-exit-stdio-guard.ts +2 -2
  99. package/src/runtime/process-status.ts +56 -56
  100. package/src/runtime/progress-event-coalescer.ts +43 -43
  101. package/src/runtime/recovery-recipes.ts +74 -74
  102. package/src/runtime/retry-executor.ts +5 -0
  103. package/src/runtime/role-permission.ts +39 -39
  104. package/src/runtime/runtime-resolver.ts +1 -1
  105. package/src/runtime/session-resources.ts +25 -0
  106. package/src/runtime/session-snapshot.ts +59 -0
  107. package/src/runtime/session-usage.ts +79 -79
  108. package/src/runtime/sidechain-output.ts +29 -29
  109. package/src/runtime/stale-reconciler.ts +179 -0
  110. package/src/runtime/subagent-manager.ts +3 -3
  111. package/src/runtime/supervisor-contact.ts +59 -0
  112. package/src/runtime/task-display.ts +38 -38
  113. package/src/runtime/task-output-context.ts +127 -127
  114. package/src/runtime/task-runner/live-executor.ts +101 -101
  115. package/src/runtime/task-runner/progress.ts +119 -111
  116. package/src/runtime/task-runner/result-utils.ts +14 -14
  117. package/src/runtime/task-runner/state-helpers.ts +22 -22
  118. package/src/runtime/task-runner.ts +14 -0
  119. package/src/runtime/team-runner.ts +9 -10
  120. package/src/runtime/worker-heartbeat.ts +21 -21
  121. package/src/runtime/worker-startup.ts +57 -57
  122. package/src/schema/config-schema.ts +2 -1
  123. package/src/schema/team-tool-schema.ts +115 -109
  124. package/src/state/artifact-store.ts +4 -2
  125. package/src/state/atomic-write.ts +12 -4
  126. package/src/state/contracts.ts +109 -105
  127. package/src/state/event-log.ts +3 -4
  128. package/src/state/jsonl-writer.ts +4 -1
  129. package/src/state/locks.ts +9 -1
  130. package/src/state/task-claims.ts +44 -42
  131. package/src/state/usage.ts +29 -29
  132. package/src/subagents/async-entry.ts +1 -1
  133. package/src/subagents/index.ts +3 -3
  134. package/src/subagents/live/control.ts +1 -1
  135. package/src/subagents/live/manager.ts +1 -1
  136. package/src/subagents/live/realtime.ts +1 -1
  137. package/src/subagents/live/session-runtime.ts +1 -1
  138. package/src/subagents/manager.ts +1 -1
  139. package/src/subagents/spawn.ts +1 -1
  140. package/src/teams/discover-teams.ts +2 -2
  141. package/src/teams/team-serializer.ts +38 -38
  142. package/src/types/diff.d.ts +18 -18
  143. package/src/ui/crew-footer.ts +101 -101
  144. package/src/ui/crew-select-list.ts +111 -111
  145. package/src/ui/crew-widget.ts +5 -4
  146. package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
  147. package/src/ui/dynamic-border.ts +25 -25
  148. package/src/ui/layout-primitives.ts +106 -106
  149. package/src/ui/live-run-sidebar.ts +1 -1
  150. package/src/ui/loaders.ts +158 -158
  151. package/src/ui/mascot.ts +3 -2
  152. package/src/ui/powerbar-publisher.ts +7 -6
  153. package/src/ui/render-diff.ts +119 -119
  154. package/src/ui/render-scheduler.ts +54 -14
  155. package/src/ui/run-dashboard.ts +39 -11
  156. package/src/ui/run-snapshot-cache.ts +336 -36
  157. package/src/ui/spinner.ts +17 -17
  158. package/src/ui/status-colors.ts +58 -54
  159. package/src/ui/syntax-highlight.ts +116 -116
  160. package/src/ui/theme-adapter.ts +1 -1
  161. package/src/ui/transcript-viewer.ts +7 -2
  162. package/src/utils/atomic-write.ts +33 -0
  163. package/src/utils/completion-dedupe.ts +63 -63
  164. package/src/utils/file-coalescer.ts +5 -3
  165. package/src/utils/frontmatter.ts +68 -36
  166. package/src/utils/git.ts +262 -262
  167. package/src/utils/ids.ts +12 -12
  168. package/src/utils/internal-error.ts +1 -1
  169. package/src/utils/names.ts +27 -26
  170. package/src/utils/paths.ts +1 -1
  171. package/src/utils/redaction.ts +44 -41
  172. package/src/utils/safe-paths.ts +47 -34
  173. package/src/utils/sleep.ts +2 -2
  174. package/src/utils/timings.ts +2 -0
  175. package/src/utils/visual.ts +9 -1
  176. package/src/workflows/discover-workflows.ts +4 -1
  177. package/src/workflows/validate-workflow.ts +40 -40
  178. package/src/worktree/branch-freshness.ts +45 -45
  179. package/src/worktree/worktree-manager.ts +6 -1
  180. package/teams/default.team.md +12 -12
  181. package/teams/fast-fix.team.md +11 -11
  182. package/teams/implementation.team.md +18 -18
  183. package/teams/parallel-research.team.md +14 -14
  184. package/teams/research.team.md +11 -11
  185. package/teams/review.team.md +12 -12
  186. package/workflows/default.workflow.md +29 -29
  187. package/workflows/fast-fix.workflow.md +22 -22
  188. package/workflows/implementation.workflow.md +38 -38
  189. package/workflows/parallel-research.workflow.md +46 -46
  190. package/workflows/research.workflow.md +22 -22
  191. package/workflows/review.workflow.md +30 -30
@@ -4,17 +4,24 @@ import * as path from "node:path";
4
4
  import { readCrewAgents, 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
- import { readEvents, type TeamEvent } from "../state/event-log.ts";
7
+ import type { TeamEvent } from "../state/event-log.ts";
8
8
  import type { MailboxMessageStatus } from "../state/mailbox.ts";
9
9
  import { loadRunManifestById } 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
12
 
13
- const DEFAULT_TTL_MS = 250;
13
+ export interface RunSnapshotCache extends RunSnapshotCacheBase {
14
+ preloadStale(runId: string): Promise<RunUiSnapshot | undefined>;
15
+ preloadAllStale(runIds: string[]): Promise<void>;
16
+ }
17
+
18
+ const DEFAULT_TTL_MS = 500;
14
19
  const DEFAULT_MAX_ENTRIES = 24;
15
20
  const DEFAULT_RECENT_EVENTS = 20;
16
21
  const DEFAULT_RECENT_OUTPUT_LINES = 20;
17
22
  const MAX_TAIL_BYTES = 32 * 1024;
23
+ /** Max JSONL lines to tail when reading growing files (events, mailbox). */
24
+ const MAX_TAIL_LINES = 500;
18
25
 
19
26
  interface FileStamp {
20
27
  mtimeMs: number;
@@ -58,6 +65,16 @@ function stampFile(filePath: string | undefined): FileStamp {
58
65
  }
59
66
  }
60
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
+
61
78
  function combineStamps(stamps: FileStamp[]): FileStamp {
62
79
  return stamps.reduce((acc, stamp) => ({ mtimeMs: Math.max(acc.mtimeMs, stamp.mtimeMs), size: acc.size + stamp.size }), zeroStamp());
63
80
  }
@@ -82,6 +99,26 @@ function mailboxStamp(manifest: TeamRunManifest): FileStamp {
82
99
  return combineStamps(stamps);
83
100
  }
84
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
+
85
122
  function safeAgentOutputPath(manifest: TeamRunManifest, agent: CrewAgentRecord): string | undefined {
86
123
  try {
87
124
  return agentOutputPath(manifest, agent.taskId);
@@ -94,6 +131,10 @@ function outputStamp(manifest: TeamRunManifest, agents: CrewAgentRecord[]): File
94
131
  return combineStamps(agents.map((agent) => stampFile(safeAgentOutputPath(manifest, agent))));
95
132
  }
96
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
+
97
138
  function sameStamp(a: FileStamp, b: FileStamp): boolean {
98
139
  return a.mtimeMs === b.mtimeMs && a.size === b.size;
99
140
  }
@@ -116,14 +157,84 @@ function readTasks(tasksPath: string): TeamTaskState[] {
116
157
  }
117
158
  }
118
159
 
119
- function safeRecentEvents(eventsPath: string, limit: number): TeamEvent[] {
160
+ async function readTasksAsync(tasksPath: string): Promise<TeamTaskState[]> {
161
+ try {
162
+ const content = await fs.promises.readFile(tasksPath, "utf-8");
163
+ const parsed = JSON.parse(content) as unknown;
164
+ return Array.isArray(parsed) ? (parsed as TeamTaskState[]) : [];
165
+ } catch {
166
+ throw new Error(`Failed to parse tasks at ${tasksPath}`);
167
+ }
168
+ }
169
+
170
+ /** Tail-read JSONL lines from a file, returning parsed objects (limited). */
171
+ function tailJsonlLines<T>(filePath: string, limit: number, parse: (line: string) => T | undefined): T[] {
172
+ if (limit <= 0) return [];
173
+ try {
174
+ const stat = fs.statSync(filePath);
175
+ const bytesToRead = Math.min(stat.size, MAX_TAIL_BYTES);
176
+ const fd = fs.openSync(filePath, "r");
177
+ try {
178
+ const buffer = Buffer.alloc(bytesToRead);
179
+ fs.readSync(fd, buffer, 0, bytesToRead, stat.size - bytesToRead);
180
+ const lines = buffer.toString("utf-8").split(/\r?\n/).filter(Boolean);
181
+ return lines.flatMap((line) => {
182
+ const item = parse(line);
183
+ return item ? [item] : [];
184
+ }).slice(-limit);
185
+ } finally {
186
+ fs.closeSync(fd);
187
+ }
188
+ } catch {
189
+ return [];
190
+ }
191
+ }
192
+
193
+ /** Async tail-read JSONL lines from a file, returning parsed objects (limited). */
194
+ async function tailJsonlLinesAsync<T>(filePath: string, limit: number, parse: (line: string) => T | undefined): Promise<T[]> {
195
+ if (limit <= 0) return [];
120
196
  try {
121
- return readEvents(eventsPath).slice(-limit);
197
+ const stat = await fs.promises.stat(filePath);
198
+ const bytesToRead = Math.min(stat.size, MAX_TAIL_BYTES);
199
+ const handle = await fs.promises.open(filePath, "r");
200
+ try {
201
+ const buffer = Buffer.alloc(bytesToRead);
202
+ await handle.read(buffer, 0, bytesToRead, stat.size - bytesToRead);
203
+ const lines = buffer.toString("utf-8").split(/\r?\n/).filter(Boolean);
204
+ return lines.flatMap((line) => {
205
+ const item = parse(line);
206
+ return item ? [item] : [];
207
+ }).slice(-limit);
208
+ } finally {
209
+ await handle.close();
210
+ }
122
211
  } catch {
123
212
  return [];
124
213
  }
125
214
  }
126
215
 
216
+ function safeRecentEvents(eventsPath: string, limit: number): TeamEvent[] {
217
+ return tailJsonlLines(eventsPath, limit, (line) => {
218
+ try {
219
+ const parsed = JSON.parse(line) as unknown;
220
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as TeamEvent) : undefined;
221
+ } catch {
222
+ return undefined;
223
+ }
224
+ });
225
+ }
226
+
227
+ async function safeRecentEventsAsync(eventsPath: string, limit: number): Promise<TeamEvent[]> {
228
+ return tailJsonlLinesAsync(eventsPath, limit, (line) => {
229
+ try {
230
+ const parsed = JSON.parse(line) as unknown;
231
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as TeamEvent) : undefined;
232
+ } catch {
233
+ return undefined;
234
+ }
235
+ });
236
+ }
237
+
127
238
  function tailLines(filePath: string, limit: number): string[] {
128
239
  if (limit <= 0) return [];
129
240
  try {
@@ -142,6 +253,24 @@ function tailLines(filePath: string, limit: number): string[] {
142
253
  }
143
254
  }
144
255
 
256
+ async function tailLinesAsync(filePath: string, limit: number): Promise<string[]> {
257
+ if (limit <= 0) return [];
258
+ try {
259
+ const stat = await fs.promises.stat(filePath);
260
+ const bytesToRead = Math.min(stat.size, MAX_TAIL_BYTES);
261
+ const handle = await fs.promises.open(filePath, "r");
262
+ try {
263
+ const buffer = Buffer.alloc(bytesToRead);
264
+ await handle.read(buffer, 0, bytesToRead, stat.size - bytesToRead);
265
+ return buffer.toString("utf-8").split(/\r?\n/).filter(Boolean).slice(-limit);
266
+ } finally {
267
+ await handle.close();
268
+ }
269
+ } catch {
270
+ return [];
271
+ }
272
+ }
273
+
145
274
  function recentOutputLines(manifest: TeamRunManifest, agents: CrewAgentRecord[], limit: number): string[] {
146
275
  const fromProgress = agents.flatMap((agent) => agent.progress?.recentOutput ?? []);
147
276
  const fromFiles = agents.flatMap((agent) => {
@@ -151,6 +280,16 @@ function recentOutputLines(manifest: TeamRunManifest, agents: CrewAgentRecord[],
151
280
  return [...fromProgress, ...fromFiles].map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean).slice(-limit);
152
281
  }
153
282
 
283
+ async function recentOutputLinesAsync(manifest: TeamRunManifest, agents: CrewAgentRecord[], limit: number): Promise<string[]> {
284
+ const fromProgress = agents.flatMap((agent) => agent.progress?.recentOutput ?? []);
285
+ const fromFilesArrays = await Promise.all(agents.map((agent) => {
286
+ const outputPath = safeAgentOutputPath(manifest, agent);
287
+ return outputPath ? tailLinesAsync(outputPath, limit) : Promise.resolve([]);
288
+ }));
289
+ const fromFiles = fromFilesArrays.flat();
290
+ return [...fromProgress, ...fromFiles].map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean).slice(-limit);
291
+ }
292
+
154
293
  function progressFromTasks(tasks: TeamTaskState[]): RunUiProgress {
155
294
  return {
156
295
  total: tasks.length,
@@ -195,41 +334,79 @@ function readDeliveryMessages(filePath: string): Record<string, MailboxMessageSt
195
334
  }
196
335
  }
197
336
 
198
- function readGroupJoinMailbox(filePath: string, delivery: Record<string, MailboxMessageStatus>): RunUiGroupJoin[] {
337
+ async function readDeliveryMessagesAsync(filePath: string): Promise<Record<string, MailboxMessageStatus>> {
199
338
  try {
200
- return fs.readFileSync(filePath, "utf-8").split(/\r?\n/).filter(Boolean).flatMap((line) => {
201
- try {
202
- const parsed = JSON.parse(line) as unknown;
203
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return [];
204
- const message = parsed as { id?: unknown; data?: unknown };
205
- const data = message.data && typeof message.data === "object" && !Array.isArray(message.data) ? message.data as Record<string, unknown> : undefined;
206
- if (typeof message.id !== "string" || data?.kind !== "group_join" || typeof data.requestId !== "string") return [];
207
- return [{ requestId: data.requestId, messageId: message.id, partial: data.partial === true, ack: delivery[message.id] === "acknowledged" ? "acknowledged" as const : "pending" as const }];
208
- } catch {
209
- return [];
210
- }
211
- });
339
+ const content = await fs.promises.readFile(filePath, "utf-8");
340
+ const parsed = JSON.parse(content) as unknown;
341
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
342
+ const messages = (parsed as { messages?: unknown }).messages;
343
+ if (!messages || typeof messages !== "object" || Array.isArray(messages)) return {};
344
+ const output: Record<string, MailboxMessageStatus> = {};
345
+ for (const [id, status] of Object.entries(messages)) if (isMailboxStatus(status)) output[id] = status;
346
+ return output;
212
347
  } catch {
213
- return [];
348
+ return {};
214
349
  }
215
350
  }
216
351
 
352
+ function readGroupJoinMailbox(filePath: string, delivery: Record<string, MailboxMessageStatus>): RunUiGroupJoin[] {
353
+ return tailJsonlLines(filePath, MAX_TAIL_LINES, (line) => {
354
+ try {
355
+ const parsed = JSON.parse(line) as unknown;
356
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
357
+ const message = parsed as { id?: unknown; data?: unknown };
358
+ const data = message.data && typeof message.data === "object" && !Array.isArray(message.data) ? message.data as Record<string, unknown> : undefined;
359
+ if (typeof message.id !== "string" || data?.kind !== "group_join" || typeof data.requestId !== "string") return undefined;
360
+ return { requestId: data.requestId, messageId: message.id, partial: data.partial === true, ack: delivery[message.id] === "acknowledged" ? "acknowledged" as const : "pending" as const };
361
+ } catch {
362
+ return undefined;
363
+ }
364
+ });
365
+ }
366
+
367
+ async function readGroupJoinMailboxAsync(filePath: string, delivery: Record<string, MailboxMessageStatus>): Promise<RunUiGroupJoin[]> {
368
+ return tailJsonlLinesAsync(filePath, MAX_TAIL_LINES, (line) => {
369
+ try {
370
+ const parsed = JSON.parse(line) as unknown;
371
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
372
+ const message = parsed as { id?: unknown; data?: unknown };
373
+ const data = message.data && typeof message.data === "object" && !Array.isArray(message.data) ? message.data as Record<string, unknown> : undefined;
374
+ if (typeof message.id !== "string" || data?.kind !== "group_join" || typeof data.requestId !== "string") return undefined;
375
+ return { requestId: data.requestId, messageId: message.id, partial: data.partial === true, ack: delivery[message.id] === "acknowledged" ? "acknowledged" as const : "pending" as const };
376
+ } catch {
377
+ return undefined;
378
+ }
379
+ });
380
+ }
381
+
217
382
  function readMailboxCounts(filePath: string, delivery: Record<string, MailboxMessageStatus>): number {
218
- try {
219
- return fs.readFileSync(filePath, "utf-8").split(/\r?\n/).filter(Boolean).reduce((count, line) => {
220
- try {
221
- const parsed = JSON.parse(line) as unknown;
222
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return count;
223
- const message = parsed as { id?: unknown; status?: unknown };
224
- if (typeof message.id !== "string" || !isMailboxStatus(message.status)) return count;
225
- return message.status !== "acknowledged" && delivery[message.id] !== "acknowledged" ? count + 1 : count;
226
- } catch {
227
- return count;
228
- }
229
- }, 0);
230
- } catch {
231
- return 0;
232
- }
383
+ const items = tailJsonlLines(filePath, MAX_TAIL_LINES, (line) => {
384
+ try {
385
+ const parsed = JSON.parse(line) as unknown;
386
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return 0;
387
+ const message = parsed as { id?: unknown; status?: unknown };
388
+ if (typeof message.id !== "string" || !isMailboxStatus(message.status)) return 0;
389
+ return message.status !== "acknowledged" && delivery[message.id] !== "acknowledged" ? 1 : 0;
390
+ } catch {
391
+ return 0;
392
+ }
393
+ }) as number[];
394
+ return items.reduce((sum, val) => sum + val, 0);
395
+ }
396
+
397
+ async function readMailboxCountsAsync(filePath: string, delivery: Record<string, MailboxMessageStatus>): Promise<number> {
398
+ const items = await tailJsonlLinesAsync(filePath, MAX_TAIL_LINES, (line) => {
399
+ try {
400
+ const parsed = JSON.parse(line) as unknown;
401
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return 0;
402
+ const message = parsed as { id?: unknown; status?: unknown };
403
+ if (typeof message.id !== "string" || !isMailboxStatus(message.status)) return 0;
404
+ return message.status !== "acknowledged" && delivery[message.id] !== "acknowledged" ? 1 : 0;
405
+ } catch {
406
+ return 0;
407
+ }
408
+ }) as number[];
409
+ return items.reduce((sum, val) => sum + val, 0);
233
410
  }
234
411
 
235
412
  function groupJoinsFrom(manifest: TeamRunManifest): RunUiGroupJoin[] {
@@ -238,6 +415,12 @@ function groupJoinsFrom(manifest: TeamRunManifest): RunUiGroupJoin[] {
238
415
  return readGroupJoinMailbox(path.join(root, "outbox.jsonl"), delivery).slice(-5);
239
416
  }
240
417
 
418
+ async function groupJoinsFromAsync(manifest: TeamRunManifest): Promise<RunUiGroupJoin[]> {
419
+ const root = path.join(manifest.stateRoot, "mailbox");
420
+ const delivery = await readDeliveryMessagesAsync(path.join(root, "delivery.json"));
421
+ return (await readGroupJoinMailboxAsync(path.join(root, "outbox.jsonl"), delivery)).slice(-5);
422
+ }
423
+
241
424
  function mailboxFrom(manifest: TeamRunManifest, agents: CrewAgentRecord[]): RunUiMailbox {
242
425
  const root = path.join(manifest.stateRoot, "mailbox");
243
426
  const delivery = readDeliveryMessages(path.join(root, "delivery.json"));
@@ -257,9 +440,29 @@ function mailboxFrom(manifest: TeamRunManifest, agents: CrewAgentRecord[]): RunU
257
440
  return { inboxUnread, outboxPending, needsAttention: inboxUnread + attentionAgents };
258
441
  }
259
442
 
443
+ async function mailboxFromAsync(manifest: TeamRunManifest, agents: CrewAgentRecord[]): Promise<RunUiMailbox> {
444
+ const root = path.join(manifest.stateRoot, "mailbox");
445
+ const delivery = await readDeliveryMessagesAsync(path.join(root, "delivery.json"));
446
+ let inboxUnread = await readMailboxCountsAsync(path.join(root, "inbox.jsonl"), delivery);
447
+ let outboxPending = await readMailboxCountsAsync(path.join(root, "outbox.jsonl"), delivery);
448
+ const tasksRoot = path.join(root, "tasks");
449
+ try {
450
+ for (const entry of await fs.promises.readdir(tasksRoot, { withFileTypes: true })) {
451
+ if (!entry.isDirectory()) continue;
452
+ inboxUnread += await readMailboxCountsAsync(path.join(tasksRoot, entry.name, "inbox.jsonl"), delivery);
453
+ outboxPending += await readMailboxCountsAsync(path.join(tasksRoot, entry.name, "outbox.jsonl"), delivery);
454
+ }
455
+ } catch {
456
+ // No task mailboxes yet.
457
+ }
458
+ const attentionAgents = agents.filter((agent) => agent.progress?.activityState === "needs_attention").length;
459
+ return { inboxUnread, outboxPending, needsAttention: inboxUnread + attentionAgents };
460
+ }
461
+
260
462
  function signatureFor(input: Omit<RunUiSnapshot, "signature" | "fetchedAt">, stamps: SnapshotStamps): string {
261
- const digest = createHash("sha256");
262
- digest.update(JSON.stringify({
463
+ try {
464
+ const digest = createHash("sha256");
465
+ digest.update(JSON.stringify({
263
466
  run: [input.manifest.runId, input.manifest.status, input.manifest.updatedAt, input.manifest.artifacts.length],
264
467
  tasks: input.tasks.map((task) => [task.id, task.status, task.startedAt, task.finishedAt, task.agentProgress, task.usage]),
265
468
  agents: input.agents.map((agent) => [agent.id, agent.status, agent.startedAt, agent.completedAt, agent.toolUses, agent.progress, agent.usage, agent.model]),
@@ -272,6 +475,10 @@ function signatureFor(input: Omit<RunUiSnapshot, "signature" | "fetchedAt">, sta
272
475
  stamps,
273
476
  }));
274
477
  return digest.digest("hex").slice(0, 16);
478
+ } catch {
479
+ // Circular reference or non-serializable data — fall back to timestamp.
480
+ return String(Date.now());
481
+ }
275
482
  }
276
483
 
277
484
  function stampsFor(manifest: TeamRunManifest, agents: CrewAgentRecord[]): SnapshotStamps {
@@ -285,6 +492,18 @@ function stampsFor(manifest: TeamRunManifest, agents: CrewAgentRecord[]): Snapsh
285
492
  };
286
493
  }
287
494
 
495
+ async function stampsForAsync(manifest: TeamRunManifest, agents: CrewAgentRecord[]): Promise<SnapshotStamps> {
496
+ const [manifestStamp, tasksStamp, agentsStamp, eventsStamp, mailbox, output] = await Promise.all([
497
+ stampFileAsync(path.join(manifest.stateRoot, "manifest.json")),
498
+ stampFileAsync(manifest.tasksPath),
499
+ stampFileAsync(agentsPath(manifest)),
500
+ stampFileAsync(manifest.eventsPath),
501
+ mailboxStampAsync(manifest),
502
+ outputStampAsync(manifest, agents),
503
+ ]);
504
+ return { manifest: manifestStamp, tasks: tasksStamp, agents: agentsStamp, events: eventsStamp, mailbox, output };
505
+ }
506
+
288
507
  export function createRunSnapshotCache(cwd: string, options: RunSnapshotCacheOptions = {}): RunSnapshotCache {
289
508
  const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
290
509
  const maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
@@ -351,6 +570,51 @@ export function createRunSnapshotCache(cwd: string, options: RunSnapshotCacheOpt
351
570
  return { snapshot, stamps, loadedAtMs: snapshot.fetchedAt, lastAccessMs: snapshot.fetchedAt };
352
571
  }
353
572
 
573
+ async function buildAsync(runId: string, previous?: CacheEntry): Promise<CacheEntry> {
574
+ let loaded: ReturnType<typeof loadRunManifestById>;
575
+ try {
576
+ loaded = loadRunManifestById(cwd, runId);
577
+ } catch {
578
+ if (previous) return previous;
579
+ throw new Error(`Run '${runId}' could not be parsed.`);
580
+ }
581
+ if (!loaded) {
582
+ if (previous) return previous;
583
+ throw new Error(`Run '${runId}' not found.`);
584
+ }
585
+ let tasks: TeamTaskState[];
586
+ let agents: CrewAgentRecord[];
587
+ try {
588
+ tasks = await readTasksAsync(loaded.manifest.tasksPath);
589
+ agents = readCrewAgents(loaded.manifest);
590
+ } catch {
591
+ if (previous) return previous;
592
+ throw new Error(`Run '${runId}' could not be parsed.`);
593
+ }
594
+ const [mailbox, groupJoins, recentEvents, recentOutput] = await Promise.all([
595
+ mailboxFromAsync(loaded.manifest, agents),
596
+ groupJoinsFromAsync(loaded.manifest),
597
+ safeRecentEventsAsync(loaded.manifest.eventsPath, recentEventsLimit),
598
+ recentOutputLinesAsync(loaded.manifest, agents, recentOutputLimit),
599
+ ]);
600
+ const base = {
601
+ runId: loaded.manifest.runId,
602
+ cwd: loaded.manifest.cwd,
603
+ manifest: loaded.manifest,
604
+ tasks,
605
+ agents,
606
+ progress: progressFromTasks(tasks),
607
+ usage: usageFrom(tasks, agents),
608
+ mailbox,
609
+ groupJoins,
610
+ recentEvents,
611
+ recentOutputLines: recentOutput,
612
+ };
613
+ const stamps = await stampsForAsync(loaded.manifest, agents);
614
+ const snapshot: RunUiSnapshot = { ...base, fetchedAt: Date.now(), signature: signatureFor(base, stamps) };
615
+ return { snapshot, stamps, loadedAtMs: snapshot.fetchedAt, lastAccessMs: snapshot.fetchedAt };
616
+ }
617
+
354
618
  function currentStamps(previous: CacheEntry): SnapshotStamps {
355
619
  const manifest = previous.snapshot.manifest;
356
620
  return {
@@ -363,6 +627,40 @@ export function createRunSnapshotCache(cwd: string, options: RunSnapshotCacheOpt
363
627
  };
364
628
  }
365
629
 
630
+ async function currentStampsAsync(previous: CacheEntry): Promise<SnapshotStamps> {
631
+ return stampsForAsync(previous.snapshot.manifest, previous.snapshot.agents);
632
+ }
633
+
634
+ async function preloadStale(runId: string): Promise<RunUiSnapshot | undefined> {
635
+ const previous = entries.get(runId);
636
+ const now = Date.now();
637
+ // Fresh enough? Return immediately
638
+ if (previous && now - previous.loadedAtMs < ttlMs) {
639
+ return touch(runId, previous);
640
+ }
641
+ // Check stamps async
642
+ if (previous) {
643
+ const stamps = await currentStampsAsync(previous);
644
+ if (sameStamps(stamps, previous.stamps)) {
645
+ previous.loadedAtMs = now;
646
+ return touch(runId, previous);
647
+ }
648
+ }
649
+ // Full async build
650
+ const entry = await buildAsync(runId, previous);
651
+ entries.set(runId, entry);
652
+ evictIfNeeded();
653
+ return entry.snapshot;
654
+ }
655
+
656
+ async function preloadAllStale(runIds: string[]): Promise<void> {
657
+ const batchSize = 4;
658
+ for (let i = 0; i < runIds.length; i += batchSize) {
659
+ const batch = runIds.slice(i, i + batchSize);
660
+ await Promise.all(batch.map((id) => preloadStale(id)));
661
+ }
662
+ }
663
+
366
664
  return {
367
665
  get(runId: string): RunUiSnapshot | undefined {
368
666
  const entry = entries.get(runId);
@@ -384,6 +682,8 @@ export function createRunSnapshotCache(cwd: string, options: RunSnapshotCacheOpt
384
682
  if (sameStamps(stamps, previous.stamps)) return touch(runId, previous);
385
683
  return this.refresh(runId);
386
684
  },
685
+ preloadStale,
686
+ preloadAllStale,
387
687
  invalidate(runId?: string): void {
388
688
  if (runId) entries.delete(runId);
389
689
  else entries.clear();
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
+ }
@@ -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 "completed":
10
- return "success";
11
- case "failed":
12
- case "stale":
13
- return "error";
14
- case "cancelled":
15
- case "blocked":
16
- case "stopped":
17
- return "warning";
18
- case "queued":
19
- default:
20
- return "dim";
21
- }
22
- }
23
-
24
- export function iconForStatus(status: RunStatus, options?: { runningGlyph?: string }): string {
25
- const glyph = options?.runningGlyph ?? "▶";
26
- switch (status) {
27
- case "completed":
28
- return "✓";
29
- case "failed":
30
- case "stale":
31
- return "";
32
- case "cancelled":
33
- case "stopped":
34
- return "";
35
- case "running":
36
- return glyph;
37
- case "queued":
38
- return "◦";
39
- case "blocked":
40
- return "";
41
- default:
42
- return "·";
43
- }
44
- }
45
-
46
- export function colorForActivity(activityState: string | undefined): CrewThemeColor {
47
- if (activityState === "needs_attention") return "warning";
48
- if (activityState === "stale") return "error";
49
- return "dim";
50
- }
51
-
52
- export function applyStatusColor(theme: CrewTheme, status: RunStatus, text: string): string {
53
- return theme.fg(colorForStatus(status), text);
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
+ }