pi-crew 0.9.41 → 0.9.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +253 -0
- package/README.md +14 -0
- package/dist/build-meta.json +6450 -12419
- package/dist/index.mjs +57998 -68417
- package/dist/index.mjs.map +4 -4
- package/docs/migration/atomic-write-v2-migration.md +2 -0
- package/docs/superpowers/plans/2026-06-09-fallow-patterns-adoption.md +2 -0
- package/package.json +4 -2
- package/scripts/README.md +6 -0
- package/scripts/build-bundle.mjs +2 -1
- package/scripts/check-all-skills.ts +3 -3
- package/scripts/dev-runner.mjs +106 -0
- package/scripts/postinstall.mjs +6 -2
- package/scripts/verify-skill.ts +2 -2
- package/src/agents/agent-config.ts +1 -1
- package/src/config/config.ts +20 -1
- package/src/config/drift-detector.ts +97 -15
- package/src/config/resilient-parser.ts +4 -4
- package/src/config/types.ts +0 -2
- package/src/extension/crew-cleanup.ts +17 -3
- package/src/extension/crew-vibes/config.ts +1 -1
- package/src/extension/crew-vibes/font-detect.ts +16 -3
- package/src/extension/crew-vibes/index.ts +1 -1
- package/src/extension/cross-extension-rpc.ts +26 -9
- package/src/extension/management.ts +6 -5
- package/src/extension/register.ts +58 -1573
- package/src/extension/registration/command-registration.ts +57 -0
- package/src/extension/registration/commands.ts +7 -2
- package/src/extension/registration/context-builder.ts +131 -0
- package/src/extension/registration/crash-recovery-cache.ts +54 -0
- package/src/extension/registration/foreground-run-controller.ts +238 -0
- package/src/extension/registration/hook-registration.ts +119 -0
- package/src/extension/registration/lazy-configurers.ts +124 -0
- package/src/extension/registration/lifecycle-handlers.ts +731 -0
- package/src/extension/registration/observability.ts +33 -4
- package/src/extension/registration/registration-types.ts +149 -0
- package/src/extension/registration/runtime-cleanup.ts +230 -0
- package/src/extension/registration/subagent-manager-setup.ts +211 -0
- package/src/extension/registration/tool-registration.ts +50 -0
- package/src/extension/registration/wire-cross-extension.ts +37 -0
- package/src/extension/rpc-hmac.ts +21 -1
- package/src/extension/run-import.ts +3 -3
- package/src/extension/team-tool/auto-summarize.ts +4 -4
- package/src/extension/team-tool/cancel.ts +5 -5
- package/src/extension/team-tool/chain-executor.ts +5 -4
- package/src/extension/team-tool/context.ts +11 -1
- package/src/extension/team-tool/doctor.ts +2 -1
- package/src/extension/team-tool/goal-wrap.ts +2 -2
- package/src/extension/team-tool/goal.ts +1 -1
- package/src/extension/team-tool/handle-schedule.ts +25 -36
- package/src/extension/team-tool/orchestrate.ts +0 -1
- package/src/extension/team-tool/parallel-dispatch.ts +5 -0
- package/src/extension/team-tool/run.ts +220 -214
- package/src/extension/team-tool/workflow-manage.ts +1 -1
- package/src/extension/team-tool.ts +69 -16
- package/src/hooks/registry.ts +7 -2
- package/src/observability/correlation.ts +4 -3
- package/src/observability/event-bus.ts +10 -1
- package/src/observability/event-to-metric.ts +8 -7
- package/src/observability/exporters/adapter.ts +6 -9
- package/src/observability/exporters/otlp-exporter.ts +81 -0
- package/src/observability/metric-retention.ts +4 -0
- package/src/observability/metric-sink.ts +14 -4
- package/src/prompt/prompt-runtime.ts +15 -0
- package/src/runtime/adaptive-plan.ts +1 -1
- package/src/runtime/async-runner.ts +13 -4
- package/src/runtime/background-runner.ts +32 -23
- package/src/runtime/capability-inventory.ts +1 -1
- package/src/runtime/chain-runner.ts +5 -5
- package/src/runtime/checkpoint.ts +6 -16
- package/src/runtime/child-pi-constants.ts +42 -0
- package/src/runtime/child-pi-kill.ts +180 -0
- package/src/runtime/child-pi-spawn.ts +234 -0
- package/src/runtime/child-pi-steering.ts +128 -0
- package/src/runtime/child-pi-streams.ts +296 -0
- package/src/runtime/child-pi-transcript.ts +169 -0
- package/src/runtime/child-pi.ts +80 -822
- package/src/runtime/coalesce-tasks.ts +1 -1
- package/src/runtime/compact-stages/tail-capture-stage.ts +1 -1
- package/src/runtime/crash-recovery.ts +63 -47
- package/src/runtime/delivery-coordinator.ts +1 -1
- package/src/runtime/deterministic-ast.ts +2 -2
- package/src/runtime/diagnostic-export.ts +2 -1
- package/src/runtime/dwf-state-store.ts +5 -0
- package/src/runtime/dynamic-workflow-context.ts +78 -18
- package/src/runtime/dynamic-workflow-runner.ts +18 -2
- package/src/runtime/foreground-control.ts +3 -2
- package/src/runtime/goal-evaluator.ts +59 -27
- package/src/runtime/goal-loop-runner.ts +46 -13
- package/src/runtime/goal-state-store.ts +1 -1
- package/src/runtime/handoff-manager.ts +0 -2
- package/src/runtime/intercom-bridge.ts +0 -2
- package/src/runtime/live-agent-manager.ts +30 -5
- package/src/runtime/live-extension-bridge.ts +0 -2
- package/src/runtime/live-session-runtime.ts +65 -57
- package/src/runtime/manifest-cache.ts +59 -0
- package/src/runtime/mcp-proxy.ts +1 -1
- package/src/runtime/orphan-worker-registry.ts +3 -3
- package/src/runtime/output-validator.ts +1 -1
- package/src/runtime/path-overlap.ts +1 -1
- package/src/runtime/pi-args.ts +3 -3
- package/src/runtime/pi-json-output.ts +4 -3
- package/src/runtime/pi-spawn.ts +7 -1
- package/src/runtime/pipeline-runner.ts +1 -4
- package/src/runtime/post-checks.ts +0 -1
- package/src/runtime/retry-runner.ts +1 -1
- package/src/runtime/run-coalesced-task-group.ts +18 -23
- package/src/runtime/scheduler.ts +2 -0
- package/src/runtime/session-resources.ts +1 -1
- package/src/runtime/settings-store.ts +31 -1
- package/src/runtime/skill-effectiveness.ts +2 -2
- package/src/runtime/stale-reconciler.ts +23 -53
- package/src/runtime/subagent-manager.ts +19 -3
- package/src/runtime/task-output-context.ts +2 -1
- package/src/runtime/task-runner/prompt-builder.ts +3 -3
- package/src/runtime/task-runner/run-projection.ts +3 -1
- package/src/runtime/task-runner/state-helpers.ts +13 -1
- package/src/runtime/task-runner.ts +31 -6
- package/src/runtime/team-runner.ts +8 -2
- package/src/runtime/verification-gates.ts +54 -5
- package/src/schema/config-schema.ts +19 -1
- package/src/schema/team-tool-schema.ts +2 -0
- package/src/skills/discover-skills.ts +25 -15
- package/src/state/active-run-registry.ts +0 -1
- package/src/state/atomic-write.ts +37 -11
- package/src/state/blob-store.ts +3 -9
- package/src/state/contracts.ts +5 -1
- package/src/state/crew-init.ts +3 -2
- package/src/state/decision-ledger.ts +2 -2
- package/src/state/event-log-rotation.ts +1 -1
- package/src/state/event-log.ts +4 -3
- package/src/state/gitignore-manager.ts +2 -1
- package/src/state/health-store.ts +5 -2
- package/src/state/instinct-store.ts +11 -2
- package/src/state/locks.ts +132 -49
- package/src/state/mailbox.ts +3 -4
- package/src/state/observation-store.ts +1 -13
- package/src/state/run-cache.ts +12 -12
- package/src/state/run-graph.ts +2 -1
- package/src/state/run-metrics.ts +1 -1
- package/src/state/schedule.ts +7 -1
- package/src/state/state-store.ts +9 -3
- package/src/state/tiered-eval.ts +1 -1
- package/src/state/worker-atomic-writer.ts +1 -0
- package/src/ui/dynamic-border.ts +13 -3
- package/src/ui/live-conversation-overlay.ts +7 -27
- package/src/ui/live-duration.ts +2 -2
- package/src/ui/live-run-sidebar.ts +56 -32
- package/src/ui/mascot.ts +19 -13
- package/src/ui/powerbar-publisher.ts +244 -178
- package/src/ui/render-coalescer.ts +44 -3
- package/src/ui/render-diff.ts +2 -2
- package/src/ui/run-dashboard.ts +122 -21
- package/src/ui/run-event-bus.ts +66 -0
- package/src/ui/run-snapshot-cache.ts +84 -81
- package/src/ui/settings-overlay.ts +3 -3
- package/src/ui/status-colors.ts +24 -0
- package/src/ui/tool-progress-formatter.ts +1 -1
- package/src/ui/tool-render.ts +3 -3
- package/src/ui/transcript-cache.ts +5 -10
- package/src/ui/transcript-entries.ts +68 -72
- package/src/ui/transcript-viewer.ts +54 -14
- package/src/ui/widget/index.ts +26 -15
- package/src/ui/widget/widget-model.ts +18 -9
- package/src/ui/widget/widget-renderer.ts +2 -5
- package/src/utils/fingerprint.ts +1 -1
- package/src/utils/paths.ts +1 -1
- package/src/utils/project-detector.ts +1 -1
- package/src/worktree/worktree-manager.ts +47 -19
- package/src/state/atomic-write-v2.ts +0 -108
|
@@ -3,26 +3,67 @@
|
|
|
3
3
|
* Prevents UI flicker when many events arrive in quick succession.
|
|
4
4
|
* Inspired by oh-my-pi's PROGRESS_COALESCE_MS (150ms) pattern.
|
|
5
5
|
*/
|
|
6
|
+
import { logInternalError } from "../utils/internal-error.ts";
|
|
7
|
+
|
|
8
|
+
export interface RenderCoalescerOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Invoked once per coalesce window with the number of `request()` calls
|
|
11
|
+
* that were collapsed into the most recent scheduled render. Defaults to
|
|
12
|
+
* a debug-scoped `logInternalError` so dropped bursts surface in logs
|
|
13
|
+
* (gated behind PI_TEAMS_DEBUG) without callers having to wire one up.
|
|
14
|
+
*/
|
|
15
|
+
onDrop?: (droppedCount: number) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
6
18
|
export class RenderCoalescer {
|
|
7
19
|
#pending = false;
|
|
8
20
|
#timerId: ReturnType<typeof setTimeout> | null = null;
|
|
9
21
|
#callback: () => void;
|
|
10
22
|
#intervalMs: number;
|
|
23
|
+
#onDrop: (droppedCount: number) => void;
|
|
24
|
+
#dropped = 0;
|
|
11
25
|
|
|
12
|
-
constructor(callback: () => void, intervalMs = 32) {
|
|
26
|
+
constructor(callback: () => void, intervalMs = 32, options: RenderCoalescerOptions = {}) {
|
|
13
27
|
this.#callback = callback;
|
|
14
28
|
this.#intervalMs = intervalMs;
|
|
29
|
+
this.#onDrop =
|
|
30
|
+
options.onDrop ??
|
|
31
|
+
((count) => {
|
|
32
|
+
logInternalError(
|
|
33
|
+
"render-coalescer",
|
|
34
|
+
new Error(`coalesced ${count} dropped request(s) into a single render`),
|
|
35
|
+
undefined,
|
|
36
|
+
"debug",
|
|
37
|
+
);
|
|
38
|
+
});
|
|
15
39
|
}
|
|
16
40
|
|
|
17
41
|
/** Request a render. Will be coalesced with other requests within the interval. */
|
|
18
42
|
request(): void {
|
|
19
|
-
if (this.#pending)
|
|
43
|
+
if (this.#pending) {
|
|
44
|
+
// A render is already scheduled — coalesce this request and bump
|
|
45
|
+
// the drop counter so the scheduled flush can report how many
|
|
46
|
+
// bursts were collapsed (useful for diagnosing UI lag).
|
|
47
|
+
this.#dropped += 1;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
20
50
|
this.#pending = true;
|
|
21
|
-
|
|
51
|
+
const timer = setTimeout(() => {
|
|
22
52
|
this.#pending = false;
|
|
23
53
|
this.#timerId = null;
|
|
54
|
+
const dropped = this.#dropped;
|
|
55
|
+
this.#dropped = 0;
|
|
56
|
+
if (dropped > 0) {
|
|
57
|
+
try {
|
|
58
|
+
this.#onDrop(dropped);
|
|
59
|
+
} catch {
|
|
60
|
+
/* drop callback errors are non-fatal */
|
|
61
|
+
}
|
|
62
|
+
}
|
|
24
63
|
this.#callback();
|
|
25
64
|
}, this.#intervalMs);
|
|
65
|
+
timer.unref();
|
|
66
|
+
this.#timerId = timer;
|
|
26
67
|
}
|
|
27
68
|
|
|
28
69
|
/** Force an immediate render, bypassing coalescing. */
|
package/src/ui/render-diff.ts
CHANGED
|
@@ -108,7 +108,7 @@ export function renderDiff(diffText: string, options: RenderDiffOptions = {}): s
|
|
|
108
108
|
const removedLines: DiffLineContent[] = [];
|
|
109
109
|
while (i < lines.length) {
|
|
110
110
|
const nextParsed = parseDiffLine(lines[i] ?? "");
|
|
111
|
-
if (
|
|
111
|
+
if (nextParsed?.prefix !== "-") break;
|
|
112
112
|
removedLines.push({
|
|
113
113
|
lineNum: nextParsed.lineNum,
|
|
114
114
|
content: nextParsed.content,
|
|
@@ -119,7 +119,7 @@ export function renderDiff(diffText: string, options: RenderDiffOptions = {}): s
|
|
|
119
119
|
const addedLines: DiffLineContent[] = [];
|
|
120
120
|
while (i < lines.length) {
|
|
121
121
|
const nextParsed = parseDiffLine(lines[i] ?? "");
|
|
122
|
-
if (
|
|
122
|
+
if (nextParsed?.prefix !== "+") break;
|
|
123
123
|
addedLines.push({
|
|
124
124
|
lineNum: nextParsed.lineNum,
|
|
125
125
|
content: nextParsed.content,
|
package/src/ui/run-dashboard.ts
CHANGED
|
@@ -21,7 +21,8 @@ import { DynamicCrewBorder } from "./dynamic-border.ts";
|
|
|
21
21
|
import { dashboardActionForKey } from "./keybinding-map.ts";
|
|
22
22
|
import { Box, Text } from "./layout-primitives.ts";
|
|
23
23
|
import { HelpOverlay } from "./overlays/help-overlay.ts";
|
|
24
|
-
import {
|
|
24
|
+
import { RenderScheduler } from "./render-scheduler.ts";
|
|
25
|
+
import { runEventBusAsRenderScheduler } from "./run-event-bus.ts";
|
|
25
26
|
import type { RunSnapshotCache, RunUiSnapshot } from "./snapshot-types.ts";
|
|
26
27
|
import { spinnerBucket, spinnerFrame } from "./spinner.ts";
|
|
27
28
|
import { applyStatusColor, colorizeStatusGlyphs, iconForStatus, type RunStatus } from "./status-colors.ts";
|
|
@@ -105,6 +106,17 @@ const TASK_READ_TTL_MS = 1000;
|
|
|
105
106
|
/** Max run rows rendered in the dashboard run-list block (L-1 window budget). */
|
|
106
107
|
const RUN_LIST_MAX = 8;
|
|
107
108
|
|
|
109
|
+
/**
|
|
110
|
+
* 1.10 (UI-P1-2) — short TTL for the buildSignature cache. `buildSignature`
|
|
111
|
+
* iterates every run and calls `snapshotFor → refreshIfStale`, which can
|
|
112
|
+
* stat multiple files per run when the TTL expires. Rendering fires several
|
|
113
|
+
* times per second (spinner + event-bus), so we cache the computed signature
|
|
114
|
+
* for 100ms: enough to coalesce one render burst without holding the cache
|
|
115
|
+
* across genuinely new data (>100ms idle means a fresh signature will be
|
|
116
|
+
* computed on the next render anyway).
|
|
117
|
+
*/
|
|
118
|
+
const SIGNATURE_CACHE_TTL_MS = 100;
|
|
119
|
+
|
|
108
120
|
/**
|
|
109
121
|
* F-4 — a live run's snapshot is considered "possibly stale" once its
|
|
110
122
|
* `fetchedAt` is this old. A progressing run rebuilds on every stamp change,
|
|
@@ -155,7 +167,17 @@ function renderLines(lines: string[], width: number): string[] {
|
|
|
155
167
|
return box.render(width);
|
|
156
168
|
}
|
|
157
169
|
|
|
158
|
-
function readProgressPreview(run: TeamRunManifest, maxLines = 5): string[] {
|
|
170
|
+
function readProgressPreview(run: TeamRunManifest, maxLines = 5, snapshotCache?: RunSnapshotCache): string[] {
|
|
171
|
+
// P0-6: prefer the snapshot's `recentOutputLines` (no disk I/O) over reading the
|
|
172
|
+
// progress artifact on every render. The progress artifact content is captured
|
|
173
|
+
// into the snapshot's recent events / output pipeline upstream.
|
|
174
|
+
const snapshot = snapshotFor(run, snapshotCache);
|
|
175
|
+
if (snapshot?.recentOutputLines?.length) {
|
|
176
|
+
return ["Progress:", ...snapshot.recentOutputLines.slice(0, maxLines)];
|
|
177
|
+
}
|
|
178
|
+
if (snapshotCache) return ["Progress: (loading…)"];
|
|
179
|
+
// Legacy fallback: tests/dev paths without a snapshot cache still read the
|
|
180
|
+
// progress artifact directly so existing assertions keep working.
|
|
159
181
|
const progress = [...run.artifacts].reverse().find((artifact) => artifact.kind === "progress");
|
|
160
182
|
if (!progress) return ["Progress: (none)"];
|
|
161
183
|
try {
|
|
@@ -192,6 +214,10 @@ function snapshotFor(run: TeamRunManifest, snapshotCache?: RunSnapshotCache): Ru
|
|
|
192
214
|
function readRunTasks(run: TeamRunManifest, snapshotCache?: RunSnapshotCache): TeamTaskState[] {
|
|
193
215
|
const snapshot = snapshotFor(run, snapshotCache);
|
|
194
216
|
if (snapshot) return snapshot.tasks;
|
|
217
|
+
// P0-6: when a snapshot cache is provided but hasn't populated yet, return
|
|
218
|
+
// empty (the render loop falls back to the empty pane placeholder) instead
|
|
219
|
+
// of reading the tasks.json file synchronously every render tick.
|
|
220
|
+
if (snapshotCache) return [];
|
|
195
221
|
const parse = () => {
|
|
196
222
|
if (!fs.existsSync(run.tasksPath)) return [];
|
|
197
223
|
const parsed = JSON.parse(fs.readFileSync(run.tasksPath, "utf-8"));
|
|
@@ -248,7 +274,11 @@ function agentPreviewLine(agent: CrewAgentRecord, task: TeamTaskState | undefine
|
|
|
248
274
|
function readAgentPreview(run: TeamRunManifest, maxLines = 5, options: RunDashboardOptions = {}): string[] {
|
|
249
275
|
try {
|
|
250
276
|
const snapshot = snapshotFor(run, options.snapshotCache);
|
|
251
|
-
|
|
277
|
+
// P0-6: when a snapshot cache is provided but hasn't populated yet, return
|
|
278
|
+
// the empty-pane placeholder instead of calling `readCrewAgents` (disk I/O)
|
|
279
|
+
// on every render tick. Legacy callers (no cache) keep the disk-read path
|
|
280
|
+
// so existing unit tests continue to assert against concrete agent data.
|
|
281
|
+
const agents = snapshot?.agents ?? (options.snapshotCache ? [] : readCrewAgents(run));
|
|
252
282
|
const tasks = snapshot?.tasks ?? readRunTasks(run, options.snapshotCache);
|
|
253
283
|
if (!agents.length) return ["Agents: (none)"];
|
|
254
284
|
const totals = tasks.reduce(
|
|
@@ -283,6 +313,10 @@ function readAgentPreview(run: TeamRunManifest, maxLines = 5, options: RunDashbo
|
|
|
283
313
|
function agentsFor(run: TeamRunManifest, snapshotCache?: RunSnapshotCache): CrewAgentRecord[] {
|
|
284
314
|
const snapshot = snapshotFor(run, snapshotCache);
|
|
285
315
|
if (snapshot) return snapshot.agents;
|
|
316
|
+
// P0-6: when a snapshot cache is provided but hasn't populated yet, return
|
|
317
|
+
// empty (callers handle the empty-state placeholder) instead of calling
|
|
318
|
+
// `readCrewAgents` synchronously on every render tick.
|
|
319
|
+
if (snapshotCache) return [];
|
|
286
320
|
try {
|
|
287
321
|
return readCrewAgents(run);
|
|
288
322
|
} catch {
|
|
@@ -387,8 +421,12 @@ export class RunDashboard implements DashboardComponent {
|
|
|
387
421
|
private cachedWidth = 0;
|
|
388
422
|
private cachedVersion = "";
|
|
389
423
|
private cachedLines: string[] = [];
|
|
424
|
+
/** 1.10 (UI-P1-2) — short-TTL cache for `buildSignature()` so per-run
|
|
425
|
+
* `snapshotFor → refreshIfStale` calls don't fire on every render tick. */
|
|
426
|
+
private cachedSignatureAt = 0;
|
|
427
|
+
private cachedSignature = "";
|
|
390
428
|
private readonly unsubscribeTheme: () => void;
|
|
391
|
-
private readonly
|
|
429
|
+
private readonly renderScheduler: RenderScheduler | undefined;
|
|
392
430
|
|
|
393
431
|
constructor(
|
|
394
432
|
runs: TeamRunManifest[],
|
|
@@ -405,17 +443,61 @@ export class RunDashboard implements DashboardComponent {
|
|
|
405
443
|
this.done = done;
|
|
406
444
|
this.theme = asCrewTheme(theme);
|
|
407
445
|
this.options = options;
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
446
|
+
// 1.10 (UI-P1-1): route run:state / worker:lifecycle / ui:invalidate
|
|
447
|
+
// through a RenderScheduler (debounce + fallback) instead of registering
|
|
448
|
+
// 3 raw runEventBus subscriptions that each schedule an immediate
|
|
449
|
+
// invalidate+requestRender. With 3 overlays subscribing independently,
|
|
450
|
+
// a single event triggered up to 9 callbacks and ~150 invalidates/sec.
|
|
451
|
+
// The scheduler keeps the per-channel subscriptions but collapses them
|
|
452
|
+
// into one debounced render + one coalesced invalidate per runId.
|
|
453
|
+
//
|
|
454
|
+
// FIND-07: scheduler must be constructed BEFORE the theme subscription
|
|
455
|
+
// and the input-handler paths so they can route their invalidate+render
|
|
456
|
+
// calls through `this.scheduleRender()` (debounced + coalesced) instead
|
|
457
|
+
// of bypassing it with direct `invalidateAndRender()` calls. Each direct
|
|
458
|
+
// call would otherwise skip the scheduler's debounce window and force
|
|
459
|
+
// a synchronous repaint, recreating the burst-storm that this scheduler
|
|
460
|
+
// was introduced to eliminate.
|
|
461
|
+
const renderTick = (): void => {
|
|
462
|
+
this.invalidateAndRender();
|
|
463
|
+
};
|
|
464
|
+
this.renderScheduler = new RenderScheduler(
|
|
465
|
+
runEventBusAsRenderScheduler(["run:state", "worker:lifecycle", "ui:invalidate"]),
|
|
466
|
+
renderTick,
|
|
467
|
+
{
|
|
468
|
+
debounceMs: 75,
|
|
469
|
+
fallbackMs: 750,
|
|
470
|
+
events: ["run:state", "worker:lifecycle", "ui:invalidate"],
|
|
471
|
+
onInvalidate: () => {
|
|
472
|
+
// Drop any cached signature — data underneath may have
|
|
473
|
+
// changed so the next buildSignature() needs to recompute.
|
|
474
|
+
this.cachedSignature = "";
|
|
475
|
+
this.cachedSignatureAt = 0;
|
|
476
|
+
},
|
|
477
|
+
},
|
|
478
|
+
);
|
|
479
|
+
// FIND-07: route theme changes through the scheduler so they coalesce
|
|
480
|
+
// with run:state / worker:lifecycle / ui:invalidate bursts instead of
|
|
481
|
+
// each firing their own immediate invalidate+requestRender.
|
|
482
|
+
this.unsubscribeTheme = subscribeThemeChange(theme, () => this.scheduleRender());
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* FIND-07: route an external invalidation through the RenderScheduler.
|
|
487
|
+
*
|
|
488
|
+
* Use this for any caller-driven invalidation (theme change, input
|
|
489
|
+
* handler action) instead of calling `invalidateAndRender()` directly.
|
|
490
|
+
* The scheduler debounces + coalesces these so a theme change arriving
|
|
491
|
+
* in the middle of a run:state burst doesn't force a separate synchronous
|
|
492
|
+
* repaint. Falls back to a direct render if the scheduler was not created
|
|
493
|
+
* (defensive — currently always created in the constructor).
|
|
494
|
+
*/
|
|
495
|
+
private scheduleRender(): void {
|
|
496
|
+
if (this.renderScheduler) {
|
|
497
|
+
this.renderScheduler.schedule();
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
this.invalidateAndRender();
|
|
419
501
|
}
|
|
420
502
|
|
|
421
503
|
/**
|
|
@@ -489,6 +571,17 @@ export class RunDashboard implements DashboardComponent {
|
|
|
489
571
|
}
|
|
490
572
|
|
|
491
573
|
private buildSignature(): string {
|
|
574
|
+
// 1.10 (UI-P1-2) — short-TTL cache so we don't re-read every run's
|
|
575
|
+
// snapshot on every render tick. `snapshotFor → refreshIfStale` can
|
|
576
|
+
// stat multiple files per run when its own TTL expires, and the
|
|
577
|
+
// dashboard's render is called several times per second (spinner +
|
|
578
|
+
// event-bus). Cache the full computed signature for ~100ms: long
|
|
579
|
+
// enough to coalesce one render burst, short enough that idle pauses
|
|
580
|
+
// still pick up new data promptly.
|
|
581
|
+
const now = Date.now();
|
|
582
|
+
if (this.cachedSignature && now - this.cachedSignatureAt < SIGNATURE_CACHE_TTL_MS) {
|
|
583
|
+
return this.cachedSignature;
|
|
584
|
+
}
|
|
492
585
|
let hasRunning = false;
|
|
493
586
|
const statuses = this.runs
|
|
494
587
|
.map((run) => {
|
|
@@ -503,17 +596,25 @@ export class RunDashboard implements DashboardComponent {
|
|
|
503
596
|
.join("|");
|
|
504
597
|
const metricsSig =
|
|
505
598
|
this.activePane === "metrics" ? `:metrics=${this.options.registry?.snapshot().length ?? 0}:${spinnerBucket()}` : "";
|
|
506
|
-
|
|
599
|
+
const sig = `${this.selected}:${this.showHelp ? 1 : 0}:${this.showFullProgress ? 1 : 0}:${this.activePane}:${statuses}${hasRunning ? `:spin=${spinnerBucket()}` : ""}${metricsSig}`;
|
|
600
|
+
this.cachedSignature = sig;
|
|
601
|
+
this.cachedSignatureAt = now;
|
|
602
|
+
return sig;
|
|
507
603
|
}
|
|
508
604
|
|
|
509
605
|
invalidate(): void {
|
|
510
606
|
this.cachedVersion = "";
|
|
511
607
|
this.cachedLines = [];
|
|
608
|
+
// 1.10 (UI-P1-2): also drop the short-TTL signature cache so the next
|
|
609
|
+
// render can't return a stale snapshot-derived signature that was
|
|
610
|
+
// computed before the invalidating event arrived.
|
|
611
|
+
this.cachedSignature = "";
|
|
612
|
+
this.cachedSignatureAt = 0;
|
|
512
613
|
}
|
|
513
614
|
|
|
514
615
|
dispose(): void {
|
|
515
616
|
this.unsubscribeTheme();
|
|
516
|
-
this.
|
|
617
|
+
this.renderScheduler?.dispose();
|
|
517
618
|
}
|
|
518
619
|
|
|
519
620
|
private selectedRunId(): string | undefined {
|
|
@@ -644,7 +745,7 @@ export class RunDashboard implements DashboardComponent {
|
|
|
644
745
|
}),
|
|
645
746
|
)
|
|
646
747
|
: safeRenderPane("transcript", () => renderTranscriptPane(snap))
|
|
647
|
-
: [...readAgentPreview(r, 4, this.options), ...readProgressPreview(r, 2)];
|
|
748
|
+
: [...readAgentPreview(r, 4, this.options), ...readProgressPreview(r, 2, this.options.snapshotCache)];
|
|
648
749
|
const filteredPane = paneLines.filter((l) => l && !l.includes("(none)") && l.trim() !== "");
|
|
649
750
|
if (filteredPane.length > 0) {
|
|
650
751
|
lines.push(row(fg("dim", `── ${this.activePane} ──`)));
|
|
@@ -719,12 +820,12 @@ export class RunDashboard implements DashboardComponent {
|
|
|
719
820
|
// (including Esc) just dismisses it first instead of acting.
|
|
720
821
|
if (action === "help") {
|
|
721
822
|
this.showHelp = !this.showHelp;
|
|
722
|
-
this.
|
|
823
|
+
this.scheduleRender();
|
|
723
824
|
return;
|
|
724
825
|
}
|
|
725
826
|
if (this.showHelp) {
|
|
726
827
|
this.showHelp = false;
|
|
727
|
-
this.
|
|
828
|
+
this.scheduleRender();
|
|
728
829
|
return;
|
|
729
830
|
}
|
|
730
831
|
const selectedRunId = this.selectedRunId();
|
|
@@ -786,7 +887,7 @@ export class RunDashboard implements DashboardComponent {
|
|
|
786
887
|
}
|
|
787
888
|
if (action) {
|
|
788
889
|
lastActivePane = this.activePane;
|
|
789
|
-
this.
|
|
890
|
+
this.scheduleRender();
|
|
790
891
|
}
|
|
791
892
|
}
|
|
792
893
|
}
|
package/src/ui/run-event-bus.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { TeamEvent } from "../state/event-log.ts";
|
|
2
2
|
import { readEventsCursor } from "../state/event-log.ts";
|
|
3
|
+
import type { RenderSchedulerEventBus } from "./render-scheduler.ts";
|
|
3
4
|
|
|
4
5
|
export type RunEventType =
|
|
5
6
|
| "task_started"
|
|
@@ -80,6 +81,14 @@ class RunEventBus {
|
|
|
80
81
|
#globalListeners = new Set<RunEventCallback>();
|
|
81
82
|
#channelListeners = new Map<EventChannel, Set<RunEventCallback>>();
|
|
82
83
|
#channelRunListeners = new Map<string, Map<EventChannel, Set<RunEventCallback>>>();
|
|
84
|
+
// 1.10 (FIND-13): microtask-batched listener fan-out. Events emitted in
|
|
85
|
+
// the same synchronous tick are coalesced into a single microtask flush
|
|
86
|
+
// instead of one synchronous fan-out per emit. Reduces listener dispatch
|
|
87
|
+
// overhead when many events arrive back-to-back (e.g. from a single
|
|
88
|
+
// appendEvent chain that emits several derived events).
|
|
89
|
+
#emitQueue: RunEventPayload[] = [];
|
|
90
|
+
#flushScheduled = false;
|
|
91
|
+
#disposed = false;
|
|
83
92
|
|
|
84
93
|
on(runId: string, callback: RunEventCallback): () => void {
|
|
85
94
|
const listeners = this.#listeners.get(runId) ?? new Set();
|
|
@@ -202,12 +211,41 @@ class RunEventBus {
|
|
|
202
211
|
}
|
|
203
212
|
|
|
204
213
|
emit(event: RunEventPayload): void {
|
|
214
|
+
if (this.#disposed) return;
|
|
205
215
|
// Auto-classify channel if not already set.
|
|
206
216
|
// M2: Use local variable for routing, but also set on event
|
|
207
217
|
// for subscriber API contract (listeners read event.channel).
|
|
208
218
|
if (!event.channel) {
|
|
209
219
|
(event as { channel?: EventChannel }).channel = classifyEventChannel(event.type);
|
|
210
220
|
}
|
|
221
|
+
// 1.10 (FIND-13): queue and defer fan-out to a microtask so multiple
|
|
222
|
+
// emits in the same synchronous tick collapse into one listener-pass
|
|
223
|
+
// each (one per emit, all in the same microtask).
|
|
224
|
+
this.#emitQueue.push(event);
|
|
225
|
+
if (this.#flushScheduled) return;
|
|
226
|
+
this.#flushScheduled = true;
|
|
227
|
+
queueMicrotask(() => {
|
|
228
|
+
this.#flushScheduled = false;
|
|
229
|
+
if (this.#disposed) {
|
|
230
|
+
this.#emitQueue.length = 0;
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
// Swap the queue before dispatching so a listener that re-emits
|
|
234
|
+
// enqueues into a fresh queue (scheduled by its own emit() call)
|
|
235
|
+
// instead of stomping the current iteration.
|
|
236
|
+
const queue = this.#emitQueue;
|
|
237
|
+
this.#emitQueue = [];
|
|
238
|
+
for (const queued of queue) this.#dispatchSync(queued);
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Synchronous per-event listener fan-out. Called from the microtask flush
|
|
244
|
+
* scheduled by emit(); not part of the public API. Mirrors the original
|
|
245
|
+
* (pre-batching) emit body so a single emit still visits every listener
|
|
246
|
+
* registered for the event's runId / channel / global set.
|
|
247
|
+
*/
|
|
248
|
+
#dispatchSync(event: RunEventPayload): void {
|
|
211
249
|
const channel = event.channel!;
|
|
212
250
|
|
|
213
251
|
// Existing: runId-specific listeners
|
|
@@ -262,10 +300,13 @@ class RunEventBus {
|
|
|
262
300
|
|
|
263
301
|
/** Dispose all subscriptions including channel-based ones. */
|
|
264
302
|
dispose(): void {
|
|
303
|
+
this.#disposed = true;
|
|
265
304
|
this.#listeners.clear();
|
|
266
305
|
this.#globalListeners.clear();
|
|
267
306
|
this.#channelListeners.clear();
|
|
268
307
|
this.#channelRunListeners.clear();
|
|
308
|
+
this.#emitQueue.length = 0;
|
|
309
|
+
this.#flushScheduled = false;
|
|
269
310
|
}
|
|
270
311
|
|
|
271
312
|
listenerCount(runId?: string): number {
|
|
@@ -283,6 +324,31 @@ class RunEventBus {
|
|
|
283
324
|
/** Global singleton run event bus for UI-first event delivery. */
|
|
284
325
|
export const runEventBus = new RunEventBus();
|
|
285
326
|
|
|
327
|
+
/**
|
|
328
|
+
* 1.10 (UI-P1-1): adapt `runEventBus.onChannel` to the
|
|
329
|
+
* `RenderSchedulerEventBus` interface so overlays can route their
|
|
330
|
+
* `run:state` / `worker:lifecycle` / `ui:invalidate` subscriptions through
|
|
331
|
+
* a `RenderScheduler` (debounce + fallback + per-runId coalescing) instead of
|
|
332
|
+
* calling `runEventBus.onChannel(...)` directly three times. Without the
|
|
333
|
+
* scheduler, a single event triggers up to 9 callbacks across the 3 overlays
|
|
334
|
+
* (dashboard + sidebar + widget) and bursts into ~150 invalidate+requestRender
|
|
335
|
+
* /sec under load.
|
|
336
|
+
*
|
|
337
|
+
* Returns a fresh adapter whose `on(event, handler)` is a thin shim over
|
|
338
|
+
* `runEventBus.onChannel`. The channels list is closed-over so we can do a
|
|
339
|
+
* defensive membership check without re-reading the channel set on every
|
|
340
|
+
* subscription.
|
|
341
|
+
*/
|
|
342
|
+
export function runEventBusAsRenderScheduler(channels: readonly EventChannel[]): RenderSchedulerEventBus {
|
|
343
|
+
const allowed = new Set<EventChannel>(channels);
|
|
344
|
+
return {
|
|
345
|
+
on: (event: string, handler: (payload: unknown) => void): (() => void) | void => {
|
|
346
|
+
if (!allowed.has(event as EventChannel)) return undefined;
|
|
347
|
+
return runEventBus.onChannel(event as EventChannel, handler);
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
286
352
|
/** Derive a RunEventType from a TeamEvent. */
|
|
287
353
|
export function teamEventToRunEventType(event: TeamEvent): RunEventType | undefined {
|
|
288
354
|
const type = event.type;
|
|
@@ -126,46 +126,33 @@ function combineStamps(stamps: FileStamp[]): FileStamp {
|
|
|
126
126
|
|
|
127
127
|
function mailboxStamp(manifest: TeamRunManifest): FileStamp {
|
|
128
128
|
const root = path.join(manifest.stateRoot, "mailbox");
|
|
129
|
-
const
|
|
129
|
+
const tasksRoot = path.join(root, "tasks");
|
|
130
|
+
// 1.10 (UI-P1-3): replace the O(tasks) per-task inbox/outbox stat loop
|
|
131
|
+
// with a single stat on the tasks dir. The dir mtime changes whenever a
|
|
132
|
+
// task mailbox subdir is created or removed (the dominant cost when a run
|
|
133
|
+
// has many tasks), and per-file content changes are still caught by the
|
|
134
|
+
// top-level inbox/outbox/delivery stats below. The event-bus
|
|
135
|
+
// (`crew.mailbox.*`) also drives invalidation, so the stamp is the
|
|
136
|
+
// fallback poll, not the only source of truth.
|
|
137
|
+
return combineStamps([
|
|
130
138
|
stampFile(path.join(root, "inbox.jsonl")),
|
|
131
139
|
stampFile(path.join(root, "outbox.jsonl")),
|
|
132
140
|
stampFile(path.join(root, "delivery.json")),
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
try {
|
|
136
|
-
for (const entry of fs.readdirSync(tasksRoot, {
|
|
137
|
-
withFileTypes: true,
|
|
138
|
-
})) {
|
|
139
|
-
if (!entry.isDirectory()) continue;
|
|
140
|
-
stamps.push(stampFile(path.join(tasksRoot, entry.name, "inbox.jsonl")));
|
|
141
|
-
stamps.push(stampFile(path.join(tasksRoot, entry.name, "outbox.jsonl")));
|
|
142
|
-
}
|
|
143
|
-
} catch {
|
|
144
|
-
// No task mailbox yet.
|
|
145
|
-
}
|
|
146
|
-
return combineStamps(stamps);
|
|
141
|
+
stampFile(tasksRoot),
|
|
142
|
+
]);
|
|
147
143
|
}
|
|
148
144
|
|
|
149
145
|
async function mailboxStampAsync(manifest: TeamRunManifest): Promise<FileStamp> {
|
|
150
146
|
const root = path.join(manifest.stateRoot, "mailbox");
|
|
151
|
-
const
|
|
147
|
+
const tasksRoot = path.join(root, "tasks");
|
|
148
|
+
// 1.10 (UI-P1-3): see sync `mailboxStamp` above — single tasksRoot stat
|
|
149
|
+
// replaces the O(tasks) per-task loop.
|
|
150
|
+
return combineStamps([
|
|
152
151
|
await stampFileAsync(path.join(root, "inbox.jsonl")),
|
|
153
152
|
await stampFileAsync(path.join(root, "outbox.jsonl")),
|
|
154
153
|
await stampFileAsync(path.join(root, "delivery.json")),
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
try {
|
|
158
|
-
for (const entry of await fs.promises.readdir(tasksRoot, {
|
|
159
|
-
withFileTypes: true,
|
|
160
|
-
})) {
|
|
161
|
-
if (!entry.isDirectory()) continue;
|
|
162
|
-
stamps.push(await stampFileAsync(path.join(tasksRoot, entry.name, "inbox.jsonl")));
|
|
163
|
-
stamps.push(await stampFileAsync(path.join(tasksRoot, entry.name, "outbox.jsonl")));
|
|
164
|
-
}
|
|
165
|
-
} catch {
|
|
166
|
-
// No task mailbox yet.
|
|
167
|
-
}
|
|
168
|
-
return combineStamps(stamps);
|
|
154
|
+
await stampFileAsync(tasksRoot),
|
|
155
|
+
]);
|
|
169
156
|
}
|
|
170
157
|
|
|
171
158
|
function safeAgentOutputPath(manifest: TeamRunManifest, agent: CrewAgentRecord): string | undefined {
|
|
@@ -673,54 +660,20 @@ function cancellationReasonFromEvents(events: TeamEvent[]): string | undefined {
|
|
|
673
660
|
| undefined;
|
|
674
661
|
}
|
|
675
662
|
|
|
676
|
-
|
|
677
|
-
try {
|
|
678
|
-
const digest = createHash("sha256");
|
|
679
|
-
digest.update(
|
|
680
|
-
JSON.stringify({
|
|
681
|
-
run: [input.manifest.runId, input.manifest.status, input.manifest.updatedAt, input.manifest.artifacts.length],
|
|
682
|
-
tasks: input.tasks.map((task) => [task.id, task.status, task.startedAt, task.finishedAt, task.agentProgress, task.usage]),
|
|
683
|
-
agents: input.agents.map((agent) => [
|
|
684
|
-
agent.id,
|
|
685
|
-
agent.status,
|
|
686
|
-
agent.startedAt,
|
|
687
|
-
agent.completedAt,
|
|
688
|
-
agent.toolUses,
|
|
689
|
-
agent.progress,
|
|
690
|
-
agent.usage,
|
|
691
|
-
agent.model,
|
|
692
|
-
]),
|
|
693
|
-
progress: input.progress,
|
|
694
|
-
usage: input.usage,
|
|
695
|
-
mailbox: input.mailbox,
|
|
696
|
-
groupJoins: input.groupJoins,
|
|
697
|
-
events: input.recentEvents.map((event) => [
|
|
698
|
-
event.metadata?.seq,
|
|
699
|
-
event.time,
|
|
700
|
-
event.type,
|
|
701
|
-
event.taskId,
|
|
702
|
-
event.message,
|
|
703
|
-
event.data?.reason,
|
|
704
|
-
]),
|
|
705
|
-
cancellationReason: input.cancellationReason,
|
|
706
|
-
dwfPhaseState: input.dwfPhaseState,
|
|
707
|
-
output: input.recentOutputLines,
|
|
708
|
-
stamps,
|
|
709
|
-
}),
|
|
710
|
-
);
|
|
711
|
-
return digest.digest("hex").slice(0, 16);
|
|
712
|
-
} catch {
|
|
713
|
-
// Circular reference or non-serializable data — fall back to timestamp.
|
|
714
|
-
return String(Date.now());
|
|
715
|
-
}
|
|
716
|
-
}
|
|
663
|
+
type SliceSignatures = NonNullable<RunUiSnapshot["sliceSignatures"]>;
|
|
717
664
|
|
|
718
665
|
/**
|
|
719
|
-
*
|
|
720
|
-
*
|
|
721
|
-
*
|
|
666
|
+
* FIND-08: compute the per-slice short hashes once per build and reuse the
|
|
667
|
+
* result for both the full-snapshot `signature` and the per-pane
|
|
668
|
+
* `sliceSignatures`. Previously, `signatureFor` and `sliceSignaturesFor` each
|
|
669
|
+
* called `JSON.stringify` + `createHash("sha256")` on the tasks, agents, and
|
|
670
|
+
* recentEvents projections independently — twice the work for the slices the
|
|
671
|
+
* two signatures share. The per-slice hashes are embedded into the full
|
|
672
|
+
* signature so the full-snapshot signature still changes whenever any slice
|
|
673
|
+
* changes (collision risk: 1 in 16^16 per build, well within tolerance for
|
|
674
|
+
* cache-invalidation use).
|
|
722
675
|
*/
|
|
723
|
-
function
|
|
676
|
+
function computeSliceSignatures(input: Omit<RunUiSnapshot, "signature" | "fetchedAt" | "sliceSignatures">): SliceSignatures {
|
|
724
677
|
const hash = (value: unknown): string => {
|
|
725
678
|
try {
|
|
726
679
|
return createHash("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 12);
|
|
@@ -757,6 +710,45 @@ function sliceSignaturesFor(input: Omit<RunUiSnapshot, "signature" | "fetchedAt"
|
|
|
757
710
|
};
|
|
758
711
|
}
|
|
759
712
|
|
|
713
|
+
function signatureFor(
|
|
714
|
+
input: Omit<RunUiSnapshot, "signature" | "fetchedAt" | "sliceSignatures">,
|
|
715
|
+
stamps: SnapshotStamps,
|
|
716
|
+
sliceSignatures: SliceSignatures,
|
|
717
|
+
): string {
|
|
718
|
+
try {
|
|
719
|
+
const digest = createHash("sha256");
|
|
720
|
+
digest.update(
|
|
721
|
+
JSON.stringify({
|
|
722
|
+
run: [input.manifest.runId, input.manifest.status, input.manifest.updatedAt, input.manifest.artifacts.length],
|
|
723
|
+
tasks: sliceSignatures.tasks,
|
|
724
|
+
agents: sliceSignatures.agents,
|
|
725
|
+
progress: input.progress,
|
|
726
|
+
usage: input.usage,
|
|
727
|
+
mailbox: input.mailbox,
|
|
728
|
+
groupJoins: input.groupJoins,
|
|
729
|
+
events: sliceSignatures.events,
|
|
730
|
+
cancellationReason: input.cancellationReason,
|
|
731
|
+
dwfPhaseState: input.dwfPhaseState,
|
|
732
|
+
output: input.recentOutputLines,
|
|
733
|
+
stamps,
|
|
734
|
+
}),
|
|
735
|
+
);
|
|
736
|
+
return digest.digest("hex").slice(0, 16);
|
|
737
|
+
} catch {
|
|
738
|
+
// Circular reference or non-serializable data — fall back to timestamp.
|
|
739
|
+
return String(Date.now());
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* 1.6 / 1.7 — compute one short hash per logical slice of the snapshot so
|
|
745
|
+
* dashboard panes / widget can short-circuit when their slice hasn't moved.
|
|
746
|
+
* The slice contents must mirror what `signatureFor` packs into each branch.
|
|
747
|
+
*/
|
|
748
|
+
function sliceSignaturesFor(sliceSignatures: SliceSignatures): SliceSignatures {
|
|
749
|
+
return sliceSignatures;
|
|
750
|
+
}
|
|
751
|
+
|
|
760
752
|
function stampsFor(manifest: TeamRunManifest, _agents: CrewAgentRecord[]): SnapshotStamps {
|
|
761
753
|
// 1.4: use events sequence file instead of stat-ing the events log directly.
|
|
762
754
|
// 1.5: drop per-agent output.log stamping; rely on event-bus invalidation
|
|
@@ -807,8 +799,17 @@ export function createRunSnapshotCache(cwd: string, options: RunSnapshotCacheOpt
|
|
|
807
799
|
|
|
808
800
|
function evictIfNeeded(): void {
|
|
809
801
|
while (entries.size > maxEntries) {
|
|
810
|
-
|
|
811
|
-
|
|
802
|
+
// Map iteration respects insertion order, so the first inactive entry
|
|
803
|
+
// we see IS the oldest inactive entry — no Array.from(...).sort()
|
|
804
|
+
// allocation, no copy of the entries map.
|
|
805
|
+
let key: string | undefined;
|
|
806
|
+
for (const [k, entry] of entries) {
|
|
807
|
+
if (!isActiveRunStatus(entry.snapshot.manifest.status)) {
|
|
808
|
+
key = k;
|
|
809
|
+
break;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
if (!key) key = entries.keys().next().value;
|
|
812
813
|
if (!key) break;
|
|
813
814
|
entries.delete(key);
|
|
814
815
|
}
|
|
@@ -854,11 +855,12 @@ export function createRunSnapshotCache(cwd: string, options: RunSnapshotCacheOpt
|
|
|
854
855
|
recentOutputLines: recentOutputLines(loaded.manifest, agents, recentOutputLimit),
|
|
855
856
|
};
|
|
856
857
|
const stamps = stampsFor(loaded.manifest, agents);
|
|
858
|
+
const sliceSignatures = computeSliceSignatures(base);
|
|
857
859
|
const snapshot: RunUiSnapshot = {
|
|
858
860
|
...base,
|
|
859
861
|
fetchedAt: Date.now(),
|
|
860
|
-
signature: signatureFor(base, stamps),
|
|
861
|
-
sliceSignatures
|
|
862
|
+
signature: signatureFor(base, stamps, sliceSignatures),
|
|
863
|
+
sliceSignatures,
|
|
862
864
|
};
|
|
863
865
|
return {
|
|
864
866
|
snapshot,
|
|
@@ -911,11 +913,12 @@ export function createRunSnapshotCache(cwd: string, options: RunSnapshotCacheOpt
|
|
|
911
913
|
recentOutputLines: recentOutput,
|
|
912
914
|
};
|
|
913
915
|
const stamps = await stampsForAsync(loaded.manifest, agents);
|
|
916
|
+
const sliceSignatures = computeSliceSignatures(base);
|
|
914
917
|
const snapshot: RunUiSnapshot = {
|
|
915
918
|
...base,
|
|
916
919
|
fetchedAt: Date.now(),
|
|
917
|
-
signature: signatureFor(base, stamps),
|
|
918
|
-
sliceSignatures
|
|
920
|
+
signature: signatureFor(base, stamps, sliceSignatures),
|
|
921
|
+
sliceSignatures,
|
|
919
922
|
};
|
|
920
923
|
return {
|
|
921
924
|
snapshot,
|