pi-crew 0.1.41 → 0.1.43
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 +47 -0
- package/README.md +51 -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-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 +4 -4
- package/src/config/config.ts +9 -4
- package/src/extension/cross-extension-rpc.ts +82 -82
- package/src/extension/management.ts +37 -8
- package/src/extension/notification-router.ts +2 -2
- package/src/extension/register.ts +3 -0
- package/src/extension/registration/commands.ts +11 -9
- package/src/extension/registration/compaction-guard.ts +125 -125
- package/src/extension/registration/subagent-tools.ts +28 -19
- package/src/extension/result-watcher.ts +4 -4
- package/src/extension/run-bundle-schema.ts +8 -4
- package/src/extension/run-import.ts +4 -0
- package/src/extension/run-index.ts +4 -1
- package/src/extension/run-maintenance.ts +43 -24
- package/src/extension/team-tool/api.ts +1 -1
- package/src/extension/team-tool/cancel.ts +31 -31
- package/src/extension/team-tool/doctor.ts +8 -1
- package/src/extension/team-tool/handle-settings.ts +188 -0
- 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/run.ts +4 -2
- package/src/extension/team-tool/status.ts +93 -93
- package/src/extension/team-tool.ts +3 -1
- package/src/i18n.ts +184 -0
- package/src/observability/correlation.ts +2 -2
- package/src/observability/event-to-metric.ts +4 -3
- package/src/observability/exporters/adapter.ts +7 -1
- package/src/observability/exporters/otlp-exporter.ts +14 -2
- package/src/observability/exporters/prometheus-exporter.ts +9 -2
- package/src/observability/metric-registry.ts +18 -3
- package/src/observability/metric-retention.ts +11 -3
- package/src/observability/metric-sink.ts +9 -4
- package/src/observability/metrics-primitives.ts +4 -3
- package/src/prompt/prompt-runtime.ts +72 -68
- 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 -23
- package/src/runtime/background-runner.ts +53 -53
- package/src/runtime/child-pi.ts +4 -4
- package/src/runtime/completion-guard.ts +103 -99
- package/src/runtime/concurrency.ts +1 -1
- package/src/runtime/crash-recovery.ts +2 -1
- package/src/runtime/crew-agent-runtime.ts +58 -58
- package/src/runtime/deadletter.ts +14 -4
- 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 +48 -4
- 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/manifest-cache.ts +2 -2
- package/src/runtime/model-fallback.ts +269 -261
- package/src/runtime/parallel-research.ts +44 -44
- package/src/runtime/parallel-utils.ts +1 -1
- package/src/runtime/pi-json-output.ts +111 -111
- package/src/runtime/policy-engine.ts +79 -78
- package/src/runtime/post-exit-stdio-guard.ts +2 -2
- package/src/runtime/process-status.ts +56 -56
- package/src/runtime/progress-event-coalescer.ts +43 -43
- package/src/runtime/recovery-recipes.ts +74 -74
- package/src/runtime/retry-executor.ts +5 -0
- package/src/runtime/role-permission.ts +39 -39
- package/src/runtime/runtime-resolver.ts +1 -1
- package/src/runtime/session-usage.ts +79 -79
- package/src/runtime/sidechain-output.ts +29 -29
- package/src/runtime/subagent-manager.ts +3 -3
- 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 -111
- package/src/runtime/task-runner/result-utils.ts +14 -14
- package/src/runtime/task-runner/state-helpers.ts +22 -22
- package/src/runtime/team-runner.ts +3 -6
- package/src/runtime/worker-heartbeat.ts +21 -21
- package/src/runtime/worker-startup.ts +57 -57
- package/src/schema/config-schema.ts +1 -1
- package/src/schema/team-tool-schema.ts +110 -109
- package/src/state/artifact-store.ts +4 -2
- package/src/state/atomic-write.ts +12 -4
- package/src/state/contracts.ts +105 -105
- package/src/state/event-log.ts +3 -4
- package/src/state/jsonl-writer.ts +4 -1
- package/src/state/locks.ts +9 -1
- package/src/state/task-claims.ts +44 -42
- 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/discover-teams.ts +2 -2
- 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/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/live-run-sidebar.ts +1 -1
- package/src/ui/loaders.ts +158 -158
- package/src/ui/mascot.ts +3 -2
- package/src/ui/powerbar-publisher.ts +4 -3
- package/src/ui/render-diff.ts +119 -119
- package/src/ui/render-scheduler.ts +54 -14
- package/src/ui/run-dashboard.ts +39 -11
- package/src/ui/run-snapshot-cache.ts +63 -37
- package/src/ui/spinner.ts +17 -17
- package/src/ui/status-colors.ts +54 -54
- package/src/ui/syntax-highlight.ts +116 -116
- package/src/ui/theme-adapter.ts +1 -1
- package/src/ui/transcript-viewer.ts +7 -2
- package/src/utils/completion-dedupe.ts +63 -63
- package/src/utils/file-coalescer.ts +5 -3
- package/src/utils/frontmatter.ts +68 -36
- package/src/utils/git.ts +262 -262
- package/src/utils/ids.ts +12 -12
- package/src/utils/internal-error.ts +1 -1
- package/src/utils/names.ts +27 -26
- package/src/utils/paths.ts +1 -1
- package/src/utils/redaction.ts +44 -41
- package/src/utils/safe-paths.ts +47 -34
- package/src/utils/sleep.ts +2 -2
- package/src/utils/timings.ts +2 -0
- package/src/utils/visual.ts +9 -1
- package/src/workflows/discover-workflows.ts +4 -1
- package/src/workflows/validate-workflow.ts +40 -40
- package/src/worktree/branch-freshness.ts +45 -45
- package/src/worktree/worktree-manager.ts +6 -1
- 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
package/src/ui/mascot.ts
CHANGED
|
@@ -136,9 +136,9 @@ export class AnimatedMascot {
|
|
|
136
136
|
this.currentArminGrid = this.style === "armin" ? this.initialArminGrid() : emptyArminGrid();
|
|
137
137
|
this.initEffect();
|
|
138
138
|
this.interval = setInterval(() => this.tick(), this.frameIntervalMs);
|
|
139
|
-
this.interval.unref
|
|
139
|
+
this.interval.unref();
|
|
140
140
|
this.timeout = this.autoCloseMs > 0 ? setTimeout(() => this.close(), this.autoCloseMs) : undefined;
|
|
141
|
-
this.timeout?.unref
|
|
141
|
+
this.timeout?.unref();
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
private resolveEffect(requested: MascotEffect | undefined): MascotEffect {
|
|
@@ -435,6 +435,7 @@ export class AnimatedMascot {
|
|
|
435
435
|
}
|
|
436
436
|
|
|
437
437
|
dispose(): void {
|
|
438
|
+
this.doneGuard.called = true;
|
|
438
439
|
if (this.interval) clearInterval(this.interval);
|
|
439
440
|
if (this.timeout) clearTimeout(this.timeout);
|
|
440
441
|
}
|
|
@@ -92,12 +92,13 @@ export function updatePiCrewPowerbar(events: EventBus, cwd: string, config?: Cre
|
|
|
92
92
|
const agents = active.flatMap((item) => item.agents);
|
|
93
93
|
const tasks = active.flatMap((item) => item.tasks);
|
|
94
94
|
const running = agents.filter((agent) => agent.status === "running").length;
|
|
95
|
-
const waiting = active.reduce((sum, item) => sum + (item.snapshot?.progress.queued ?? item.tasks.
|
|
96
|
-
const completed = active.reduce((sum, item) => sum + (item.snapshot?.progress.completed ?? item.tasks.
|
|
95
|
+
const waiting = active.reduce((sum, item) => sum + (item.snapshot?.progress.queued ?? item.tasks.reduce((s, t) => s + (t.status === "queued" ? 1 : 0), 0)), 0);
|
|
96
|
+
const completed = active.reduce((sum, item) => sum + (item.snapshot?.progress.completed ?? item.tasks.reduce((s, t) => s + (t.status === "completed" ? 1 : 0), 0)), 0);
|
|
97
97
|
const total = Math.max(1, active.reduce((sum, item) => sum + (item.snapshot?.progress.total ?? item.tasks.length), 0) || agents.length);
|
|
98
98
|
const usage = aggregateUsage(tasks);
|
|
99
99
|
const snapshotTokens = active.reduce((sum, item) => sum + (item.snapshot ? item.snapshot.usage.tokensIn + item.snapshot.usage.tokensOut : 0), 0);
|
|
100
|
-
const
|
|
100
|
+
const hasUsage = usage && ((usage.input ?? 0) + (usage.output ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0)) > 0;
|
|
101
|
+
const tokenTotal = hasUsage ? (usage.input ?? 0) + (usage.output ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0) : snapshotTokens;
|
|
101
102
|
const model = config?.showModel === false ? undefined : agents.find((agent) => agent.model)?.model?.split("/").at(-1);
|
|
102
103
|
const tokenText = config?.showTokens === false || !tokenTotal ? undefined : compactTokens(tokenTotal);
|
|
103
104
|
const activeText = `crew ${running}a/${waiting}w${notificationBadge(notificationCount)}`;
|
package/src/ui/render-diff.ts
CHANGED
|
@@ -1,119 +1,119 @@
|
|
|
1
|
-
import * as Diff from "diff";
|
|
2
|
-
import type { CrewTheme } from "./theme-adapter.ts";
|
|
3
|
-
import { asCrewTheme } from "./theme-adapter.ts";
|
|
4
|
-
|
|
5
|
-
interface ParsedDiffLine {
|
|
6
|
-
prefix: string;
|
|
7
|
-
lineNum: string; content: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
interface DiffLineContent {
|
|
11
|
-
lineNum: string;
|
|
12
|
-
content: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function parseDiffLine(line: string): ParsedDiffLine | null {
|
|
16
|
-
const match = line.match(/^([+-\s])(\s*\d*)\s(.*)$/);
|
|
17
|
-
if (!match) return null;
|
|
18
|
-
return { prefix: match[1], lineNum: match[2], content: match[3] };
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function replaceTabs(text: string): string {
|
|
22
|
-
return text.replace(/\t/g, " ");
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function renderIntraLineDiff(theme: CrewTheme, oldContent: string, newContent: string): { removedLine: string; addedLine: string } {
|
|
26
|
-
const wordDiff = Diff.diffWords(oldContent, newContent);
|
|
27
|
-
let removedLine = "";
|
|
28
|
-
let addedLine = "";
|
|
29
|
-
let isFirstRemoved = true;
|
|
30
|
-
let isFirstAdded = true;
|
|
31
|
-
|
|
32
|
-
for (const part of wordDiff) {
|
|
33
|
-
if (part.removed) {
|
|
34
|
-
let value = part.value;
|
|
35
|
-
if (isFirstRemoved) {
|
|
36
|
-
const leadingWs = value.match(/^(\s*)/)?.[1] ?? "";
|
|
37
|
-
value = value.slice(leadingWs.length);
|
|
38
|
-
removedLine += leadingWs;
|
|
39
|
-
isFirstRemoved = false;
|
|
40
|
-
}
|
|
41
|
-
if (value) removedLine += theme.inverse?.(value) ?? value;
|
|
42
|
-
} else if (part.added) {
|
|
43
|
-
let value = part.value;
|
|
44
|
-
if (isFirstAdded) {
|
|
45
|
-
const leadingWs = value.match(/^(\s*)/)?.[1] ?? "";
|
|
46
|
-
value = value.slice(leadingWs.length);
|
|
47
|
-
addedLine += leadingWs;
|
|
48
|
-
isFirstAdded = false;
|
|
49
|
-
}
|
|
50
|
-
if (value) addedLine += theme.inverse?.(value) ?? value;
|
|
51
|
-
} else {
|
|
52
|
-
removedLine += part.value;
|
|
53
|
-
addedLine += part.value;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return { removedLine, addedLine };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export interface RenderDiffOptions {
|
|
61
|
-
filePath?: string;
|
|
62
|
-
theme?: unknown;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function renderDiff(diffText: string, options: RenderDiffOptions = {}): string {
|
|
66
|
-
const theme = asCrewTheme(options.theme);
|
|
67
|
-
const lines = diffText.split("\n");
|
|
68
|
-
const result: string[] = [];
|
|
69
|
-
let i = 0;
|
|
70
|
-
|
|
71
|
-
while (i < lines.length) {
|
|
72
|
-
const line = lines[i] ?? "";
|
|
73
|
-
const parsed = parseDiffLine(line);
|
|
74
|
-
if (!parsed) {
|
|
75
|
-
result.push(theme.fg("toolDiffContext", line));
|
|
76
|
-
i++;
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (parsed.prefix === "-") {
|
|
81
|
-
const removedLines: DiffLineContent[] = [];
|
|
82
|
-
while (i < lines.length) {
|
|
83
|
-
const nextParsed = parseDiffLine(lines[i] ?? "");
|
|
84
|
-
if (!nextParsed || nextParsed.prefix !== "-") break;
|
|
85
|
-
removedLines.push({ lineNum: nextParsed.lineNum, content: nextParsed.content });
|
|
86
|
-
i++;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const addedLines: DiffLineContent[] = [];
|
|
90
|
-
while (i < lines.length) {
|
|
91
|
-
const nextParsed = parseDiffLine(lines[i] ?? "");
|
|
92
|
-
if (!nextParsed || nextParsed.prefix !== "+") break;
|
|
93
|
-
addedLines.push({ lineNum: nextParsed.lineNum, content: nextParsed.content });
|
|
94
|
-
i++;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (removedLines.length === 1 && addedLines.length === 1) {
|
|
98
|
-
const { removedLine, addedLine } = renderIntraLineDiff(theme, replaceTabs(removedLines[0]!.content), replaceTabs(addedLines[0]!.content));
|
|
99
|
-
result.push(theme.fg("toolDiffRemoved", `-${removedLines[0]!.lineNum} ${removedLine}`));
|
|
100
|
-
result.push(theme.fg("toolDiffAdded", `+${addedLines[0]!.lineNum} ${addedLine}`));
|
|
101
|
-
} else {
|
|
102
|
-
for (const removed of removedLines) {
|
|
103
|
-
result.push(theme.fg("toolDiffRemoved", `-${removed.lineNum} ${replaceTabs(removed.content)}`));
|
|
104
|
-
}
|
|
105
|
-
for (const added of addedLines) {
|
|
106
|
-
result.push(theme.fg("toolDiffAdded", `+${added.lineNum} ${replaceTabs(added.content)}`));
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
} else if (parsed.prefix === "+") {
|
|
110
|
-
result.push(theme.fg("toolDiffAdded", `+${parsed.lineNum} ${replaceTabs(parsed.content)}`));
|
|
111
|
-
i++;
|
|
112
|
-
} else {
|
|
113
|
-
result.push(theme.fg("toolDiffContext", ` ${parsed.lineNum} ${replaceTabs(parsed.content)}`));
|
|
114
|
-
i++;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
return result.join("\n");
|
|
119
|
-
}
|
|
1
|
+
import * as Diff from "diff";
|
|
2
|
+
import type { CrewTheme } from "./theme-adapter.ts";
|
|
3
|
+
import { asCrewTheme } from "./theme-adapter.ts";
|
|
4
|
+
|
|
5
|
+
interface ParsedDiffLine {
|
|
6
|
+
prefix: string;
|
|
7
|
+
lineNum: string; content: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface DiffLineContent {
|
|
11
|
+
lineNum: string;
|
|
12
|
+
content: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function parseDiffLine(line: string): ParsedDiffLine | null {
|
|
16
|
+
const match = line.match(/^([+-\s])(\s*\d*)\s(.*)$/);
|
|
17
|
+
if (!match) return null;
|
|
18
|
+
return { prefix: match[1], lineNum: match[2], content: match[3] };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function replaceTabs(text: string): string {
|
|
22
|
+
return text.replace(/\t/g, " ");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function renderIntraLineDiff(theme: CrewTheme, oldContent: string, newContent: string): { removedLine: string; addedLine: string } {
|
|
26
|
+
const wordDiff = Diff.diffWords(oldContent, newContent);
|
|
27
|
+
let removedLine = "";
|
|
28
|
+
let addedLine = "";
|
|
29
|
+
let isFirstRemoved = true;
|
|
30
|
+
let isFirstAdded = true;
|
|
31
|
+
|
|
32
|
+
for (const part of wordDiff) {
|
|
33
|
+
if (part.removed) {
|
|
34
|
+
let value = part.value;
|
|
35
|
+
if (isFirstRemoved) {
|
|
36
|
+
const leadingWs = value.match(/^(\s*)/)?.[1] ?? "";
|
|
37
|
+
value = value.slice(leadingWs.length);
|
|
38
|
+
removedLine += leadingWs;
|
|
39
|
+
isFirstRemoved = false;
|
|
40
|
+
}
|
|
41
|
+
if (value) removedLine += theme.inverse?.(value) ?? value;
|
|
42
|
+
} else if (part.added) {
|
|
43
|
+
let value = part.value;
|
|
44
|
+
if (isFirstAdded) {
|
|
45
|
+
const leadingWs = value.match(/^(\s*)/)?.[1] ?? "";
|
|
46
|
+
value = value.slice(leadingWs.length);
|
|
47
|
+
addedLine += leadingWs;
|
|
48
|
+
isFirstAdded = false;
|
|
49
|
+
}
|
|
50
|
+
if (value) addedLine += theme.inverse?.(value) ?? value;
|
|
51
|
+
} else {
|
|
52
|
+
removedLine += part.value;
|
|
53
|
+
addedLine += part.value;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { removedLine, addedLine };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface RenderDiffOptions {
|
|
61
|
+
filePath?: string;
|
|
62
|
+
theme?: unknown;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function renderDiff(diffText: string, options: RenderDiffOptions = {}): string {
|
|
66
|
+
const theme = asCrewTheme(options.theme);
|
|
67
|
+
const lines = diffText.split("\n");
|
|
68
|
+
const result: string[] = [];
|
|
69
|
+
let i = 0;
|
|
70
|
+
|
|
71
|
+
while (i < lines.length) {
|
|
72
|
+
const line = lines[i] ?? "";
|
|
73
|
+
const parsed = parseDiffLine(line);
|
|
74
|
+
if (!parsed) {
|
|
75
|
+
result.push(theme.fg("toolDiffContext", line));
|
|
76
|
+
i++;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (parsed.prefix === "-") {
|
|
81
|
+
const removedLines: DiffLineContent[] = [];
|
|
82
|
+
while (i < lines.length) {
|
|
83
|
+
const nextParsed = parseDiffLine(lines[i] ?? "");
|
|
84
|
+
if (!nextParsed || nextParsed.prefix !== "-") break;
|
|
85
|
+
removedLines.push({ lineNum: nextParsed.lineNum, content: nextParsed.content });
|
|
86
|
+
i++;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const addedLines: DiffLineContent[] = [];
|
|
90
|
+
while (i < lines.length) {
|
|
91
|
+
const nextParsed = parseDiffLine(lines[i] ?? "");
|
|
92
|
+
if (!nextParsed || nextParsed.prefix !== "+") break;
|
|
93
|
+
addedLines.push({ lineNum: nextParsed.lineNum, content: nextParsed.content });
|
|
94
|
+
i++;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (removedLines.length === 1 && addedLines.length === 1) {
|
|
98
|
+
const { removedLine, addedLine } = renderIntraLineDiff(theme, replaceTabs(removedLines[0]!.content), replaceTabs(addedLines[0]!.content));
|
|
99
|
+
result.push(theme.fg("toolDiffRemoved", `-${removedLines[0]!.lineNum} ${removedLine}`));
|
|
100
|
+
result.push(theme.fg("toolDiffAdded", `+${addedLines[0]!.lineNum} ${addedLine}`));
|
|
101
|
+
} else {
|
|
102
|
+
for (const removed of removedLines) {
|
|
103
|
+
result.push(theme.fg("toolDiffRemoved", `-${removed.lineNum} ${replaceTabs(removed.content)}`));
|
|
104
|
+
}
|
|
105
|
+
for (const added of addedLines) {
|
|
106
|
+
result.push(theme.fg("toolDiffAdded", `+${added.lineNum} ${replaceTabs(added.content)}`));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} else if (parsed.prefix === "+") {
|
|
110
|
+
result.push(theme.fg("toolDiffAdded", `+${parsed.lineNum} ${replaceTabs(parsed.content)}`));
|
|
111
|
+
i++;
|
|
112
|
+
} else {
|
|
113
|
+
result.push(theme.fg("toolDiffContext", ` ${parsed.lineNum} ${replaceTabs(parsed.content)}`));
|
|
114
|
+
i++;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return result.join("\n");
|
|
119
|
+
}
|
|
@@ -22,15 +22,26 @@ const DEFAULT_EVENTS = [
|
|
|
22
22
|
"crew.mailbox.message",
|
|
23
23
|
];
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Coordinates UI renders with debounce + fallback polling.
|
|
27
|
+
*
|
|
28
|
+
* Critical: uses recursive setTimeout instead of setInterval + a rendering
|
|
29
|
+
* guard (`rendering` / `pendingRender`) so that when render() takes longer
|
|
30
|
+
* than the fallback interval, callbacks do NOT pile up and storm the event
|
|
31
|
+
* loop. Instead, overlapping schedules are collapsed into a single deferred
|
|
32
|
+
* re-render.
|
|
33
|
+
*/
|
|
25
34
|
export class RenderScheduler {
|
|
26
35
|
private readonly render: () => void;
|
|
27
36
|
private readonly onInvalidate?: (payload: unknown) => void;
|
|
28
37
|
private readonly debounceMs: number;
|
|
29
38
|
private readonly fallbackMs: number;
|
|
30
39
|
private debounceTimer: ReturnType<typeof setTimeout> | undefined;
|
|
31
|
-
private fallbackTimer: ReturnType<typeof
|
|
40
|
+
private fallbackTimer: ReturnType<typeof setTimeout> | undefined;
|
|
32
41
|
private disposed = false;
|
|
33
42
|
private lastEventAt = 0;
|
|
43
|
+
private rendering = false;
|
|
44
|
+
private pendingRender = false;
|
|
34
45
|
private readonly unsubs: Array<() => void> = [];
|
|
35
46
|
|
|
36
47
|
constructor(events: RenderSchedulerEventBus | undefined, render: () => void, options: RenderSchedulerOptions = {}) {
|
|
@@ -39,8 +50,8 @@ export class RenderScheduler {
|
|
|
39
50
|
this.debounceMs = options.debounceMs ?? 75;
|
|
40
51
|
this.fallbackMs = options.fallbackMs ?? 750;
|
|
41
52
|
for (const event of options.events ?? DEFAULT_EVENTS) this.subscribe(events, event);
|
|
42
|
-
this.fallbackTimer =
|
|
43
|
-
this.fallbackTimer.unref
|
|
53
|
+
this.fallbackTimer = setTimeout(() => this.fallbackLoop(), this.fallbackMs);
|
|
54
|
+
this.fallbackTimer.unref();
|
|
44
55
|
}
|
|
45
56
|
|
|
46
57
|
private subscribe(events: RenderSchedulerEventBus | undefined, event: string): void {
|
|
@@ -54,10 +65,19 @@ export class RenderScheduler {
|
|
|
54
65
|
}
|
|
55
66
|
}
|
|
56
67
|
|
|
57
|
-
|
|
68
|
+
/** Recursive setTimeout — avoids setInterval timer storms. */
|
|
69
|
+
private fallbackLoop(): void {
|
|
58
70
|
if (this.disposed) return;
|
|
59
|
-
if (Date.now() - this.lastEventAt < this.fallbackMs)
|
|
60
|
-
|
|
71
|
+
if (Date.now() - this.lastEventAt < this.fallbackMs) {
|
|
72
|
+
if (this.disposed) return;
|
|
73
|
+
this.fallbackTimer = setTimeout(() => this.fallbackLoop(), this.fallbackMs);
|
|
74
|
+
this.fallbackTimer.unref();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
this.schedule();
|
|
78
|
+
if (this.disposed) return;
|
|
79
|
+
this.fallbackTimer = setTimeout(() => this.fallbackLoop(), this.fallbackMs);
|
|
80
|
+
this.fallbackTimer.unref();
|
|
61
81
|
}
|
|
62
82
|
|
|
63
83
|
schedule(payload?: unknown): void {
|
|
@@ -73,15 +93,39 @@ export class RenderScheduler {
|
|
|
73
93
|
this.debounceTimer = undefined;
|
|
74
94
|
this.flush();
|
|
75
95
|
}, this.debounceMs);
|
|
76
|
-
this.debounceTimer.unref
|
|
96
|
+
this.debounceTimer.unref();
|
|
77
97
|
}
|
|
78
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Flush a render. If a render is already in progress the request is
|
|
101
|
+
* collapsed: `pendingRender` is set and the caller that holds
|
|
102
|
+
* `rendering==true` will loop one more time after finishing.
|
|
103
|
+
*/
|
|
79
104
|
flush(): void {
|
|
80
105
|
if (this.disposed) return;
|
|
106
|
+
if (this.rendering) {
|
|
107
|
+
this.pendingRender = true;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
this.rendering = true;
|
|
111
|
+
this.pendingRender = false;
|
|
112
|
+
let iterations = 0;
|
|
81
113
|
try {
|
|
82
|
-
|
|
114
|
+
do {
|
|
115
|
+
this.pendingRender = false;
|
|
116
|
+
this.render();
|
|
117
|
+
iterations += 1;
|
|
118
|
+
// Safety valve: 5 re-renders max per flush to prevent infinite loops
|
|
119
|
+
// if render() itself calls flush() synchronously.
|
|
120
|
+
} while (this.pendingRender && !this.disposed && iterations < 5);
|
|
83
121
|
} catch (error) {
|
|
84
122
|
logInternalError("render-scheduler.render", error);
|
|
123
|
+
} finally {
|
|
124
|
+
this.rendering = false;
|
|
125
|
+
// If we hit the iteration cap, schedule one more render to drain.
|
|
126
|
+
if (iterations >= 5 && this.pendingRender && !this.disposed) {
|
|
127
|
+
this.schedule();
|
|
128
|
+
}
|
|
85
129
|
}
|
|
86
130
|
}
|
|
87
131
|
|
|
@@ -89,15 +133,11 @@ export class RenderScheduler {
|
|
|
89
133
|
if (this.disposed) return;
|
|
90
134
|
this.disposed = true;
|
|
91
135
|
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
92
|
-
if (this.fallbackTimer)
|
|
136
|
+
if (this.fallbackTimer) clearTimeout(this.fallbackTimer);
|
|
93
137
|
this.debounceTimer = undefined;
|
|
94
138
|
this.fallbackTimer = undefined;
|
|
95
139
|
for (const unsub of this.unsubs.splice(0)) {
|
|
96
|
-
try {
|
|
97
|
-
unsub();
|
|
98
|
-
} catch (error) {
|
|
99
|
-
logInternalError("render-scheduler.unsubscribe", error);
|
|
100
|
-
}
|
|
140
|
+
try { unsub(); } catch (error) { logInternalError("render-scheduler.unsubscribe", error); }
|
|
101
141
|
}
|
|
102
142
|
}
|
|
103
143
|
}
|
package/src/ui/run-dashboard.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { Box, Text } from "./layout-primitives.ts";
|
|
|
12
12
|
import { DynamicCrewBorder } from "./dynamic-border.ts";
|
|
13
13
|
import { CrewFooter } from "./crew-footer.ts";
|
|
14
14
|
import { aggregateUsage } from "../state/usage.ts";
|
|
15
|
+
import { logInternalError } from "../utils/internal-error.ts";
|
|
15
16
|
import { renderAgentsPane } from "./dashboard-panes/agents-pane.ts";
|
|
16
17
|
import { renderMailboxPane } from "./dashboard-panes/mailbox-pane.ts";
|
|
17
18
|
import { renderProgressPane } from "./dashboard-panes/progress-pane.ts";
|
|
@@ -194,18 +195,38 @@ function runLabel(run: TeamRunManifest, selected: boolean, snapshotCache?: RunSn
|
|
|
194
195
|
const stale = isLikelyOrphanedActiveRun(run, agents);
|
|
195
196
|
const running = agents.find((agent) => agent.status === "running");
|
|
196
197
|
const queued = agents.find((agent) => agent.status === "queued");
|
|
197
|
-
const step = stale ? "orphaned queued run" : running ? `step ${running.taskId}` : queued ? `queued ${queued.taskId}` : `agents ${agents.length}`;
|
|
198
|
+
const step = stale ? "orphaned queued run" : running ? `step ${running.taskId.replace(/[\x00-\x1f\x7f-\x9f]/g, "")}` : queued ? `queued ${queued.taskId.replace(/[\x00-\x1f\x7f-\x9f]/g, "")}` : `agents ${agents.length}`;
|
|
198
199
|
const status: RunStatus = stale ? "stale" : (run.status as RunStatus);
|
|
199
200
|
const marker = selected ? "›" : " ";
|
|
200
201
|
return `${marker} ${iconForStatus(status, { runningGlyph: spinnerFrame(run.runId) })} ${run.runId.slice(-8)} ${status} | ${run.team}/${run.workflow ?? "none"} | ${step} | ${run.goal}`;
|
|
201
202
|
}
|
|
202
203
|
|
|
204
|
+
interface ResolvedRun {
|
|
205
|
+
manifest: TeamRunManifest;
|
|
206
|
+
snapshot: RunUiSnapshot | undefined;
|
|
207
|
+
agents: CrewAgentRecord[];
|
|
208
|
+
status: RunStatus;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function resolveRuns(runs: TeamRunManifest[], snapshotCache?: RunSnapshotCache): Map<string, ResolvedRun> {
|
|
212
|
+
const map = new Map<string, ResolvedRun>();
|
|
213
|
+
for (const run of runs) {
|
|
214
|
+
const snapshot = snapshotFor(run, snapshotCache);
|
|
215
|
+
const agents = snapshot?.agents ?? agentsFor(run, snapshotCache);
|
|
216
|
+
const displayRun = snapshot?.manifest ?? run;
|
|
217
|
+
const status: RunStatus = isLikelyOrphanedActiveRun(displayRun, agents) ? "stale" : (displayRun.status as RunStatus);
|
|
218
|
+
map.set(run.runId, { manifest: run, snapshot, agents, status });
|
|
219
|
+
}
|
|
220
|
+
return map;
|
|
221
|
+
}
|
|
222
|
+
|
|
203
223
|
function groupedRuns(runs: TeamRunManifest[], snapshotCache?: RunSnapshotCache): Array<{ label: string; run?: TeamRunManifest }> {
|
|
204
|
-
const
|
|
205
|
-
const recent = runs.filter((run) => !isDisplayActiveRun(snapshotFor(run, snapshotCache)?.manifest ?? run, agentsFor(run, snapshotCache)));
|
|
224
|
+
const resolved = resolveRuns(runs, snapshotCache);
|
|
206
225
|
const rows: Array<{ label: string; run?: TeamRunManifest }> = [];
|
|
226
|
+
const active = runs.filter((run) => isDisplayActiveRun(resolved.get(run.runId)?.snapshot?.manifest ?? run, resolved.get(run.runId)?.agents ?? []));
|
|
227
|
+
const rest = runs.filter((run) => !isDisplayActiveRun(resolved.get(run.runId)?.snapshot?.manifest ?? run, resolved.get(run.runId)?.agents ?? []));
|
|
207
228
|
if (active.length) rows.push({ label: "Active" }, ...active.map((run) => ({ label: run.runId, run })));
|
|
208
|
-
if (
|
|
229
|
+
if (rest.length) rows.push({ label: "Recent" }, ...rest.map((run) => ({ label: run.runId, run })));
|
|
209
230
|
return rows;
|
|
210
231
|
}
|
|
211
232
|
|
|
@@ -214,13 +235,9 @@ function selectedRunFromGrouped(runs: TeamRunManifest[], selected: number, snaps
|
|
|
214
235
|
}
|
|
215
236
|
|
|
216
237
|
function countByStatus(runs: TeamRunManifest[], snapshotCache?: RunSnapshotCache): string {
|
|
238
|
+
const resolved = resolveRuns(runs, snapshotCache);
|
|
217
239
|
const counts = new Map<RunStatus, number>();
|
|
218
|
-
for (const
|
|
219
|
-
const snapshot = snapshotFor(run, snapshotCache);
|
|
220
|
-
const displayRun = snapshot?.manifest ?? run;
|
|
221
|
-
const status: RunStatus = isLikelyOrphanedActiveRun(displayRun, snapshot?.agents ?? agentsFor(run, snapshotCache)) ? "stale" : (displayRun.status as RunStatus);
|
|
222
|
-
counts.set(status, (counts.get(status) ?? 0) + 1);
|
|
223
|
-
}
|
|
240
|
+
for (const r of resolved.values()) counts.set(r.status, (counts.get(r.status) ?? 0) + 1);
|
|
224
241
|
return [...counts.entries()].map(([status, count]) => `${status}=${count}`).join(", ") || "none";
|
|
225
242
|
}
|
|
226
243
|
|
|
@@ -253,10 +270,12 @@ export class RunDashboard implements DashboardComponent {
|
|
|
253
270
|
private refreshRuns(): void {
|
|
254
271
|
if (!this.options.runProvider) return;
|
|
255
272
|
const selectedRunId = this.selectedRunId();
|
|
256
|
-
|
|
273
|
+
const next = this.options.runProvider();
|
|
274
|
+
this.runs = Array.isArray(next) ? next : this.runs;
|
|
257
275
|
if (selectedRunId) {
|
|
258
276
|
const nextIndex = groupedRuns(this.runs, this.options.snapshotCache).filter((row) => row.run).findIndex((row) => row.run?.runId === selectedRunId);
|
|
259
277
|
if (nextIndex >= 0) this.selected = nextIndex;
|
|
278
|
+
else this.selected = 0;
|
|
260
279
|
}
|
|
261
280
|
}
|
|
262
281
|
|
|
@@ -289,6 +308,15 @@ export class RunDashboard implements DashboardComponent {
|
|
|
289
308
|
}
|
|
290
309
|
|
|
291
310
|
render(width: number): string[] {
|
|
311
|
+
try {
|
|
312
|
+
return this.renderUnsafe(width);
|
|
313
|
+
} catch (error) {
|
|
314
|
+
logInternalError("run-dashboard.render", error);
|
|
315
|
+
return renderLines(["Dashboard error — see logs for details."], width);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private renderUnsafe(width: number): string[] {
|
|
292
320
|
this.refreshRuns();
|
|
293
321
|
const signature = this.buildSignature();
|
|
294
322
|
if (signature !== this.cachedVersion || this.cachedWidth !== width) {
|
|
@@ -4,17 +4,19 @@ 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 {
|
|
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
11
|
import type { RunSnapshotCache, RunUiGroupJoin, RunUiMailbox, RunUiProgress, RunUiSnapshot, RunUiUsage } from "./snapshot-types.ts";
|
|
12
12
|
|
|
13
|
-
const DEFAULT_TTL_MS =
|
|
13
|
+
const DEFAULT_TTL_MS = 500;
|
|
14
14
|
const DEFAULT_MAX_ENTRIES = 24;
|
|
15
15
|
const DEFAULT_RECENT_EVENTS = 20;
|
|
16
16
|
const DEFAULT_RECENT_OUTPUT_LINES = 20;
|
|
17
17
|
const MAX_TAIL_BYTES = 32 * 1024;
|
|
18
|
+
/** Max JSONL lines to tail when reading growing files (events, mailbox). */
|
|
19
|
+
const MAX_TAIL_LINES = 500;
|
|
18
20
|
|
|
19
21
|
interface FileStamp {
|
|
20
22
|
mtimeMs: number;
|
|
@@ -116,14 +118,40 @@ function readTasks(tasksPath: string): TeamTaskState[] {
|
|
|
116
118
|
}
|
|
117
119
|
}
|
|
118
120
|
|
|
119
|
-
|
|
121
|
+
/** Tail-read JSONL lines from a file, returning parsed objects (limited). */
|
|
122
|
+
function tailJsonlLines<T>(filePath: string, limit: number, parse: (line: string) => T | undefined): T[] {
|
|
123
|
+
if (limit <= 0) return [];
|
|
120
124
|
try {
|
|
121
|
-
|
|
125
|
+
const stat = fs.statSync(filePath);
|
|
126
|
+
const bytesToRead = Math.min(stat.size, MAX_TAIL_BYTES);
|
|
127
|
+
const fd = fs.openSync(filePath, "r");
|
|
128
|
+
try {
|
|
129
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
130
|
+
fs.readSync(fd, buffer, 0, bytesToRead, stat.size - bytesToRead);
|
|
131
|
+
const lines = buffer.toString("utf-8").split(/\r?\n/).filter(Boolean);
|
|
132
|
+
return lines.flatMap((line) => {
|
|
133
|
+
const item = parse(line);
|
|
134
|
+
return item ? [item] : [];
|
|
135
|
+
}).slice(-limit);
|
|
136
|
+
} finally {
|
|
137
|
+
fs.closeSync(fd);
|
|
138
|
+
}
|
|
122
139
|
} catch {
|
|
123
140
|
return [];
|
|
124
141
|
}
|
|
125
142
|
}
|
|
126
143
|
|
|
144
|
+
function safeRecentEvents(eventsPath: string, limit: number): TeamEvent[] {
|
|
145
|
+
return tailJsonlLines(eventsPath, limit, (line) => {
|
|
146
|
+
try {
|
|
147
|
+
const parsed = JSON.parse(line) as unknown;
|
|
148
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as TeamEvent) : undefined;
|
|
149
|
+
} catch {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
127
155
|
function tailLines(filePath: string, limit: number): string[] {
|
|
128
156
|
if (limit <= 0) return [];
|
|
129
157
|
try {
|
|
@@ -196,40 +224,33 @@ function readDeliveryMessages(filePath: string): Record<string, MailboxMessageSt
|
|
|
196
224
|
}
|
|
197
225
|
|
|
198
226
|
function readGroupJoinMailbox(filePath: string, delivery: Record<string, MailboxMessageStatus>): RunUiGroupJoin[] {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
});
|
|
212
|
-
} catch {
|
|
213
|
-
return [];
|
|
214
|
-
}
|
|
227
|
+
return tailJsonlLines(filePath, MAX_TAIL_LINES, (line) => {
|
|
228
|
+
try {
|
|
229
|
+
const parsed = JSON.parse(line) as unknown;
|
|
230
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
|
|
231
|
+
const message = parsed as { id?: unknown; data?: unknown };
|
|
232
|
+
const data = message.data && typeof message.data === "object" && !Array.isArray(message.data) ? message.data as Record<string, unknown> : undefined;
|
|
233
|
+
if (typeof message.id !== "string" || data?.kind !== "group_join" || typeof data.requestId !== "string") return undefined;
|
|
234
|
+
return { requestId: data.requestId, messageId: message.id, partial: data.partial === true, ack: delivery[message.id] === "acknowledged" ? "acknowledged" as const : "pending" as const };
|
|
235
|
+
} catch {
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
});
|
|
215
239
|
}
|
|
216
240
|
|
|
217
241
|
function readMailboxCounts(filePath: string, delivery: Record<string, MailboxMessageStatus>): number {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
} catch {
|
|
231
|
-
return 0;
|
|
232
|
-
}
|
|
242
|
+
const items = tailJsonlLines(filePath, MAX_TAIL_LINES, (line) => {
|
|
243
|
+
try {
|
|
244
|
+
const parsed = JSON.parse(line) as unknown;
|
|
245
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return 0;
|
|
246
|
+
const message = parsed as { id?: unknown; status?: unknown };
|
|
247
|
+
if (typeof message.id !== "string" || !isMailboxStatus(message.status)) return 0;
|
|
248
|
+
return message.status !== "acknowledged" && delivery[message.id] !== "acknowledged" ? 1 : 0;
|
|
249
|
+
} catch {
|
|
250
|
+
return 0;
|
|
251
|
+
}
|
|
252
|
+
}) as number[];
|
|
253
|
+
return items.reduce((sum, val) => sum + val, 0);
|
|
233
254
|
}
|
|
234
255
|
|
|
235
256
|
function groupJoinsFrom(manifest: TeamRunManifest): RunUiGroupJoin[] {
|
|
@@ -258,8 +279,9 @@ function mailboxFrom(manifest: TeamRunManifest, agents: CrewAgentRecord[]): RunU
|
|
|
258
279
|
}
|
|
259
280
|
|
|
260
281
|
function signatureFor(input: Omit<RunUiSnapshot, "signature" | "fetchedAt">, stamps: SnapshotStamps): string {
|
|
261
|
-
|
|
262
|
-
|
|
282
|
+
try {
|
|
283
|
+
const digest = createHash("sha256");
|
|
284
|
+
digest.update(JSON.stringify({
|
|
263
285
|
run: [input.manifest.runId, input.manifest.status, input.manifest.updatedAt, input.manifest.artifacts.length],
|
|
264
286
|
tasks: input.tasks.map((task) => [task.id, task.status, task.startedAt, task.finishedAt, task.agentProgress, task.usage]),
|
|
265
287
|
agents: input.agents.map((agent) => [agent.id, agent.status, agent.startedAt, agent.completedAt, agent.toolUses, agent.progress, agent.usage, agent.model]),
|
|
@@ -272,6 +294,10 @@ function signatureFor(input: Omit<RunUiSnapshot, "signature" | "fetchedAt">, sta
|
|
|
272
294
|
stamps,
|
|
273
295
|
}));
|
|
274
296
|
return digest.digest("hex").slice(0, 16);
|
|
297
|
+
} catch {
|
|
298
|
+
// Circular reference or non-serializable data — fall back to timestamp.
|
|
299
|
+
return String(Date.now());
|
|
300
|
+
}
|
|
275
301
|
}
|
|
276
302
|
|
|
277
303
|
function stampsFor(manifest: TeamRunManifest, agents: CrewAgentRecord[]): SnapshotStamps {
|