pi-crew 0.2.3 → 0.2.5
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/AGENTS.md +57 -32
- package/CHANGELOG.md +466 -448
- package/LICENSE +21 -21
- package/NOTICE.md +16 -16
- package/README.md +323 -323
- package/docs/FEATURE_INTAKE.md +126 -0
- package/docs/HARNESS.md +86 -0
- package/docs/HARNESS_BACKLOG.md +41 -0
- package/docs/TEST_MATRIX.md +49 -0
- package/docs/actions-reference.md +595 -595
- package/docs/architecture.md +180 -180
- package/docs/code-review-2026-05-11.md +592 -592
- package/docs/commands-reference.md +347 -347
- package/docs/comparison-pi-subagents-vs-pi-crew.md +303 -0
- package/docs/decisions/0001-durable-state.md +41 -0
- package/docs/decisions/0002-child-process-for-async.md +42 -0
- package/docs/decisions/0003-depth-guard.md +36 -0
- package/docs/decisions/0004-execfile-over-exec.md +34 -0
- package/docs/decisions/0005-no-parameter-properties.md +49 -0
- package/docs/decisions/0006-publish-bundled-esm.md +63 -0
- package/docs/decisions/0007-active-run-binary-index.md +54 -0
- package/docs/decisions/0008-child-pi-warm-pool.md +61 -0
- package/docs/decisions/README.md +23 -0
- package/docs/followup-review-round4-2026-05-13.md +107 -0
- package/docs/implementation-plan-top3.md +333 -0
- package/docs/live-mailbox-runtime.md +36 -36
- package/docs/next-upgrade-roadmap.md +808 -808
- package/docs/oh-my-pi-research.md +509 -0
- package/docs/perf/baseline-2026-05.md +113 -0
- package/docs/perf/final-report-2026-05.md +206 -0
- package/docs/perf/sprint-1-report.md +71 -0
- package/docs/perf/sprint-2-report.md +81 -0
- package/docs/perf/sprint-2.5-report.md +53 -0
- package/docs/perf/sprint-3-report.md +36 -0
- package/docs/perf/sprint-4-report.md +47 -0
- package/docs/perf/sprint-5-report.md +51 -0
- package/docs/perf/sprint-6-report.md +94 -0
- package/docs/perf/sprint-7-report.md +74 -0
- package/docs/perf/upgrade-plan-2026-05.md +147 -0
- package/docs/pi-subagents3-deep-analysis.md +508 -0
- package/docs/product/README.md +31 -0
- package/docs/product/platform.md +27 -0
- package/docs/product/runtime-safety.md +37 -0
- package/docs/product/team-run.md +39 -0
- package/docs/product/team-tool.md +37 -0
- package/docs/publishing.md +65 -65
- package/docs/resource-formats.md +134 -134
- package/docs/runtime-analysis-child-vs-live.md +171 -0
- package/docs/runtime-flow.md +148 -148
- package/docs/runtime-migration-in-process-analysis.md +250 -0
- package/docs/stories/README.md +30 -0
- package/docs/stories/backlog.md +36 -0
- package/docs/templates/decision.md +27 -0
- package/docs/templates/story.md +44 -0
- package/docs/templates/validation-report.md +32 -0
- package/docs/usage.md +238 -238
- package/index.ts +7 -6
- package/install.mjs +65 -65
- package/package.json +107 -100
- package/schema.json +222 -222
- package/skills/child-pi-spawning/SKILL.md +213 -0
- package/skills/context-artifact-hygiene/SKILL.md +32 -0
- package/skills/event-log-tracing/SKILL.md +299 -0
- package/skills/git-master/SKILL.md +225 -24
- package/skills/live-agent-lifecycle/SKILL.md +192 -0
- package/skills/mailbox-interactive/SKILL.md +300 -19
- package/skills/model-routing-context/SKILL.md +94 -0
- package/skills/multi-perspective-review/SKILL.md +88 -0
- package/skills/read-only-explorer/SKILL.md +250 -26
- package/skills/safe-bash/SKILL.md +307 -21
- package/skills/verification-before-done/SKILL.md +11 -2
- package/skills/widget-rendering/SKILL.md +258 -0
- package/skills/workspace-isolation/SKILL.md +202 -0
- package/skills/worktree-isolation/SKILL.md +202 -18
- package/src/adapters/claude-adapter.ts +25 -25
- package/src/adapters/codex-adapter.ts +21 -21
- package/src/adapters/cursor-adapter.ts +17 -17
- package/src/adapters/export-util.ts +137 -137
- package/src/adapters/index.ts +15 -15
- package/src/adapters/registry.ts +18 -18
- package/src/adapters/types.ts +23 -23
- package/src/agents/agent-config.ts +38 -38
- package/src/agents/agent-serializer.ts +38 -38
- package/src/agents/discover-agents.ts +121 -118
- package/src/config/config.ts +740 -858
- package/src/config/defaults.ts +96 -96
- package/src/config/drift-detector.ts +211 -211
- package/src/config/markers.ts +327 -327
- package/src/config/resilient-parser.ts +109 -108
- package/src/config/suggestions.ts +74 -74
- package/src/config/types.ts +199 -0
- package/src/extension/async-notifier.ts +123 -89
- package/src/extension/autonomous-policy.ts +169 -169
- package/src/extension/cross-extension-rpc.ts +104 -104
- package/src/extension/help.ts +47 -47
- package/src/extension/import-index.ts +69 -69
- package/src/extension/management.ts +395 -382
- package/src/extension/notification-router.ts +116 -116
- package/src/extension/notification-sink.ts +51 -51
- package/src/extension/project-init.ts +168 -168
- package/src/extension/register.ts +859 -668
- package/src/extension/registration/artifact-cleanup.ts +15 -15
- package/src/extension/registration/command-utils.ts +54 -54
- package/src/extension/registration/commands.ts +559 -452
- package/src/extension/registration/compaction-guard.ts +125 -125
- package/src/extension/registration/subagent-helpers.ts +102 -102
- package/src/extension/registration/subagent-tools.ts +220 -159
- package/src/extension/registration/team-tool.ts +159 -99
- package/src/extension/registration/viewers.ts +29 -0
- package/src/extension/result-watcher.ts +128 -128
- package/src/extension/run-bundle-schema.ts +89 -89
- package/src/extension/run-export.ts +73 -73
- package/src/extension/run-import.ts +84 -84
- package/src/extension/run-index.ts +94 -94
- package/src/extension/run-maintenance.ts +142 -142
- package/src/extension/session-summary.ts +8 -8
- package/src/extension/team-manager-command.ts +96 -96
- package/src/extension/team-recommendation.ts +188 -188
- package/src/extension/team-tool/api.ts +5 -2
- package/src/extension/team-tool/cancel.ts +224 -209
- package/src/extension/team-tool/config-patch.ts +36 -36
- package/src/extension/team-tool/context.ts +60 -60
- package/src/extension/team-tool/doctor.ts +242 -242
- package/src/extension/team-tool/handle-settings.ts +421 -195
- package/src/extension/team-tool/inspect.ts +41 -41
- package/src/extension/team-tool/lifecycle-actions.ts +139 -139
- package/src/extension/team-tool/parallel-dispatch.ts +156 -156
- package/src/extension/team-tool/plan.ts +19 -19
- package/src/extension/team-tool/respond.ts +112 -111
- package/src/extension/team-tool/run.ts +246 -229
- package/src/extension/team-tool/status.ts +110 -110
- package/src/extension/team-tool-types.ts +13 -13
- package/src/extension/team-tool.ts +344 -344
- package/src/extension/tool-result.ts +16 -16
- package/src/extension/validate-resources.ts +77 -77
- package/src/hooks/registry.ts +61 -61
- package/src/hooks/types.ts +40 -40
- package/src/i18n.ts +184 -184
- package/src/observability/correlation.ts +35 -35
- package/src/observability/event-to-metric.ts +68 -68
- package/src/observability/exporters/adapter.ts +30 -30
- package/src/observability/exporters/otlp-exporter.ts +106 -92
- package/src/observability/exporters/prometheus-exporter.ts +54 -54
- package/src/observability/metric-registry.ts +87 -87
- package/src/observability/metric-retention.ts +54 -54
- package/src/observability/metric-sink.ts +81 -56
- package/src/observability/metrics-primitives.ts +167 -167
- package/src/prompt/prompt-runtime.ts +72 -72
- package/src/runtime/adaptive-plan.ts +338 -0
- package/src/runtime/agent-control.ts +169 -169
- 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/async-runner.ts +153 -153
- package/src/runtime/attention-events.ts +28 -28
- package/src/runtime/auto-resume.ts +100 -100
- package/src/runtime/background-runner.ts +122 -89
- package/src/runtime/cancellation.ts +61 -61
- package/src/runtime/capability-inventory.ts +116 -116
- package/src/runtime/child-pi-pool.ts +68 -0
- package/src/runtime/child-pi.ts +541 -461
- package/src/runtime/code-summary.ts +247 -247
- package/src/runtime/compaction-summary.ts +271 -271
- package/src/runtime/concurrency.ts +58 -58
- package/src/runtime/crash-recovery.ts +317 -301
- package/src/runtime/crew-agent-records.ts +379 -281
- package/src/runtime/crew-agent-runtime.ts +60 -60
- package/src/runtime/cross-extension-rpc.ts +72 -0
- package/src/runtime/custom-tools/irc-tool.ts +201 -201
- package/src/runtime/custom-tools/submit-result-tool.ts +90 -90
- package/src/runtime/deadletter.ts +47 -47
- package/src/runtime/delivery-coordinator.ts +176 -176
- package/src/runtime/delta-conflict.ts +360 -360
- package/src/runtime/diagnostic-export.ts +102 -102
- package/src/runtime/direct-run.ts +35 -35
- package/src/runtime/effectiveness.ts +82 -81
- package/src/runtime/errors/crew-errors.ts +166 -0
- package/src/runtime/event-stream-bridge.ts +92 -92
- package/src/runtime/foreground-control.ts +82 -82
- package/src/runtime/green-contract.ts +46 -46
- package/src/runtime/group-join.ts +234 -106
- package/src/runtime/heartbeat-watcher.ts +145 -124
- package/src/runtime/iteration-hooks.ts +267 -267
- package/src/runtime/live-agent-control.ts +88 -88
- package/src/runtime/live-agent-manager.ts +377 -179
- package/src/runtime/live-control-realtime.ts +36 -36
- package/src/runtime/live-session-runtime.ts +676 -600
- package/src/runtime/loop-gates.ts +129 -129
- package/src/runtime/manifest-cache.ts +263 -263
- package/src/runtime/mcp-proxy.ts +113 -113
- package/src/runtime/metric-parser.ts +40 -40
- package/src/runtime/model-fallback.ts +282 -274
- package/src/runtime/model-resolver.ts +118 -0
- package/src/runtime/output-validator.ts +187 -187
- package/src/runtime/overflow-recovery.ts +175 -175
- package/src/runtime/parallel-research.ts +44 -44
- package/src/runtime/parallel-utils.ts +156 -156
- package/src/runtime/parent-guard.ts +80 -80
- package/src/runtime/phase-progress.ts +217 -217
- package/src/runtime/pi-args.ts +165 -165
- package/src/runtime/pi-json-output.ts +111 -111
- package/src/runtime/pi-spawn.ts +167 -167
- package/src/runtime/policy-engine.ts +79 -79
- package/src/runtime/post-checks.ts +125 -125
- package/src/runtime/post-exit-stdio-guard.ts +86 -86
- package/src/runtime/process-status.ts +97 -73
- package/src/runtime/progress-event-coalescer.ts +43 -43
- package/src/runtime/recovery-recipes.ts +74 -74
- package/src/runtime/retry-executor.ts +81 -81
- package/src/runtime/role-permission.ts +39 -39
- package/src/runtime/run-tracker.ts +99 -0
- package/src/runtime/runtime-policy.ts +21 -0
- package/src/runtime/runtime-resolver.ts +94 -91
- package/src/runtime/scheduler.ts +294 -0
- package/src/runtime/semaphore.ts +131 -131
- package/src/runtime/sensitive-paths.ts +92 -92
- package/src/runtime/session-usage.ts +79 -79
- package/src/runtime/settings-store.ts +103 -0
- package/src/runtime/sidechain-output.ts +29 -29
- package/src/runtime/skill-instructions.ts +222 -222
- package/src/runtime/stale-reconciler.ts +198 -189
- package/src/runtime/streaming-output.ts +47 -0
- package/src/runtime/subagent-manager.ts +404 -400
- package/src/runtime/subprocess-tool-registry.ts +67 -67
- package/src/runtime/task-display.ts +38 -38
- package/src/runtime/task-graph-scheduler.ts +122 -122
- package/src/runtime/task-graph.ts +207 -207
- package/src/runtime/task-output-context.ts +177 -177
- package/src/runtime/task-packet.ts +93 -93
- package/src/runtime/task-quality.ts +207 -207
- package/src/runtime/task-runner/capabilities.ts +78 -78
- package/src/runtime/task-runner/live-executor.ts +131 -113
- package/src/runtime/task-runner/progress.ts +119 -119
- package/src/runtime/task-runner/prompt-builder.ts +139 -139
- package/src/runtime/task-runner/prompt-pipeline.ts +64 -64
- package/src/runtime/task-runner/result-utils.ts +14 -14
- package/src/runtime/task-runner/run-projection.ts +103 -103
- package/src/runtime/task-runner/state-helpers.ts +22 -22
- package/src/runtime/task-runner.ts +469 -459
- package/src/runtime/team-runner.ts +693 -945
- package/src/runtime/usage-tracker.ts +71 -0
- package/src/runtime/worker-heartbeat.ts +21 -21
- package/src/runtime/worker-startup.ts +57 -57
- package/src/runtime/workflow-state.ts +187 -187
- package/src/runtime/yield-handler.ts +190 -190
- package/src/schema/config-schema.ts +172 -168
- package/src/schema/team-tool-schema.ts +126 -126
- package/src/schema/validation-types.ts +151 -148
- package/src/skills/discover-skills.ts +67 -67
- package/src/skills/skill-templates.ts +374 -374
- package/src/state/active-run-registry.ts +227 -191
- package/src/state/artifact-store.ts +130 -129
- package/src/state/atomic-write.ts +262 -195
- package/src/state/blob-store.ts +116 -116
- package/src/state/contracts.ts +111 -111
- package/src/state/event-log-rotation.ts +161 -158
- package/src/state/event-log.ts +383 -303
- package/src/state/event-reconstructor.ts +217 -217
- package/src/state/jsonl-writer.ts +82 -82
- package/src/state/locks.ts +146 -146
- package/src/state/mailbox.ts +446 -405
- package/src/state/state-store.ts +364 -351
- package/src/state/task-claims.ts +44 -44
- package/src/state/types.ts +285 -285
- 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 +116 -116
- package/src/teams/team-config.ts +27 -27
- package/src/teams/team-serializer.ts +38 -38
- package/src/types/diff.d.ts +18 -18
- package/src/ui/agent-management-overlay.ts +144 -144
- package/src/ui/crew-widget.ts +487 -370
- package/src/ui/dashboard-panes/agents-pane.ts +109 -28
- package/src/ui/dashboard-panes/cancellation-pane.ts +42 -42
- package/src/ui/dashboard-panes/capability-pane.ts +59 -59
- package/src/ui/dashboard-panes/health-pane.ts +30 -30
- package/src/ui/dashboard-panes/mailbox-pane.ts +35 -35
- package/src/ui/dashboard-panes/progress-pane.ts +30 -30
- package/src/ui/dashboard-panes/transcript-pane.ts +10 -10
- package/src/ui/heartbeat-aggregator.ts +63 -63
- package/src/ui/keybinding-map.ts +97 -94
- package/src/ui/live-conversation-overlay.ts +152 -0
- package/src/ui/live-run-sidebar.ts +180 -180
- package/src/ui/mascot.ts +442 -442
- package/src/ui/overlays/agent-picker-overlay.ts +57 -57
- package/src/ui/overlays/confirm-overlay.ts +58 -58
- package/src/ui/overlays/mailbox-compose-overlay.ts +144 -144
- package/src/ui/overlays/mailbox-compose-preview.ts +63 -63
- package/src/ui/overlays/mailbox-detail-overlay.ts +122 -122
- package/src/ui/pi-ui-compat.ts +57 -57
- package/src/ui/powerbar-publisher.ts +221 -197
- package/src/ui/render-scheduler.ts +216 -143
- package/src/ui/run-action-dispatcher.ts +118 -118
- package/src/ui/run-dashboard.ts +526 -464
- package/src/ui/run-event-bus.ts +208 -208
- package/src/ui/run-snapshot-cache.ts +826 -777
- package/src/ui/settings-overlay.ts +721 -0
- package/src/ui/snapshot-types.ts +86 -70
- package/src/ui/theme-adapter.ts +190 -190
- package/src/ui/tool-progress-formatter.ts +89 -0
- package/src/ui/transcript-cache.ts +94 -94
- package/src/ui/transcript-viewer.ts +335 -335
- package/src/utils/conflict-detect.ts +662 -0
- package/src/utils/file-coalescer.ts +86 -86
- package/src/utils/frontmatter.ts +68 -68
- package/src/utils/fs-watch.ts +88 -31
- package/src/utils/gh-protocol.ts +479 -0
- package/src/utils/ids.ts +17 -17
- package/src/utils/incremental-reader.ts +104 -104
- package/src/utils/internal-error.ts +6 -6
- package/src/utils/names.ts +27 -27
- package/src/utils/paths.ts +102 -63
- package/src/utils/redaction.ts +44 -44
- package/src/utils/safe-paths.ts +47 -47
- package/src/utils/scan-cache.ts +136 -136
- package/src/utils/sse-parser.ts +134 -134
- package/src/utils/task-name-generator.ts +337 -337
- package/src/utils/timings.ts +33 -33
- package/src/utils/visual.ts +243 -198
- package/src/workflows/discover-workflows.ts +139 -139
- package/src/workflows/validate-workflow.ts +40 -40
- package/src/workflows/workflow-config.ts +26 -26
- package/src/workflows/workflow-serializer.ts +32 -32
- package/src/worktree/branch-freshness.ts +45 -45
- package/src/worktree/cleanup.ts +75 -75
- package/src/worktree/worktree-manager.ts +188 -188
- 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/tsconfig.json +19 -19
- package/workflows/default.workflow.md +30 -30
- package/workflows/fast-fix.workflow.md +23 -23
- package/workflows/implementation.workflow.md +43 -43
- package/workflows/parallel-research.workflow.md +46 -46
- package/workflows/research.workflow.md +22 -22
- package/workflows/review.workflow.md +30 -30
- package/skills/task-packet/SKILL.md +0 -28
- package/skills/verify-evidence/SKILL.md +0 -27
|
@@ -1,777 +1,826 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import * as fs from "node:fs";
|
|
3
|
-
import * as path from "node:path";
|
|
4
|
-
import { readCrewAgents, readCrewAgentsAsync, agentsPath, agentOutputPath } from "../runtime/crew-agent-records.ts";
|
|
5
|
-
import type { CrewAgentRecord } from "../runtime/crew-agent-runtime.ts";
|
|
6
|
-
import { isActiveRunStatus } from "../runtime/process-status.ts";
|
|
7
|
-
import type { TeamEvent } from "../state/event-log.ts";
|
|
8
|
-
import type { MailboxMessageStatus } from "../state/mailbox.ts";
|
|
9
|
-
import { loadRunManifestById, loadRunManifestByIdAsync } from "../state/state-store.ts";
|
|
10
|
-
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
11
|
-
import type { RunSnapshotCache as RunSnapshotCacheBase, RunUiGroupJoin, RunUiMailbox, RunUiProgress, RunUiSnapshot, RunUiUsage } from "./snapshot-types.ts";
|
|
12
|
-
import { runEventBus } from "./run-event-bus.ts";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
interface CacheEntry {
|
|
42
|
-
snapshot: RunUiSnapshot;
|
|
43
|
-
stamps: SnapshotStamps;
|
|
44
|
-
loadedAtMs: number;
|
|
45
|
-
lastAccessMs: number;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export interface RunSnapshotCacheOptions {
|
|
49
|
-
ttlMs?: number;
|
|
50
|
-
maxEntries?: number;
|
|
51
|
-
recentEvents?: number;
|
|
52
|
-
recentOutputLines?: number;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function zeroStamp(): FileStamp {
|
|
56
|
-
return { mtimeMs: 0, size: 0 };
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function stampFile(filePath: string | undefined): FileStamp {
|
|
60
|
-
if (!filePath) return zeroStamp();
|
|
61
|
-
try {
|
|
62
|
-
const stat = fs.statSync(filePath);
|
|
63
|
-
return { mtimeMs: stat.mtimeMs, size: stat.size };
|
|
64
|
-
} catch {
|
|
65
|
-
return zeroStamp();
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async function stampFileAsync(filePath: string | undefined): Promise<FileStamp> {
|
|
70
|
-
if (!filePath) return zeroStamp();
|
|
71
|
-
try {
|
|
72
|
-
const stat = await fs.promises.stat(filePath);
|
|
73
|
-
return { mtimeMs: stat.mtimeMs, size: stat.size };
|
|
74
|
-
} catch {
|
|
75
|
-
return zeroStamp();
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
return [];
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
return
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
return
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
return
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
return
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
const kindCounts = { steer: 0, followUp: 0, response: 0, message: 0 };
|
|
432
|
-
const items =
|
|
433
|
-
try {
|
|
434
|
-
const parsed = JSON.parse(line) as unknown;
|
|
435
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return 0;
|
|
436
|
-
const msg = parsed as { id?: unknown; status?: unknown; kind?: unknown; data?: unknown };
|
|
437
|
-
if (typeof msg.id !== "string" || !isMailboxStatus(msg.status)) return 0;
|
|
438
|
-
if (msg.status !== "acknowledged" && delivery[msg.id] !== "acknowledged") {
|
|
439
|
-
const kind = typeof msg.kind === "string" ? msg.kind : typeof (msg.data as Record<string, unknown>)?.kind === "string" ? (msg.data as Record<string, unknown>).kind as string : undefined;
|
|
440
|
-
if (kind === "steer") kindCounts.steer++;
|
|
441
|
-
else if (kind === "follow-up") kindCounts.followUp++;
|
|
442
|
-
else if (kind === "response") kindCounts.response++;
|
|
443
|
-
else kindCounts.message++;
|
|
444
|
-
return 1;
|
|
445
|
-
}
|
|
446
|
-
return 0;
|
|
447
|
-
} catch {
|
|
448
|
-
return 0;
|
|
449
|
-
}
|
|
450
|
-
}) as number[];
|
|
451
|
-
const count = items.reduce((sum, val) => sum + val, 0);
|
|
452
|
-
return { count, approximate:
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
function
|
|
456
|
-
const
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
};
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
let
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { readCrewAgents, readCrewAgentsAsync, agentsPath, agentOutputPath } from "../runtime/crew-agent-records.ts";
|
|
5
|
+
import type { CrewAgentRecord } from "../runtime/crew-agent-runtime.ts";
|
|
6
|
+
import { isActiveRunStatus } from "../runtime/process-status.ts";
|
|
7
|
+
import type { TeamEvent } from "../state/event-log.ts";
|
|
8
|
+
import type { MailboxMessageStatus } from "../state/mailbox.ts";
|
|
9
|
+
import { loadRunManifestById, loadRunManifestByIdAsync } from "../state/state-store.ts";
|
|
10
|
+
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
11
|
+
import type { RunSnapshotCache as RunSnapshotCacheBase, RunUiGroupJoin, RunUiMailbox, RunUiProgress, RunUiSnapshot, RunUiUsage } from "./snapshot-types.ts";
|
|
12
|
+
import { runEventBus } from "./run-event-bus.ts";
|
|
13
|
+
import { sequencePath } from "../state/event-log.ts";
|
|
14
|
+
|
|
15
|
+
export interface RunSnapshotCache extends RunSnapshotCacheBase {
|
|
16
|
+
preloadStale(runId: string): Promise<RunUiSnapshot | undefined>;
|
|
17
|
+
preloadAllStale(runIds: string[]): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const DEFAULT_TTL_MS = 1500;
|
|
21
|
+
const DEFAULT_MAX_ENTRIES = 24;
|
|
22
|
+
const DEFAULT_RECENT_EVENTS = 20;
|
|
23
|
+
const DEFAULT_RECENT_OUTPUT_LINES = 20;
|
|
24
|
+
const MAX_TAIL_BYTES = 32 * 1024;
|
|
25
|
+
/** Max JSONL lines to tail when reading growing files (events, mailbox). */
|
|
26
|
+
const MAX_TAIL_LINES = 500;
|
|
27
|
+
|
|
28
|
+
interface FileStamp {
|
|
29
|
+
mtimeMs: number;
|
|
30
|
+
size: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface SnapshotStamps {
|
|
34
|
+
manifest: FileStamp;
|
|
35
|
+
tasks: FileStamp;
|
|
36
|
+
agents: FileStamp;
|
|
37
|
+
events: FileStamp;
|
|
38
|
+
mailbox: FileStamp;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface CacheEntry {
|
|
42
|
+
snapshot: RunUiSnapshot;
|
|
43
|
+
stamps: SnapshotStamps;
|
|
44
|
+
loadedAtMs: number;
|
|
45
|
+
lastAccessMs: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface RunSnapshotCacheOptions {
|
|
49
|
+
ttlMs?: number;
|
|
50
|
+
maxEntries?: number;
|
|
51
|
+
recentEvents?: number;
|
|
52
|
+
recentOutputLines?: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function zeroStamp(): FileStamp {
|
|
56
|
+
return { mtimeMs: 0, size: 0 };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function stampFile(filePath: string | undefined): FileStamp {
|
|
60
|
+
if (!filePath) return zeroStamp();
|
|
61
|
+
try {
|
|
62
|
+
const stat = fs.statSync(filePath);
|
|
63
|
+
return { mtimeMs: stat.mtimeMs, size: stat.size };
|
|
64
|
+
} catch {
|
|
65
|
+
return zeroStamp();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function stampFileAsync(filePath: string | undefined): Promise<FileStamp> {
|
|
70
|
+
if (!filePath) return zeroStamp();
|
|
71
|
+
try {
|
|
72
|
+
const stat = await fs.promises.stat(filePath);
|
|
73
|
+
return { mtimeMs: stat.mtimeMs, size: stat.size };
|
|
74
|
+
} catch {
|
|
75
|
+
return zeroStamp();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Sprint-1 / 1.4 — events stamp uses the `.seq` sidecar instead of stat-ing
|
|
81
|
+
* the JSONL itself. The sequence file is a few bytes long and the persisted
|
|
82
|
+
* counter monotonically increases even when the events log gets rotated and
|
|
83
|
+
* shrinks (rotation truncates `size` but keeps `seq` ascending). Encoding the
|
|
84
|
+
* counter into the size field keeps the FileStamp structure unchanged so
|
|
85
|
+
* `sameStamp` continues to work.
|
|
86
|
+
*/
|
|
87
|
+
function eventsStamp(eventsPath: string): FileStamp {
|
|
88
|
+
try {
|
|
89
|
+
const raw = fs.readFileSync(sequencePath(eventsPath), "utf-8");
|
|
90
|
+
const seq = Number.parseInt(raw.trim(), 10);
|
|
91
|
+
if (Number.isFinite(seq) && seq >= 0) return { mtimeMs: 0, size: seq + 1 };
|
|
92
|
+
} catch { /* fall through to legacy stat */ }
|
|
93
|
+
return stampFile(eventsPath);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function eventsStampAsync(eventsPath: string): Promise<FileStamp> {
|
|
97
|
+
try {
|
|
98
|
+
const raw = await fs.promises.readFile(sequencePath(eventsPath), "utf-8");
|
|
99
|
+
const seq = Number.parseInt(raw.trim(), 10);
|
|
100
|
+
if (Number.isFinite(seq) && seq >= 0) return { mtimeMs: 0, size: seq + 1 };
|
|
101
|
+
} catch { /* fall through to legacy stat */ }
|
|
102
|
+
return stampFileAsync(eventsPath);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function combineStamps(stamps: FileStamp[]): FileStamp {
|
|
106
|
+
return stamps.reduce((acc, stamp) => ({ mtimeMs: Math.max(acc.mtimeMs, stamp.mtimeMs), size: acc.size + stamp.size }), zeroStamp());
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function mailboxStamp(manifest: TeamRunManifest): FileStamp {
|
|
110
|
+
const root = path.join(manifest.stateRoot, "mailbox");
|
|
111
|
+
const stamps: FileStamp[] = [
|
|
112
|
+
stampFile(path.join(root, "inbox.jsonl")),
|
|
113
|
+
stampFile(path.join(root, "outbox.jsonl")),
|
|
114
|
+
stampFile(path.join(root, "delivery.json")),
|
|
115
|
+
];
|
|
116
|
+
const tasksRoot = path.join(root, "tasks");
|
|
117
|
+
try {
|
|
118
|
+
for (const entry of fs.readdirSync(tasksRoot, { withFileTypes: true })) {
|
|
119
|
+
if (!entry.isDirectory()) continue;
|
|
120
|
+
stamps.push(stampFile(path.join(tasksRoot, entry.name, "inbox.jsonl")));
|
|
121
|
+
stamps.push(stampFile(path.join(tasksRoot, entry.name, "outbox.jsonl")));
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
// No task mailbox yet.
|
|
125
|
+
}
|
|
126
|
+
return combineStamps(stamps);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function mailboxStampAsync(manifest: TeamRunManifest): Promise<FileStamp> {
|
|
130
|
+
const root = path.join(manifest.stateRoot, "mailbox");
|
|
131
|
+
const stamps: FileStamp[] = [
|
|
132
|
+
await stampFileAsync(path.join(root, "inbox.jsonl")),
|
|
133
|
+
await stampFileAsync(path.join(root, "outbox.jsonl")),
|
|
134
|
+
await stampFileAsync(path.join(root, "delivery.json")),
|
|
135
|
+
];
|
|
136
|
+
const tasksRoot = path.join(root, "tasks");
|
|
137
|
+
try {
|
|
138
|
+
for (const entry of await fs.promises.readdir(tasksRoot, { withFileTypes: true })) {
|
|
139
|
+
if (!entry.isDirectory()) continue;
|
|
140
|
+
stamps.push(await stampFileAsync(path.join(tasksRoot, entry.name, "inbox.jsonl")));
|
|
141
|
+
stamps.push(await stampFileAsync(path.join(tasksRoot, entry.name, "outbox.jsonl")));
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
// No task mailbox yet.
|
|
145
|
+
}
|
|
146
|
+
return combineStamps(stamps);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function safeAgentOutputPath(manifest: TeamRunManifest, agent: CrewAgentRecord): string | undefined {
|
|
150
|
+
try {
|
|
151
|
+
return agentOutputPath(manifest, agent.taskId);
|
|
152
|
+
} catch {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function outputStamp(manifest: TeamRunManifest, agents: CrewAgentRecord[]): FileStamp {
|
|
158
|
+
return combineStamps(agents.map((agent) => stampFile(safeAgentOutputPath(manifest, agent))));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function outputStampAsync(manifest: TeamRunManifest, agents: CrewAgentRecord[]): Promise<FileStamp> {
|
|
162
|
+
return combineStamps(await Promise.all(agents.map((agent) => stampFileAsync(safeAgentOutputPath(manifest, agent)))));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function sameStamp(a: FileStamp, b: FileStamp): boolean {
|
|
166
|
+
return a.mtimeMs === b.mtimeMs && a.size === b.size;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function sameStamps(a: SnapshotStamps, b: SnapshotStamps): boolean {
|
|
170
|
+
return sameStamp(a.manifest, b.manifest)
|
|
171
|
+
&& sameStamp(a.tasks, b.tasks)
|
|
172
|
+
&& sameStamp(a.agents, b.agents)
|
|
173
|
+
&& sameStamp(a.events, b.events)
|
|
174
|
+
&& sameStamp(a.mailbox, b.mailbox);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function readTasks(tasksPath: string): TeamTaskState[] {
|
|
178
|
+
try {
|
|
179
|
+
const parsed = JSON.parse(fs.readFileSync(tasksPath, "utf-8")) as unknown;
|
|
180
|
+
return Array.isArray(parsed) ? (parsed as TeamTaskState[]) : [];
|
|
181
|
+
} catch {
|
|
182
|
+
throw new Error(`Failed to parse tasks at ${tasksPath}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Tail-read JSONL lines from a file, returning parsed objects (limited). */
|
|
187
|
+
function tailJsonlLines<T>(filePath: string, limit: number, parse: (line: string) => T | undefined): T[] {
|
|
188
|
+
if (limit <= 0) return [];
|
|
189
|
+
try {
|
|
190
|
+
const stat = fs.statSync(filePath);
|
|
191
|
+
const bytesToRead = Math.min(stat.size, MAX_TAIL_BYTES);
|
|
192
|
+
const fd = fs.openSync(filePath, "r");
|
|
193
|
+
try {
|
|
194
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
195
|
+
fs.readSync(fd, buffer, 0, bytesToRead, stat.size - bytesToRead);
|
|
196
|
+
const lines = buffer.toString("utf-8").split(/\r?\n/).filter(Boolean);
|
|
197
|
+
return lines.flatMap((line) => {
|
|
198
|
+
const item = parse(line);
|
|
199
|
+
return item ? [item] : [];
|
|
200
|
+
}).slice(-limit);
|
|
201
|
+
} finally {
|
|
202
|
+
fs.closeSync(fd);
|
|
203
|
+
}
|
|
204
|
+
} catch {
|
|
205
|
+
return [];
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Async tail-read JSONL lines from a file, returning parsed objects (limited). */
|
|
210
|
+
async function tailJsonlLinesAsync<T>(filePath: string, limit: number, parse: (line: string) => T | undefined): Promise<T[]> {
|
|
211
|
+
if (limit <= 0) return [];
|
|
212
|
+
try {
|
|
213
|
+
const stat = await fs.promises.stat(filePath);
|
|
214
|
+
const bytesToRead = Math.min(stat.size, MAX_TAIL_BYTES);
|
|
215
|
+
const handle = await fs.promises.open(filePath, "r");
|
|
216
|
+
try {
|
|
217
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
218
|
+
await handle.read(buffer, 0, bytesToRead, stat.size - bytesToRead);
|
|
219
|
+
const lines = buffer.toString("utf-8").split(/\r?\n/).filter(Boolean);
|
|
220
|
+
return lines.flatMap((line) => {
|
|
221
|
+
const item = parse(line);
|
|
222
|
+
return item ? [item] : [];
|
|
223
|
+
}).slice(-limit);
|
|
224
|
+
} finally {
|
|
225
|
+
await handle.close();
|
|
226
|
+
}
|
|
227
|
+
} catch {
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function safeRecentEvents(eventsPath: string, limit: number): TeamEvent[] {
|
|
233
|
+
return tailJsonlLines(eventsPath, limit, (line) => {
|
|
234
|
+
try {
|
|
235
|
+
const parsed = JSON.parse(line) as unknown;
|
|
236
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as TeamEvent) : undefined;
|
|
237
|
+
} catch {
|
|
238
|
+
return undefined;
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function safeRecentEventsAsync(eventsPath: string, limit: number): Promise<TeamEvent[]> {
|
|
244
|
+
return tailJsonlLinesAsync(eventsPath, limit, (line) => {
|
|
245
|
+
try {
|
|
246
|
+
const parsed = JSON.parse(line) as unknown;
|
|
247
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as TeamEvent) : undefined;
|
|
248
|
+
} catch {
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function tailLines(filePath: string, limit: number): string[] {
|
|
255
|
+
if (limit <= 0) return [];
|
|
256
|
+
try {
|
|
257
|
+
const stat = fs.statSync(filePath);
|
|
258
|
+
const bytesToRead = Math.min(stat.size, MAX_TAIL_BYTES);
|
|
259
|
+
const fd = fs.openSync(filePath, "r");
|
|
260
|
+
try {
|
|
261
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
262
|
+
fs.readSync(fd, buffer, 0, bytesToRead, stat.size - bytesToRead);
|
|
263
|
+
return buffer.toString("utf-8").split(/\r?\n/).filter(Boolean).slice(-limit);
|
|
264
|
+
} finally {
|
|
265
|
+
fs.closeSync(fd);
|
|
266
|
+
}
|
|
267
|
+
} catch {
|
|
268
|
+
return [];
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function tailLinesAsync(filePath: string, limit: number): Promise<string[]> {
|
|
273
|
+
if (limit <= 0) return [];
|
|
274
|
+
try {
|
|
275
|
+
const stat = await fs.promises.stat(filePath);
|
|
276
|
+
const bytesToRead = Math.min(stat.size, MAX_TAIL_BYTES);
|
|
277
|
+
const handle = await fs.promises.open(filePath, "r");
|
|
278
|
+
try {
|
|
279
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
280
|
+
await handle.read(buffer, 0, bytesToRead, stat.size - bytesToRead);
|
|
281
|
+
return buffer.toString("utf-8").split(/\r?\n/).filter(Boolean).slice(-limit);
|
|
282
|
+
} finally {
|
|
283
|
+
await handle.close();
|
|
284
|
+
}
|
|
285
|
+
} catch {
|
|
286
|
+
return [];
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function recentOutputLines(manifest: TeamRunManifest, agents: CrewAgentRecord[], limit: number): string[] {
|
|
291
|
+
const fromProgress = agents.flatMap((agent) => agent.progress?.recentOutput ?? []);
|
|
292
|
+
const fromFiles = agents.flatMap((agent) => {
|
|
293
|
+
const outputPath = safeAgentOutputPath(manifest, agent);
|
|
294
|
+
return outputPath ? tailLines(outputPath, limit) : [];
|
|
295
|
+
});
|
|
296
|
+
return [...fromProgress, ...fromFiles].map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean).slice(-limit);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function recentOutputLinesAsync(manifest: TeamRunManifest, agents: CrewAgentRecord[], limit: number): Promise<string[]> {
|
|
300
|
+
const fromProgress = agents.flatMap((agent) => agent.progress?.recentOutput ?? []);
|
|
301
|
+
const fromFilesArrays = await Promise.all(agents.map((agent) => {
|
|
302
|
+
const outputPath = safeAgentOutputPath(manifest, agent);
|
|
303
|
+
return outputPath ? tailLinesAsync(outputPath, limit) : Promise.resolve([]);
|
|
304
|
+
}));
|
|
305
|
+
const fromFiles = fromFilesArrays.flat();
|
|
306
|
+
return [...fromProgress, ...fromFiles].map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean).slice(-limit);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function progressFromTasks(tasks: TeamTaskState[]): RunUiProgress {
|
|
310
|
+
const progress: RunUiProgress = { total: tasks.length, completed: 0, running: 0, failed: 0, queued: 0, waiting: 0, cancelled: 0, skipped: 0 };
|
|
311
|
+
for (const task of tasks) {
|
|
312
|
+
if (task.status === "completed") progress.completed += 1;
|
|
313
|
+
else if (task.status === "running") progress.running += 1;
|
|
314
|
+
else if (task.status === "failed") progress.failed += 1;
|
|
315
|
+
else if (task.status === "queued") progress.queued += 1;
|
|
316
|
+
else if (task.status === "waiting") progress.waiting = (progress.waiting ?? 0) + 1;
|
|
317
|
+
else if (task.status === "cancelled") progress.cancelled = (progress.cancelled ?? 0) + 1;
|
|
318
|
+
else if (task.status === "skipped") progress.skipped = (progress.skipped ?? 0) + 1;
|
|
319
|
+
}
|
|
320
|
+
return progress;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function usageFrom(tasks: TeamTaskState[], agents: CrewAgentRecord[]): RunUiUsage {
|
|
324
|
+
const taskUsage = tasks.reduce((acc, task) => {
|
|
325
|
+
acc.tokensIn += task.usage?.input ?? 0;
|
|
326
|
+
acc.tokensOut += task.usage?.output ?? 0;
|
|
327
|
+
acc.toolUses += task.agentProgress?.toolCount ?? 0;
|
|
328
|
+
return acc;
|
|
329
|
+
}, { tokensIn: 0, tokensOut: 0, toolUses: 0 });
|
|
330
|
+
if (taskUsage.tokensIn || taskUsage.tokensOut || taskUsage.toolUses) return taskUsage;
|
|
331
|
+
return agents.reduce((acc, agent) => {
|
|
332
|
+
acc.tokensIn += agent.usage?.input ?? 0;
|
|
333
|
+
acc.tokensOut += agent.usage?.output ?? agent.progress?.tokens ?? 0;
|
|
334
|
+
acc.toolUses += agent.toolUses ?? agent.progress?.toolCount ?? 0;
|
|
335
|
+
return acc;
|
|
336
|
+
}, { tokensIn: 0, tokensOut: 0, toolUses: 0 });
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function isMailboxStatus(value: unknown): value is MailboxMessageStatus {
|
|
340
|
+
return value === "queued" || value === "delivered" || value === "acknowledged";
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function readDeliveryMessages(filePath: string): Record<string, MailboxMessageStatus> {
|
|
344
|
+
try {
|
|
345
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8")) as unknown;
|
|
346
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
347
|
+
const messages = (parsed as { messages?: unknown }).messages;
|
|
348
|
+
if (!messages || typeof messages !== "object" || Array.isArray(messages)) return {};
|
|
349
|
+
const output: Record<string, MailboxMessageStatus> = {};
|
|
350
|
+
for (const [id, status] of Object.entries(messages)) if (isMailboxStatus(status)) output[id] = status;
|
|
351
|
+
return output;
|
|
352
|
+
} catch {
|
|
353
|
+
return {};
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async function readDeliveryMessagesAsync(filePath: string): Promise<Record<string, MailboxMessageStatus>> {
|
|
358
|
+
try {
|
|
359
|
+
const content = await fs.promises.readFile(filePath, "utf-8");
|
|
360
|
+
const parsed = JSON.parse(content) as unknown;
|
|
361
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
362
|
+
const messages = (parsed as { messages?: unknown }).messages;
|
|
363
|
+
if (!messages || typeof messages !== "object" || Array.isArray(messages)) return {};
|
|
364
|
+
const output: Record<string, MailboxMessageStatus> = {};
|
|
365
|
+
for (const [id, status] of Object.entries(messages)) if (isMailboxStatus(status)) output[id] = status;
|
|
366
|
+
return output;
|
|
367
|
+
} catch {
|
|
368
|
+
return {};
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function readGroupJoinMailbox(filePath: string, delivery: Record<string, MailboxMessageStatus>): RunUiGroupJoin[] {
|
|
373
|
+
return tailJsonlLines(filePath, MAX_TAIL_LINES, (line) => {
|
|
374
|
+
try {
|
|
375
|
+
const parsed = JSON.parse(line) as unknown;
|
|
376
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
|
|
377
|
+
const message = parsed as { id?: unknown; data?: unknown };
|
|
378
|
+
const data = message.data && typeof message.data === "object" && !Array.isArray(message.data) ? message.data as Record<string, unknown> : undefined;
|
|
379
|
+
if (typeof message.id !== "string" || data?.kind !== "group_join" || typeof data.requestId !== "string") return undefined;
|
|
380
|
+
return { requestId: data.requestId, messageId: message.id, partial: data.partial === true, ack: delivery[message.id] === "acknowledged" ? "acknowledged" as const : "pending" as const };
|
|
381
|
+
} catch {
|
|
382
|
+
return undefined;
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async function readGroupJoinMailboxAsync(filePath: string, delivery: Record<string, MailboxMessageStatus>): Promise<RunUiGroupJoin[]> {
|
|
388
|
+
return tailJsonlLinesAsync(filePath, MAX_TAIL_LINES, (line) => {
|
|
389
|
+
try {
|
|
390
|
+
const parsed = JSON.parse(line) as unknown;
|
|
391
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
|
|
392
|
+
const message = parsed as { id?: unknown; data?: unknown };
|
|
393
|
+
const data = message.data && typeof message.data === "object" && !Array.isArray(message.data) ? message.data as Record<string, unknown> : undefined;
|
|
394
|
+
if (typeof message.id !== "string" || data?.kind !== "group_join" || typeof data.requestId !== "string") return undefined;
|
|
395
|
+
return { requestId: data.requestId, messageId: message.id, partial: data.partial === true, ack: delivery[message.id] === "acknowledged" ? "acknowledged" as const : "pending" as const };
|
|
396
|
+
} catch {
|
|
397
|
+
return undefined;
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
interface MailboxCount {
|
|
403
|
+
count: number;
|
|
404
|
+
approximate: boolean;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
interface MailboxKindCount extends MailboxCount {
|
|
408
|
+
steer: number;
|
|
409
|
+
followUp: number;
|
|
410
|
+
response: number;
|
|
411
|
+
message: number;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function tailApproximate(filePath: string): boolean {
|
|
415
|
+
try {
|
|
416
|
+
return fs.statSync(filePath).size > MAX_TAIL_BYTES;
|
|
417
|
+
} catch {
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async function tailApproximateAsync(filePath: string): Promise<boolean> {
|
|
423
|
+
try {
|
|
424
|
+
return (await fs.promises.stat(filePath)).size > MAX_TAIL_BYTES;
|
|
425
|
+
} catch {
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function readMailboxCounts(filePath: string, delivery: Record<string, MailboxMessageStatus>): MailboxKindCount {
|
|
431
|
+
const kindCounts = { steer: 0, followUp: 0, response: 0, message: 0 };
|
|
432
|
+
const items = tailJsonlLines(filePath, MAX_TAIL_LINES, (line) => {
|
|
433
|
+
try {
|
|
434
|
+
const parsed = JSON.parse(line) as unknown;
|
|
435
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return 0;
|
|
436
|
+
const msg = parsed as { id?: unknown; status?: unknown; kind?: unknown; data?: unknown };
|
|
437
|
+
if (typeof msg.id !== "string" || !isMailboxStatus(msg.status)) return 0;
|
|
438
|
+
if (msg.status !== "acknowledged" && delivery[msg.id] !== "acknowledged") {
|
|
439
|
+
const kind = typeof msg.kind === "string" ? msg.kind : typeof (msg.data as Record<string, unknown>)?.kind === "string" ? (msg.data as Record<string, unknown>).kind as string : undefined;
|
|
440
|
+
if (kind === "steer") kindCounts.steer++;
|
|
441
|
+
else if (kind === "follow-up") kindCounts.followUp++;
|
|
442
|
+
else if (kind === "response") kindCounts.response++;
|
|
443
|
+
else kindCounts.message++;
|
|
444
|
+
return 1;
|
|
445
|
+
}
|
|
446
|
+
return 0;
|
|
447
|
+
} catch {
|
|
448
|
+
return 0;
|
|
449
|
+
}
|
|
450
|
+
}) as number[];
|
|
451
|
+
const count = items.reduce((sum, val) => sum + val, 0);
|
|
452
|
+
return { count, approximate: tailApproximate(filePath), steer: kindCounts.steer, followUp: kindCounts.followUp, response: kindCounts.response, message: kindCounts.message };
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async function readMailboxCountsAsync(filePath: string, delivery: Record<string, MailboxMessageStatus>): Promise<MailboxKindCount> {
|
|
456
|
+
const kindCounts = { steer: 0, followUp: 0, response: 0, message: 0 };
|
|
457
|
+
const items = await tailJsonlLinesAsync(filePath, MAX_TAIL_LINES, (line) => {
|
|
458
|
+
try {
|
|
459
|
+
const parsed = JSON.parse(line) as unknown;
|
|
460
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return 0;
|
|
461
|
+
const msg = parsed as { id?: unknown; status?: unknown; kind?: unknown; data?: unknown };
|
|
462
|
+
if (typeof msg.id !== "string" || !isMailboxStatus(msg.status)) return 0;
|
|
463
|
+
if (msg.status !== "acknowledged" && delivery[msg.id] !== "acknowledged") {
|
|
464
|
+
const kind = typeof msg.kind === "string" ? msg.kind : typeof (msg.data as Record<string, unknown>)?.kind === "string" ? (msg.data as Record<string, unknown>).kind as string : undefined;
|
|
465
|
+
if (kind === "steer") kindCounts.steer++;
|
|
466
|
+
else if (kind === "follow-up") kindCounts.followUp++;
|
|
467
|
+
else if (kind === "response") kindCounts.response++;
|
|
468
|
+
else kindCounts.message++;
|
|
469
|
+
return 1;
|
|
470
|
+
}
|
|
471
|
+
return 0;
|
|
472
|
+
} catch {
|
|
473
|
+
return 0;
|
|
474
|
+
}
|
|
475
|
+
}) as number[];
|
|
476
|
+
const count = items.reduce((sum, val) => sum + val, 0);
|
|
477
|
+
return { count, approximate: await tailApproximateAsync(filePath), steer: kindCounts.steer, followUp: kindCounts.followUp, response: kindCounts.response, message: kindCounts.message };
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function groupJoinsFrom(manifest: TeamRunManifest): RunUiGroupJoin[] {
|
|
481
|
+
const root = path.join(manifest.stateRoot, "mailbox");
|
|
482
|
+
const delivery = readDeliveryMessages(path.join(root, "delivery.json"));
|
|
483
|
+
return readGroupJoinMailbox(path.join(root, "outbox.jsonl"), delivery).slice(-5);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async function groupJoinsFromAsync(manifest: TeamRunManifest): Promise<RunUiGroupJoin[]> {
|
|
487
|
+
const root = path.join(manifest.stateRoot, "mailbox");
|
|
488
|
+
const delivery = await readDeliveryMessagesAsync(path.join(root, "delivery.json"));
|
|
489
|
+
return (await readGroupJoinMailboxAsync(path.join(root, "outbox.jsonl"), delivery)).slice(-5);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function mergeKindCounts(a: MailboxKindCount, b: MailboxKindCount): MailboxKindCount {
|
|
493
|
+
return {
|
|
494
|
+
count: a.count + b.count,
|
|
495
|
+
approximate: a.approximate || b.approximate,
|
|
496
|
+
steer: a.steer + b.steer,
|
|
497
|
+
followUp: a.followUp + b.followUp,
|
|
498
|
+
response: a.response + b.response,
|
|
499
|
+
message: a.message + b.message,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function mailboxFrom(manifest: TeamRunManifest, agents: CrewAgentRecord[]): RunUiMailbox {
|
|
504
|
+
const root = path.join(manifest.stateRoot, "mailbox");
|
|
505
|
+
const delivery = readDeliveryMessages(path.join(root, "delivery.json"));
|
|
506
|
+
let inbox = readMailboxCounts(path.join(root, "inbox.jsonl"), delivery);
|
|
507
|
+
let outbox = readMailboxCounts(path.join(root, "outbox.jsonl"), delivery);
|
|
508
|
+
const tasksRoot = path.join(root, "tasks");
|
|
509
|
+
try {
|
|
510
|
+
for (const entry of fs.readdirSync(tasksRoot, { withFileTypes: true })) {
|
|
511
|
+
if (!entry.isDirectory()) continue;
|
|
512
|
+
const taskInbox = readMailboxCounts(path.join(tasksRoot, entry.name, "inbox.jsonl"), delivery);
|
|
513
|
+
const taskOutbox = readMailboxCounts(path.join(tasksRoot, entry.name, "outbox.jsonl"), delivery);
|
|
514
|
+
inbox = mergeKindCounts(inbox, taskInbox);
|
|
515
|
+
outbox = mergeKindCounts(outbox, taskOutbox);
|
|
516
|
+
}
|
|
517
|
+
} catch {
|
|
518
|
+
// No task mailboxes yet.
|
|
519
|
+
}
|
|
520
|
+
const attentionAgents = agents.filter((agent) => agent.progress?.activityState === "needs_attention").length;
|
|
521
|
+
return {
|
|
522
|
+
inboxUnread: inbox.count, outboxPending: outbox.count, needsAttention: inbox.count + attentionAgents, approximate: inbox.approximate || outbox.approximate,
|
|
523
|
+
steerUnread: inbox.steer + outbox.steer, followUpUnread: inbox.followUp + outbox.followUp, responseUnread: inbox.response + outbox.response, messageUnread: inbox.message + outbox.message,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
async function mailboxFromAsync(manifest: TeamRunManifest, agents: CrewAgentRecord[]): Promise<RunUiMailbox> {
|
|
528
|
+
const root = path.join(manifest.stateRoot, "mailbox");
|
|
529
|
+
const delivery = await readDeliveryMessagesAsync(path.join(root, "delivery.json"));
|
|
530
|
+
let inbox = await readMailboxCountsAsync(path.join(root, "inbox.jsonl"), delivery);
|
|
531
|
+
let outbox = await readMailboxCountsAsync(path.join(root, "outbox.jsonl"), delivery);
|
|
532
|
+
const tasksRoot = path.join(root, "tasks");
|
|
533
|
+
try {
|
|
534
|
+
for (const entry of await fs.promises.readdir(tasksRoot, { withFileTypes: true })) {
|
|
535
|
+
if (!entry.isDirectory()) continue;
|
|
536
|
+
const taskInbox = await readMailboxCountsAsync(path.join(tasksRoot, entry.name, "inbox.jsonl"), delivery);
|
|
537
|
+
const taskOutbox = await readMailboxCountsAsync(path.join(tasksRoot, entry.name, "outbox.jsonl"), delivery);
|
|
538
|
+
inbox = mergeKindCounts(inbox, taskInbox);
|
|
539
|
+
outbox = mergeKindCounts(outbox, taskOutbox);
|
|
540
|
+
}
|
|
541
|
+
} catch {
|
|
542
|
+
// No task mailboxes yet.
|
|
543
|
+
}
|
|
544
|
+
const attentionAgents = agents.filter((agent) => agent.progress?.activityState === "needs_attention").length;
|
|
545
|
+
return {
|
|
546
|
+
inboxUnread: inbox.count, outboxPending: outbox.count, needsAttention: inbox.count + attentionAgents, approximate: inbox.approximate || outbox.approximate,
|
|
547
|
+
steerUnread: inbox.steer + outbox.steer, followUpUnread: inbox.followUp + outbox.followUp, responseUnread: inbox.response + outbox.response, messageUnread: inbox.message + outbox.message,
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function cancellationReasonFromEvents(events: TeamEvent[]): string | undefined {
|
|
552
|
+
return [...events].reverse().find((event) => event.type === "run.cancelled" && typeof event.data?.reason === "string")?.data?.reason as string | undefined;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function signatureFor(input: Omit<RunUiSnapshot, "signature" | "fetchedAt" | "sliceSignatures">, stamps: SnapshotStamps): string {
|
|
556
|
+
try {
|
|
557
|
+
const digest = createHash("sha256");
|
|
558
|
+
digest.update(JSON.stringify({
|
|
559
|
+
run: [input.manifest.runId, input.manifest.status, input.manifest.updatedAt, input.manifest.artifacts.length],
|
|
560
|
+
tasks: input.tasks.map((task) => [task.id, task.status, task.startedAt, task.finishedAt, task.agentProgress, task.usage]),
|
|
561
|
+
agents: input.agents.map((agent) => [agent.id, agent.status, agent.startedAt, agent.completedAt, agent.toolUses, agent.progress, agent.usage, agent.model]),
|
|
562
|
+
progress: input.progress,
|
|
563
|
+
usage: input.usage,
|
|
564
|
+
mailbox: input.mailbox,
|
|
565
|
+
groupJoins: input.groupJoins,
|
|
566
|
+
events: input.recentEvents.map((event) => [event.metadata?.seq, event.time, event.type, event.taskId, event.message, event.data?.reason]),
|
|
567
|
+
cancellationReason: input.cancellationReason,
|
|
568
|
+
output: input.recentOutputLines,
|
|
569
|
+
stamps,
|
|
570
|
+
}));
|
|
571
|
+
return digest.digest("hex").slice(0, 16);
|
|
572
|
+
} catch {
|
|
573
|
+
// Circular reference or non-serializable data — fall back to timestamp.
|
|
574
|
+
return String(Date.now());
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* 1.6 / 1.7 — compute one short hash per logical slice of the snapshot so
|
|
580
|
+
* dashboard panes / widget can short-circuit when their slice hasn't moved.
|
|
581
|
+
* The slice contents must mirror what `signatureFor` packs into each branch.
|
|
582
|
+
*/
|
|
583
|
+
function sliceSignaturesFor(input: Omit<RunUiSnapshot, "signature" | "fetchedAt" | "sliceSignatures">): RunUiSnapshot["sliceSignatures"] {
|
|
584
|
+
const hash = (value: unknown): string => {
|
|
585
|
+
try {
|
|
586
|
+
return createHash("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 12);
|
|
587
|
+
} catch {
|
|
588
|
+
return String(Date.now());
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
return {
|
|
592
|
+
tasks: hash(input.tasks.map((task) => [task.id, task.status, task.startedAt, task.finishedAt, task.agentProgress, task.usage])),
|
|
593
|
+
agents: hash(input.agents.map((agent) => [agent.id, agent.status, agent.startedAt, agent.completedAt, agent.toolUses, agent.progress, agent.usage, agent.model])),
|
|
594
|
+
mailbox: hash([input.mailbox, input.groupJoins]),
|
|
595
|
+
progress: hash([input.progress, input.usage, input.cancellationReason]),
|
|
596
|
+
events: hash(input.recentEvents.map((event) => [event.metadata?.seq, event.time, event.type, event.taskId, event.message, event.data?.reason])),
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function stampsFor(manifest: TeamRunManifest, _agents: CrewAgentRecord[]): SnapshotStamps {
|
|
601
|
+
// 1.4: use events sequence file instead of stat-ing the events log directly.
|
|
602
|
+
// 1.5: drop per-agent output.log stamping; rely on event-bus invalidation
|
|
603
|
+
// (`crew.subagent.*` and stream events) and on agents.json mtime which
|
|
604
|
+
// updates whenever crew-agent-records.appendCrewAgentOutput touches the
|
|
605
|
+
// aggregate. Saves O(N) statSync per render tick.
|
|
606
|
+
return {
|
|
607
|
+
manifest: stampFile(path.join(manifest.stateRoot, "manifest.json")),
|
|
608
|
+
tasks: stampFile(manifest.tasksPath),
|
|
609
|
+
agents: stampFile(agentsPath(manifest)),
|
|
610
|
+
events: eventsStamp(manifest.eventsPath),
|
|
611
|
+
mailbox: mailboxStamp(manifest),
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
async function stampsForAsync(manifest: TeamRunManifest, _agents: CrewAgentRecord[]): Promise<SnapshotStamps> {
|
|
616
|
+
const [manifestStamp, tasksStamp, agentsStamp, eventsStampValue, mailbox] = await Promise.all([
|
|
617
|
+
stampFileAsync(path.join(manifest.stateRoot, "manifest.json")),
|
|
618
|
+
stampFileAsync(manifest.tasksPath),
|
|
619
|
+
stampFileAsync(agentsPath(manifest)),
|
|
620
|
+
eventsStampAsync(manifest.eventsPath),
|
|
621
|
+
mailboxStampAsync(manifest),
|
|
622
|
+
]);
|
|
623
|
+
return { manifest: manifestStamp, tasks: tasksStamp, agents: agentsStamp, events: eventsStampValue, mailbox };
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
export function createRunSnapshotCache(cwd: string, options: RunSnapshotCacheOptions = {}): RunSnapshotCache {
|
|
627
|
+
const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
628
|
+
const maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
629
|
+
const recentEventsLimit = options.recentEvents ?? DEFAULT_RECENT_EVENTS;
|
|
630
|
+
const recentOutputLimit = options.recentOutputLines ?? DEFAULT_RECENT_OUTPUT_LINES;
|
|
631
|
+
const entries = new Map<string, CacheEntry>();
|
|
632
|
+
|
|
633
|
+
function touch(runId: string, entry: CacheEntry): RunUiSnapshot {
|
|
634
|
+
entry.lastAccessMs = Date.now();
|
|
635
|
+
if (entries.has(runId)) {
|
|
636
|
+
entries.delete(runId);
|
|
637
|
+
entries.set(runId, entry);
|
|
638
|
+
}
|
|
639
|
+
return entry.snapshot;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function evictIfNeeded(): void {
|
|
643
|
+
while (entries.size > maxEntries) {
|
|
644
|
+
const oldestInactive = [...entries.entries()].find(([, entry]) => !isActiveRunStatus(entry.snapshot.manifest.status));
|
|
645
|
+
const key = oldestInactive?.[0] ?? entries.keys().next().value;
|
|
646
|
+
if (!key) break;
|
|
647
|
+
entries.delete(key);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function build(runId: string, previous?: CacheEntry): CacheEntry {
|
|
652
|
+
let loaded: ReturnType<typeof loadRunManifestById>;
|
|
653
|
+
try {
|
|
654
|
+
loaded = loadRunManifestById(cwd, runId);
|
|
655
|
+
} catch {
|
|
656
|
+
if (previous) return previous;
|
|
657
|
+
throw new Error(`Run '${runId}' could not be parsed.`);
|
|
658
|
+
}
|
|
659
|
+
if (!loaded) {
|
|
660
|
+
if (previous) return previous;
|
|
661
|
+
throw new Error(`Run '${runId}' not found.`);
|
|
662
|
+
}
|
|
663
|
+
let tasks: TeamTaskState[];
|
|
664
|
+
let agents: CrewAgentRecord[];
|
|
665
|
+
try {
|
|
666
|
+
tasks = readTasks(loaded.manifest.tasksPath);
|
|
667
|
+
agents = readCrewAgents(loaded.manifest);
|
|
668
|
+
} catch {
|
|
669
|
+
if (previous) return previous;
|
|
670
|
+
throw new Error(`Run '${runId}' could not be parsed.`);
|
|
671
|
+
}
|
|
672
|
+
const mailbox = mailboxFrom(loaded.manifest, agents);
|
|
673
|
+
const groupJoins = groupJoinsFrom(loaded.manifest);
|
|
674
|
+
const recentEvents = safeRecentEvents(loaded.manifest.eventsPath, recentEventsLimit);
|
|
675
|
+
const base = {
|
|
676
|
+
runId: loaded.manifest.runId,
|
|
677
|
+
cwd: loaded.manifest.cwd,
|
|
678
|
+
manifest: loaded.manifest,
|
|
679
|
+
tasks,
|
|
680
|
+
agents,
|
|
681
|
+
progress: progressFromTasks(tasks),
|
|
682
|
+
usage: usageFrom(tasks, agents),
|
|
683
|
+
mailbox,
|
|
684
|
+
groupJoins,
|
|
685
|
+
cancellationReason: cancellationReasonFromEvents(recentEvents),
|
|
686
|
+
recentEvents,
|
|
687
|
+
recentOutputLines: recentOutputLines(loaded.manifest, agents, recentOutputLimit),
|
|
688
|
+
};
|
|
689
|
+
const stamps = stampsFor(loaded.manifest, agents);
|
|
690
|
+
const snapshot: RunUiSnapshot = { ...base, fetchedAt: Date.now(), signature: signatureFor(base, stamps), sliceSignatures: sliceSignaturesFor(base) };
|
|
691
|
+
return { snapshot, stamps, loadedAtMs: snapshot.fetchedAt, lastAccessMs: snapshot.fetchedAt };
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
async function buildAsync(runId: string, previous?: CacheEntry): Promise<CacheEntry> {
|
|
695
|
+
let loaded: Awaited<ReturnType<typeof loadRunManifestByIdAsync>>;
|
|
696
|
+
try {
|
|
697
|
+
loaded = await loadRunManifestByIdAsync(cwd, runId);
|
|
698
|
+
} catch {
|
|
699
|
+
if (previous) return previous;
|
|
700
|
+
throw new Error(`Run '${runId}' could not be parsed.`);
|
|
701
|
+
}
|
|
702
|
+
if (!loaded) {
|
|
703
|
+
if (previous) return previous;
|
|
704
|
+
throw new Error(`Run '${runId}' not found.`);
|
|
705
|
+
}
|
|
706
|
+
let tasks: TeamTaskState[];
|
|
707
|
+
let agents: CrewAgentRecord[];
|
|
708
|
+
try {
|
|
709
|
+
tasks = loaded.tasks;
|
|
710
|
+
agents = await readCrewAgentsAsync(loaded.manifest);
|
|
711
|
+
} catch {
|
|
712
|
+
if (previous) return previous;
|
|
713
|
+
throw new Error(`Run '${runId}' could not be parsed.`);
|
|
714
|
+
}
|
|
715
|
+
const [mailbox, groupJoins, recentEvents, recentOutput] = await Promise.all([
|
|
716
|
+
mailboxFromAsync(loaded.manifest, agents),
|
|
717
|
+
groupJoinsFromAsync(loaded.manifest),
|
|
718
|
+
safeRecentEventsAsync(loaded.manifest.eventsPath, recentEventsLimit),
|
|
719
|
+
recentOutputLinesAsync(loaded.manifest, agents, recentOutputLimit),
|
|
720
|
+
]);
|
|
721
|
+
const base = {
|
|
722
|
+
runId: loaded.manifest.runId,
|
|
723
|
+
cwd: loaded.manifest.cwd,
|
|
724
|
+
manifest: loaded.manifest,
|
|
725
|
+
tasks,
|
|
726
|
+
agents,
|
|
727
|
+
progress: progressFromTasks(tasks),
|
|
728
|
+
usage: usageFrom(tasks, agents),
|
|
729
|
+
mailbox,
|
|
730
|
+
groupJoins,
|
|
731
|
+
cancellationReason: cancellationReasonFromEvents(recentEvents),
|
|
732
|
+
recentEvents,
|
|
733
|
+
recentOutputLines: recentOutput,
|
|
734
|
+
};
|
|
735
|
+
const stamps = await stampsForAsync(loaded.manifest, agents);
|
|
736
|
+
const snapshot: RunUiSnapshot = { ...base, fetchedAt: Date.now(), signature: signatureFor(base, stamps), sliceSignatures: sliceSignaturesFor(base) };
|
|
737
|
+
return { snapshot, stamps, loadedAtMs: snapshot.fetchedAt, lastAccessMs: snapshot.fetchedAt };
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function currentStamps(previous: CacheEntry): SnapshotStamps {
|
|
741
|
+
const manifest = previous.snapshot.manifest;
|
|
742
|
+
return {
|
|
743
|
+
manifest: stampFile(path.join(manifest.stateRoot, "manifest.json")),
|
|
744
|
+
tasks: stampFile(manifest.tasksPath),
|
|
745
|
+
agents: stampFile(agentsPath(manifest)),
|
|
746
|
+
events: eventsStamp(manifest.eventsPath),
|
|
747
|
+
mailbox: mailboxStamp(manifest),
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
async function currentStampsAsync(previous: CacheEntry): Promise<SnapshotStamps> {
|
|
752
|
+
return stampsForAsync(previous.snapshot.manifest, previous.snapshot.agents);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
async function preloadStale(runId: string): Promise<RunUiSnapshot | undefined> {
|
|
756
|
+
const previous = entries.get(runId);
|
|
757
|
+
const now = Date.now();
|
|
758
|
+
// Fresh enough? Return immediately
|
|
759
|
+
if (previous && now - previous.loadedAtMs < ttlMs) {
|
|
760
|
+
return touch(runId, previous);
|
|
761
|
+
}
|
|
762
|
+
// Check stamps async
|
|
763
|
+
if (previous) {
|
|
764
|
+
const stamps = await currentStampsAsync(previous);
|
|
765
|
+
if (sameStamps(stamps, previous.stamps)) {
|
|
766
|
+
previous.loadedAtMs = now;
|
|
767
|
+
return touch(runId, previous);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
// Full async build
|
|
771
|
+
const entry = await buildAsync(runId, previous);
|
|
772
|
+
entries.set(runId, entry);
|
|
773
|
+
evictIfNeeded();
|
|
774
|
+
return entry.snapshot;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
async function preloadAllStale(runIds: string[]): Promise<void> {
|
|
778
|
+
const batchSize = 4;
|
|
779
|
+
for (let i = 0; i < runIds.length; i += batchSize) {
|
|
780
|
+
const batch = runIds.slice(i, i + batchSize);
|
|
781
|
+
await Promise.all(batch.map((id) => preloadStale(id)));
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
const unsubscribe = runEventBus.onAny((event) => {
|
|
786
|
+
if (entries.has(event.runId)) {
|
|
787
|
+
entries.delete(event.runId);
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
return {
|
|
792
|
+
get(runId: string): RunUiSnapshot | undefined {
|
|
793
|
+
const entry = entries.get(runId);
|
|
794
|
+
return entry ? touch(runId, entry) : undefined;
|
|
795
|
+
},
|
|
796
|
+
refresh(runId: string): RunUiSnapshot {
|
|
797
|
+
const previous = entries.get(runId);
|
|
798
|
+
const entry = build(runId, previous);
|
|
799
|
+
entries.set(runId, entry);
|
|
800
|
+
evictIfNeeded();
|
|
801
|
+
return entry.snapshot;
|
|
802
|
+
},
|
|
803
|
+
refreshIfStale(runId: string): RunUiSnapshot {
|
|
804
|
+
const previous = entries.get(runId);
|
|
805
|
+
if (!previous) return this.refresh(runId);
|
|
806
|
+
const now = Date.now();
|
|
807
|
+
if (now - previous.loadedAtMs < ttlMs) return touch(runId, previous);
|
|
808
|
+
const stamps = currentStamps(previous);
|
|
809
|
+
if (sameStamps(stamps, previous.stamps)) return touch(runId, previous);
|
|
810
|
+
return this.refresh(runId);
|
|
811
|
+
},
|
|
812
|
+
preloadStale,
|
|
813
|
+
preloadAllStale,
|
|
814
|
+
invalidate(runId?: string): void {
|
|
815
|
+
if (runId) entries.delete(runId);
|
|
816
|
+
else entries.clear();
|
|
817
|
+
},
|
|
818
|
+
snapshotsByKey(): Map<string, RunUiSnapshot> {
|
|
819
|
+
return new Map([...entries.entries()].map(([key, entry]) => [key, entry.snapshot]));
|
|
820
|
+
},
|
|
821
|
+
dispose(): void {
|
|
822
|
+
unsubscribe();
|
|
823
|
+
entries.clear();
|
|
824
|
+
},
|
|
825
|
+
};
|
|
826
|
+
}
|