pi-crew 0.10.2 → 0.10.4
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 +2 -1
- package/CHANGELOG.md +249 -0
- package/README.md +5 -1
- package/dist/index.mjs +10844 -7250
- package/docs/architecture.md +4 -4
- package/docs/commands-reference.md +3 -0
- package/docs/publishing.md +15 -3
- package/install.mjs +90 -39
- package/package.json +9 -3
- package/schema.json +11 -0
- package/scripts/README.md +4 -3
- package/skills/real-test-pi-crew/REPORT-TEMPLATE.md +7 -2
- package/skills/real-test-pi-crew/SKILL.md +428 -82
- package/src/config/config-merge.ts +11 -1
- package/src/config/config-validation.ts +40 -1
- package/src/config/config.ts +28 -6
- package/src/config/defaults.ts +35 -10
- package/src/config/env-vars.ts +27 -2
- package/src/config/migration-validator.ts +113 -0
- package/src/config/types.ts +36 -0
- package/src/extension/cross-extension-rpc.ts +3 -7
- package/src/extension/register.ts +13 -0
- package/src/extension/registration/lifecycle-handlers.ts +40 -9
- package/src/extension/registration/observability.ts +3 -7
- package/src/extension/registration/subagent-tools.ts +3 -7
- package/src/extension/registration/team-tool.ts +56 -12
- package/src/extension/registration/ui.ts +3 -8
- package/src/extension/registration/viewers.ts +3 -10
- package/src/extension/team-manager-command.ts +3 -7
- package/src/extension/team-tool/api/agent-control.ts +17 -10
- package/src/extension/team-tool/api/heartbeat.ts +4 -3
- package/src/extension/team-tool/api/mailbox.ts +33 -20
- package/src/extension/team-tool/api/plan-approval.ts +5 -5
- package/src/extension/team-tool/api/task-claims.ts +8 -7
- package/src/extension/team-tool/cancel.ts +6 -0
- package/src/extension/team-tool/doctor.ts +364 -7
- package/src/extension/team-tool/handle-settings.ts +23 -1
- package/src/extension/team-tool/inspect.ts +10 -2
- package/src/extension/team-tool/run.ts +3 -7
- package/src/extension/team-tool/status.ts +12 -0
- package/src/extension/team-tool.ts +41 -16
- package/src/hooks/registry.ts +62 -56
- package/src/prompt/inbox-poll.ts +90 -0
- package/src/prompt/message-tool.ts +166 -0
- package/src/prompt/prompt-runtime.ts +201 -18
- package/src/prompt/scratchpad-lifecycle.ts +3 -3
- package/src/prompt/surface-worker.ts +720 -0
- package/src/prompt/worker-events-channel.ts +49 -3
- package/src/runtime/async-runner.ts +29 -1
- package/src/runtime/background-runner.ts +43 -42
- package/src/runtime/broker/broker-issuer.ts +27 -2
- package/src/runtime/broker/crew-broker-tokens.ts +56 -4
- package/src/runtime/broker/crew-broker.ts +334 -443
- package/src/runtime/broker/delegate/delegate-event.ts +37 -0
- package/src/runtime/broker/mailbox-observer/mailbox-fanout.ts +59 -0
- package/src/runtime/broker/protocol/connection-state.ts +103 -0
- package/src/runtime/broker/protocol/events-replay.ts +68 -0
- package/src/runtime/broker/protocol/manifest-loader.ts +20 -0
- package/src/runtime/broker/protocol/msg-inbox.ts +69 -0
- package/src/runtime/broker/protocol/request-parsers.ts +175 -0
- package/src/runtime/broker/protocol/wait-auth.ts +46 -0
- package/src/runtime/child-pi/child-pi-spawn.ts +23 -9
- package/src/runtime/child-pi/child-pi-streams.ts +9 -1
- package/src/runtime/child-pi/child-pi.ts +368 -5
- package/src/runtime/crew-agent-records.ts +13 -1
- package/src/runtime/dispatch-batch.ts +12 -1
- package/src/runtime/event-log-tail-source.ts +374 -0
- package/src/runtime/finalize-run.ts +19 -7
- package/src/runtime/foreground-control.ts +19 -6
- package/src/runtime/goal-workflow/dynamic-workflow-context.ts +6 -0
- package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +3 -0
- package/src/runtime/goal-workflow/goal-loop-runner.ts +29 -27
- package/src/runtime/goal-workflow/goal-state-store.ts +3 -0
- package/src/runtime/heartbeat/heartbeat-watcher.ts +3 -3
- package/src/runtime/live-session/live-agent-manager.ts +34 -1
- package/src/runtime/live-session/live-control-realtime.ts +10 -0
- package/src/runtime/live-session/live-session-runtime.ts +47 -27
- package/src/runtime/manifest-cache.ts +128 -17
- package/src/runtime/model/pi-args.ts +59 -65
- package/src/runtime/output/sidechain-output.ts +61 -6
- package/src/runtime/plan-replan.ts +3 -0
- package/src/runtime/process/proc-stat.ts +46 -0
- package/src/runtime/process/zombie-scanner.ts +32 -19
- package/src/runtime/spawn-policy.ts +27 -41
- package/src/runtime/stale-reconciler.ts +28 -3
- package/src/runtime/supervisor-contact.ts +3 -0
- package/src/runtime/surface/degrade.ts +776 -0
- package/src/runtime/surface/herdr-provider.ts +546 -0
- package/src/runtime/surface/launch-script.ts +172 -0
- package/src/runtime/surface/resolve-surface.ts +274 -0
- package/src/runtime/surface/surface-provider.ts +129 -0
- package/src/runtime/surface/surface-spawn.ts +475 -0
- package/src/runtime/surface/tmux-provider.ts +400 -0
- package/src/runtime/task-runner/child-executor.ts +80 -0
- package/src/runtime/task-runner/post-execution.ts +57 -2
- package/src/runtime/task-runner/prompt-builder.ts +1 -0
- package/src/runtime/task-runner/retrieval-orchestrator.ts +191 -56
- package/src/runtime/task-runner/state-helpers.ts +54 -30
- package/src/runtime/task-runner.ts +4 -2
- package/src/runtime/team-runner.ts +104 -3
- package/src/schema/config-schema.ts +24 -0
- package/src/state/atomic-write.ts +219 -40
- package/src/state/coordination/locks.ts +7 -5
- package/src/state/coordination/mailbox.ts +56 -10
- package/src/state/event-log/cursor.ts +413 -23
- package/src/state/event-log/event-log.ts +120 -113
- package/src/state/event-log/sequence-cache.ts +21 -3
- package/src/state/stores/ownership-map.ts +5 -4
- package/src/state/stores/plan-store.ts +12 -0
- package/src/state/stores/state-store.ts +103 -6
- package/src/state/types.ts +51 -0
- package/src/ui/inline-panel/agent-pane.ts +3 -0
- package/src/ui/powerbar-publisher.ts +3 -7
- package/src/ui/render-diff.ts +16 -8
- package/src/ui/run-action-dispatcher.ts +7 -10
- package/src/ui/run-dashboard.ts +87 -42
- package/src/ui/run-event-bus.ts +10 -1
- package/src/ui/run-snapshot-cache.ts +83 -35
- package/src/ui/settings-overlay.ts +4 -1
- package/src/ui/transcript-cache.ts +101 -13
- package/src/ui/transcript-viewer.ts +92 -24
- package/src/ui/widget/index.ts +32 -8
- package/src/utils/visual.ts +43 -0
- package/src/worktree/worktree-manager.ts +65 -4
|
@@ -20,7 +20,7 @@ import { createRequire } from "node:module";
|
|
|
20
20
|
import type { AgentConfig } from "../../agents/agent-config.ts";
|
|
21
21
|
import { loadConfig } from "../../config/config.ts";
|
|
22
22
|
import { effectiveRunConfig } from "../../extension/team-tool/config-patch.ts";
|
|
23
|
-
import {
|
|
23
|
+
import { appendEventBuffered } from "../../state/event-log/event-log.ts";
|
|
24
24
|
import { registerActiveRun, unregisterActiveRun } from "../../state/stores/active-run-registry.ts";
|
|
25
25
|
import { collectRunMetrics } from "../../state/stores/run-metrics.ts";
|
|
26
26
|
import { createRunManifest, saveRunTasks } from "../../state/stores/state-store.ts";
|
|
@@ -121,7 +121,7 @@ export const realGoalEvaluator = async (
|
|
|
121
121
|
const drift = compareSnapshot(snapshot.snapshot, current);
|
|
122
122
|
if (drift.length > 0) {
|
|
123
123
|
verificationCompromised = drift;
|
|
124
|
-
|
|
124
|
+
appendEventBuffered(turnManifest.eventsPath, {
|
|
125
125
|
type: "goal.verification_compromised",
|
|
126
126
|
runId: turnRunId,
|
|
127
127
|
data: {
|
|
@@ -129,7 +129,7 @@ export const realGoalEvaluator = async (
|
|
|
129
129
|
driftedFiles: drift,
|
|
130
130
|
phase: "T_snap",
|
|
131
131
|
},
|
|
132
|
-
});
|
|
132
|
+
}).catch((e) => logInternalError("goal-loop.buffered", e, "type=goal.verification_compromised phase=T_snap"));
|
|
133
133
|
}
|
|
134
134
|
} catch (error) {
|
|
135
135
|
logInternalError("goal-loop.integritySnap", error, `goalId=${goal.goalId} phase=T_snap`);
|
|
@@ -201,7 +201,7 @@ export const realGoalEvaluator = async (
|
|
|
201
201
|
const postDrift = compareSnapshot(snapshot.snapshot, post);
|
|
202
202
|
if (postDrift.length > 0) {
|
|
203
203
|
verificationCompromised = postDrift;
|
|
204
|
-
|
|
204
|
+
appendEventBuffered(turnManifest.eventsPath, {
|
|
205
205
|
type: "goal.verification_compromised",
|
|
206
206
|
runId: turnRunId,
|
|
207
207
|
data: {
|
|
@@ -209,7 +209,9 @@ export const realGoalEvaluator = async (
|
|
|
209
209
|
driftedFiles: postDrift,
|
|
210
210
|
phase: "T_verify_done",
|
|
211
211
|
},
|
|
212
|
-
})
|
|
212
|
+
}).catch((e) =>
|
|
213
|
+
logInternalError("goal-loop.buffered", e, "type=goal.verification_compromised phase=T_verify_done"),
|
|
214
|
+
);
|
|
213
215
|
}
|
|
214
216
|
} catch (error) {
|
|
215
217
|
logInternalError("goal-loop.integritySnap", error, `goalId=${goal.goalId} phase=T_verify_done`);
|
|
@@ -521,7 +523,7 @@ export async function runGoalLoop(input: RunGoalLoopInput): Promise<RunGoalLoopR
|
|
|
521
523
|
const workflow = resolveGoalTurnWorkflow(goal);
|
|
522
524
|
const agents = input.deps.discoverAgents(goal.cwd);
|
|
523
525
|
|
|
524
|
-
|
|
526
|
+
appendEventBuffered(eventsPath, {
|
|
525
527
|
type: "goal.loop_start",
|
|
526
528
|
runId: manifest.runId,
|
|
527
529
|
data: {
|
|
@@ -529,7 +531,7 @@ export async function runGoalLoop(input: RunGoalLoopInput): Promise<RunGoalLoopR
|
|
|
529
531
|
objective: goal.objective,
|
|
530
532
|
maxTurns: goal.maxTurns,
|
|
531
533
|
},
|
|
532
|
-
});
|
|
534
|
+
}).catch((e) => logInternalError("goal-loop.buffered", e, "type=goal.loop_start"));
|
|
533
535
|
|
|
534
536
|
// P1g (RFC v0.5 §P1g, cold-review #2 BLOCKING fix): acquire the workspace lock for the
|
|
535
537
|
// goal's lifetime. This serializes concurrent goals targeting the same cwd (workspaceMode:
|
|
@@ -545,14 +547,14 @@ export async function runGoalLoop(input: RunGoalLoopInput): Promise<RunGoalLoopR
|
|
|
545
547
|
} catch (error) {
|
|
546
548
|
logInternalError("goal-loop.workspaceLock", error, `goalId=${goal.goalId} cwd=${goal.cwd}`);
|
|
547
549
|
goal = safeSetStatus(store, goal.goalId, "blocked", goal, eventsPath);
|
|
548
|
-
|
|
550
|
+
appendEventBuffered(eventsPath, {
|
|
549
551
|
type: "goal.workspace_lock_failed",
|
|
550
552
|
runId: manifest.runId,
|
|
551
553
|
data: {
|
|
552
554
|
goalId: goal.goalId,
|
|
553
555
|
error: error instanceof Error ? error.message : String(error),
|
|
554
556
|
},
|
|
555
|
-
});
|
|
557
|
+
}).catch((e) => logInternalError("goal-loop.buffered", e, "type=goal.workspace_lock_failed"));
|
|
556
558
|
return { manifest, tasks: [], goalState: goal };
|
|
557
559
|
}
|
|
558
560
|
|
|
@@ -570,7 +572,7 @@ export async function runGoalLoop(input: RunGoalLoopInput): Promise<RunGoalLoopR
|
|
|
570
572
|
if (goal.budgetUnlimited !== true && goal.budgetTotal !== undefined && goal.budgetTotal > 0 && goal.budgetAbort !== undefined) {
|
|
571
573
|
if (goal.budgetUsed >= goal.budgetAbort * goal.budgetTotal) {
|
|
572
574
|
goal = safeSetStatus(store, goal.goalId, "budget_exceeded", goal, eventsPath);
|
|
573
|
-
|
|
575
|
+
appendEventBuffered(eventsPath, {
|
|
574
576
|
type: "goal.budget_warning",
|
|
575
577
|
runId: manifest.runId,
|
|
576
578
|
data: {
|
|
@@ -579,11 +581,11 @@ export async function runGoalLoop(input: RunGoalLoopInput): Promise<RunGoalLoopR
|
|
|
579
581
|
budgetTotal: goal.budgetTotal,
|
|
580
582
|
threshold: "abort",
|
|
581
583
|
},
|
|
582
|
-
});
|
|
584
|
+
}).catch((e) => logInternalError("goal-loop.buffered", e, "type=goal.budget_warning threshold=abort"));
|
|
583
585
|
break;
|
|
584
586
|
}
|
|
585
587
|
if (goal.budgetUsed >= (goal.budgetWarning ?? 0.8) * goal.budgetTotal) {
|
|
586
|
-
|
|
588
|
+
appendEventBuffered(eventsPath, {
|
|
587
589
|
type: "goal.budget_warning",
|
|
588
590
|
runId: manifest.runId,
|
|
589
591
|
data: {
|
|
@@ -592,12 +594,12 @@ export async function runGoalLoop(input: RunGoalLoopInput): Promise<RunGoalLoopR
|
|
|
592
594
|
budgetTotal: goal.budgetTotal,
|
|
593
595
|
threshold: "warning",
|
|
594
596
|
},
|
|
595
|
-
});
|
|
597
|
+
}).catch((e) => logInternalError("goal-loop.buffered", e, "type=goal.budget_warning threshold=warning"));
|
|
596
598
|
}
|
|
597
599
|
}
|
|
598
600
|
|
|
599
601
|
const turnIndex = goal.turnsUsed + 1;
|
|
600
|
-
|
|
602
|
+
appendEventBuffered(eventsPath, {
|
|
601
603
|
type: "goal.turn_start",
|
|
602
604
|
runId: manifest.runId,
|
|
603
605
|
data: {
|
|
@@ -605,7 +607,7 @@ export async function runGoalLoop(input: RunGoalLoopInput): Promise<RunGoalLoopR
|
|
|
605
607
|
turn: turnIndex,
|
|
606
608
|
maxTurns: goal.maxTurns,
|
|
607
609
|
},
|
|
608
|
-
});
|
|
610
|
+
}).catch((e) => logInternalError("goal-loop.buffered", e, "type=goal.turn_start"));
|
|
609
611
|
|
|
610
612
|
// ── TURN: fresh manifest per turn (G2) + executeTeamRun ──────────────────
|
|
611
613
|
const turnGoalText = composeGoalPrompt(goal);
|
|
@@ -630,7 +632,7 @@ export async function runGoalLoop(input: RunGoalLoopInput): Promise<RunGoalLoopR
|
|
|
630
632
|
// Fix round-6: re-check state AFTER patching (user may have paused/stopped in the inter-turn gap).
|
|
631
633
|
// Without this, a pause that lands between store.patch and executeTeamRun lets one extra turn run.
|
|
632
634
|
if (goal.state !== "running") {
|
|
633
|
-
|
|
635
|
+
appendEventBuffered(eventsPath, {
|
|
634
636
|
type: "goal.loop_end",
|
|
635
637
|
runId: manifest.runId,
|
|
636
638
|
data: {
|
|
@@ -638,7 +640,7 @@ export async function runGoalLoop(input: RunGoalLoopInput): Promise<RunGoalLoopR
|
|
|
638
640
|
state: goal.state,
|
|
639
641
|
reason: "state changed before turn spawn",
|
|
640
642
|
},
|
|
641
|
-
});
|
|
643
|
+
}).catch((e) => logInternalError("goal-loop.buffered", e, "type=goal.loop_end reason=state_changed"));
|
|
642
644
|
break;
|
|
643
645
|
}
|
|
644
646
|
registerActiveRun(created.manifest);
|
|
@@ -688,7 +690,7 @@ export async function runGoalLoop(input: RunGoalLoopInput): Promise<RunGoalLoopR
|
|
|
688
690
|
const turnStatus = turnResult.manifest.status;
|
|
689
691
|
if (turnStatus === "blocked" || turnStatus === "failed") {
|
|
690
692
|
goal = safeSetStatus(store, goal.goalId, "blocked", goal, eventsPath);
|
|
691
|
-
|
|
693
|
+
appendEventBuffered(eventsPath, {
|
|
692
694
|
type: "goal.turn_terminal_status",
|
|
693
695
|
runId: manifest.runId,
|
|
694
696
|
data: {
|
|
@@ -697,7 +699,7 @@ export async function runGoalLoop(input: RunGoalLoopInput): Promise<RunGoalLoopR
|
|
|
697
699
|
turnRunId: created.manifest.runId,
|
|
698
700
|
turnStatus,
|
|
699
701
|
},
|
|
700
|
-
});
|
|
702
|
+
}).catch((e) => logInternalError("goal-loop.buffered", e, "type=goal.turn_terminal_status"));
|
|
701
703
|
break;
|
|
702
704
|
}
|
|
703
705
|
|
|
@@ -735,7 +737,7 @@ export async function runGoalLoop(input: RunGoalLoopInput): Promise<RunGoalLoopR
|
|
|
735
737
|
eventsPath,
|
|
736
738
|
) ?? goal;
|
|
737
739
|
|
|
738
|
-
|
|
740
|
+
appendEventBuffered(eventsPath, {
|
|
739
741
|
type: "goal.turn_evaluated",
|
|
740
742
|
runId: manifest.runId,
|
|
741
743
|
data: {
|
|
@@ -744,9 +746,9 @@ export async function runGoalLoop(input: RunGoalLoopInput): Promise<RunGoalLoopR
|
|
|
744
746
|
achieved: verdict.achieved,
|
|
745
747
|
reason: verdict.reason,
|
|
746
748
|
},
|
|
747
|
-
});
|
|
749
|
+
}).catch((e) => logInternalError("goal-loop.buffered", e, "type=goal.turn_evaluated"));
|
|
748
750
|
if (!verdict.achieved && goal.nextTurnFeedback) {
|
|
749
|
-
|
|
751
|
+
appendEventBuffered(eventsPath, {
|
|
750
752
|
type: "goal.feedback_steered",
|
|
751
753
|
runId: manifest.runId,
|
|
752
754
|
data: {
|
|
@@ -754,7 +756,7 @@ export async function runGoalLoop(input: RunGoalLoopInput): Promise<RunGoalLoopR
|
|
|
754
756
|
turn: turnIndex,
|
|
755
757
|
feedback: goal.nextTurnFeedback,
|
|
756
758
|
},
|
|
757
|
-
});
|
|
759
|
+
}).catch((e) => logInternalError("goal-loop.buffered", e, "type=goal.feedback_steered"));
|
|
758
760
|
}
|
|
759
761
|
|
|
760
762
|
// ── STOP CONDITIONS (round-7: re-read disk before applying — external cancel/pause wins) ─
|
|
@@ -781,7 +783,7 @@ export async function runGoalLoop(input: RunGoalLoopInput): Promise<RunGoalLoopR
|
|
|
781
783
|
const stuck = store.compareAndSetStatus(goal.goalId, "running", "stuck", eventsPath);
|
|
782
784
|
if (stuck) {
|
|
783
785
|
goal = stuck;
|
|
784
|
-
|
|
786
|
+
appendEventBuffered(eventsPath, {
|
|
785
787
|
type: "goal.stuck",
|
|
786
788
|
runId: manifest.runId,
|
|
787
789
|
data: {
|
|
@@ -789,7 +791,7 @@ export async function runGoalLoop(input: RunGoalLoopInput): Promise<RunGoalLoopR
|
|
|
789
791
|
turn: goal.turnsUsed,
|
|
790
792
|
lastReasons: goal.verdicts.slice(-3).map((v) => v.reason.slice(0, 200)),
|
|
791
793
|
},
|
|
792
|
-
});
|
|
794
|
+
}).catch((e) => logInternalError("goal-loop.buffered", e, "type=goal.stuck"));
|
|
793
795
|
break;
|
|
794
796
|
}
|
|
795
797
|
}
|
|
@@ -811,7 +813,7 @@ export async function runGoalLoop(input: RunGoalLoopInput): Promise<RunGoalLoopR
|
|
|
811
813
|
} catch {
|
|
812
814
|
/* best-effort */
|
|
813
815
|
}
|
|
814
|
-
|
|
816
|
+
appendEventBuffered(eventsPath, {
|
|
815
817
|
type: "goal.loop_end",
|
|
816
818
|
runId: manifest.runId,
|
|
817
819
|
data: {
|
|
@@ -820,7 +822,7 @@ export async function runGoalLoop(input: RunGoalLoopInput): Promise<RunGoalLoopR
|
|
|
820
822
|
turnsUsed: goal.turnsUsed,
|
|
821
823
|
budgetUsed: goal.budgetUsed,
|
|
822
824
|
},
|
|
823
|
-
});
|
|
825
|
+
}).catch((e) => logInternalError("goal-loop.buffered", e, "type=goal.loop_end reason=normal"));
|
|
824
826
|
}
|
|
825
827
|
|
|
826
828
|
return { manifest, tasks: [], goalState: goal };
|
|
@@ -81,6 +81,9 @@ export class GoalStore {
|
|
|
81
81
|
mkdirSync(dirname(path), { recursive: true });
|
|
82
82
|
atomicWriteJson(path, next);
|
|
83
83
|
if (eventsPath) {
|
|
84
|
+
// REVIEW FIX (2026-09-10): reverted M2b buffered conversion —
|
|
85
|
+
// goal.state_changed is read back synchronously by CAS tests and
|
|
86
|
+
// goal status display; low-frequency transition event.
|
|
84
87
|
appendEvent(eventsPath, {
|
|
85
88
|
type: "goal.state_changed",
|
|
86
89
|
runId: state.goalId,
|
|
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { NotificationDescriptor } from "../../extension/notification-router.ts";
|
|
4
4
|
import type { MetricRegistry } from "../../observability/metric-registry.ts";
|
|
5
|
-
import {
|
|
5
|
+
import { appendEventBuffered } from "../../state/event-log/event-log.ts";
|
|
6
6
|
import { loadRunManifestById } from "../../state/stores/state-store.ts";
|
|
7
7
|
import type { TeamRunManifest } from "../../state/types.ts";
|
|
8
8
|
import { logInternalError } from "../../utils/internal-error.ts";
|
|
@@ -175,7 +175,7 @@ export class HeartbeatWatcher {
|
|
|
175
175
|
this.lastLevel.set(key, level);
|
|
176
176
|
if (level === "dead" && previous !== "dead") {
|
|
177
177
|
this.opts.registry.counter("crew.heartbeat.dead_total", "Dead heartbeat detections").inc({ runId: run.runId });
|
|
178
|
-
|
|
178
|
+
appendEventBuffered(loaded.manifest.eventsPath, {
|
|
179
179
|
type: "crew.task.heartbeat_dead",
|
|
180
180
|
runId: run.runId,
|
|
181
181
|
taskId: task.id,
|
|
@@ -183,7 +183,7 @@ export class HeartbeatWatcher {
|
|
|
183
183
|
data: {
|
|
184
184
|
elapsedMs: Number.isFinite(elapsed) ? elapsed : undefined,
|
|
185
185
|
},
|
|
186
|
-
});
|
|
186
|
+
}).catch((e) => logInternalError("heartbeat_watcher.buffered", e, "type=crew.task.heartbeat_dead"));
|
|
187
187
|
// W9 fix — prefix title with short run label (first 8 chars of runId)
|
|
188
188
|
// so ambient notifications are scannable when multiple runs are
|
|
189
189
|
// in flight. Full runId remains in the notification object.
|
|
@@ -119,15 +119,18 @@ export function registerLiveAgent(
|
|
|
119
119
|
const completed = [...liveAgents.entries()].find(([, h]) => h.activity.completedAtMs > 0);
|
|
120
120
|
if (completed) {
|
|
121
121
|
liveAgents.delete(completed[0]);
|
|
122
|
+
invalidateSortedLiveAgents();
|
|
122
123
|
} else {
|
|
123
124
|
const oldestKey = liveAgents.keys().next().value;
|
|
124
125
|
if (oldestKey !== undefined) {
|
|
125
126
|
logInternalError("live-agent-manager.cap", new Error(`liveAgents at cap ${MAX_LIVE_AGENTS}; evicting oldest ${oldestKey}`));
|
|
126
127
|
liveAgents.delete(oldestKey);
|
|
128
|
+
invalidateSortedLiveAgents();
|
|
127
129
|
}
|
|
128
130
|
}
|
|
129
131
|
}
|
|
130
132
|
liveAgents.set(input.agentId, handle);
|
|
133
|
+
invalidateSortedLiveAgents();
|
|
131
134
|
try {
|
|
132
135
|
if (eventLogFn && eventsPath)
|
|
133
136
|
eventLogFn(eventsPath, {
|
|
@@ -172,6 +175,7 @@ export function updateLiveAgentStatus(agentId: string, status: CrewAgentRecord["
|
|
|
172
175
|
if (!handle) return;
|
|
173
176
|
handle.status = status;
|
|
174
177
|
handle.updatedAt = new Date().toISOString();
|
|
178
|
+
invalidateSortedLiveAgents();
|
|
175
179
|
}
|
|
176
180
|
|
|
177
181
|
function safeDisposeLiveSession(handle: LiveAgentHandle): void {
|
|
@@ -198,6 +202,7 @@ export async function terminateLiveAgent(
|
|
|
198
202
|
if (!handle) return undefined;
|
|
199
203
|
handle.status = status;
|
|
200
204
|
handle.updatedAt = new Date().toISOString();
|
|
205
|
+
invalidateSortedLiveAgents();
|
|
201
206
|
try {
|
|
202
207
|
if (eventLogFn && eventsPath)
|
|
203
208
|
eventLogFn(eventsPath, {
|
|
@@ -220,6 +225,7 @@ export async function terminateLiveAgent(
|
|
|
220
225
|
} finally {
|
|
221
226
|
safeDisposeLiveSession(handle);
|
|
222
227
|
liveAgents.delete(handle.agentId); // Move AFTER abort completes to prevent race
|
|
228
|
+
invalidateSortedLiveAgents();
|
|
223
229
|
}
|
|
224
230
|
return handle;
|
|
225
231
|
}
|
|
@@ -260,6 +266,7 @@ export function evictStaleLiveAgentHandles(now = Date.now()): number {
|
|
|
260
266
|
// Terminal handle — evict after grace period
|
|
261
267
|
if (age > STALE_HANDLE_MS) {
|
|
262
268
|
liveAgents.delete(agentId);
|
|
269
|
+
invalidateSortedLiveAgents();
|
|
263
270
|
safeDisposeLiveSession(handle);
|
|
264
271
|
evicted++;
|
|
265
272
|
}
|
|
@@ -289,6 +296,7 @@ export function evictStaleLiveAgentHandles(now = Date.now()): number {
|
|
|
289
296
|
const liveness = checkProcessLiveness(sessionPid);
|
|
290
297
|
if (!liveness.alive) {
|
|
291
298
|
liveAgents.delete(agentId);
|
|
299
|
+
invalidateSortedLiveAgents();
|
|
292
300
|
safeDisposeLiveSession(handle);
|
|
293
301
|
evicted++;
|
|
294
302
|
}
|
|
@@ -297,8 +305,24 @@ export function evictStaleLiveAgentHandles(now = Date.now()): number {
|
|
|
297
305
|
return evicted;
|
|
298
306
|
}
|
|
299
307
|
|
|
308
|
+
// PERF (2026-08-24): listLiveAgents is called 4-6x per render frame and used to
|
|
309
|
+
// copy + sort with ICU localeCompare every call. ISO-8601 strings are fixed-width
|
|
310
|
+
// and sort correctly with plain operators; the sorted array is memoized and
|
|
311
|
+
// invalidated at every map mutation. NOTE: callers receive the SHARED memoized
|
|
312
|
+
// array — never sort or mutate it in place; copy (slice/spread) first.
|
|
313
|
+
let sortedLiveAgents: LiveAgentHandle[] | undefined;
|
|
314
|
+
function invalidateSortedLiveAgents(): void {
|
|
315
|
+
sortedLiveAgents = undefined;
|
|
316
|
+
}
|
|
300
317
|
export function listLiveAgents(): LiveAgentHandle[] {
|
|
301
|
-
|
|
318
|
+
if (!sortedLiveAgents) {
|
|
319
|
+
sortedLiveAgents = [...liveAgents.values()].sort((a, b) => {
|
|
320
|
+
const au = a.updatedAt ?? "";
|
|
321
|
+
const bu = b.updatedAt ?? "";
|
|
322
|
+
return au < bu ? 1 : au > bu ? -1 : 0;
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
return sortedLiveAgents;
|
|
302
326
|
}
|
|
303
327
|
|
|
304
328
|
export function listActiveLiveAgents(): LiveAgentHandle[] {
|
|
@@ -324,6 +348,7 @@ export async function steerLiveAgent(agentIdOrTaskId: string, message: string):
|
|
|
324
348
|
}
|
|
325
349
|
await handle.session.steer(message);
|
|
326
350
|
handle.updatedAt = new Date().toISOString();
|
|
351
|
+
invalidateSortedLiveAgents();
|
|
327
352
|
return handle;
|
|
328
353
|
}
|
|
329
354
|
|
|
@@ -339,6 +364,7 @@ export async function followUpLiveAgent(agentIdOrTaskId: string, prompt: string)
|
|
|
339
364
|
expandPromptTemplates: false,
|
|
340
365
|
});
|
|
341
366
|
handle.updatedAt = new Date().toISOString();
|
|
367
|
+
invalidateSortedLiveAgents();
|
|
342
368
|
return handle;
|
|
343
369
|
}
|
|
344
370
|
|
|
@@ -359,6 +385,7 @@ export async function resumeLiveAgent(agentIdOrTaskId: string, prompt: string):
|
|
|
359
385
|
});
|
|
360
386
|
handle.status = "completed";
|
|
361
387
|
handle.updatedAt = new Date().toISOString();
|
|
388
|
+
invalidateSortedLiveAgents();
|
|
362
389
|
return handle;
|
|
363
390
|
}
|
|
364
391
|
|
|
@@ -377,6 +404,7 @@ export function trackLiveAgentToolStart(agentIdOrTaskId: string, toolName: strin
|
|
|
377
404
|
handle.activity.activeTools.set(toolName, toolName);
|
|
378
405
|
handle.activity.toolUses++;
|
|
379
406
|
handle.updatedAt = new Date().toISOString();
|
|
407
|
+
invalidateSortedLiveAgents();
|
|
380
408
|
}
|
|
381
409
|
|
|
382
410
|
/** G2: Track tool end for a live agent. */
|
|
@@ -394,6 +422,7 @@ export function trackLiveAgentTurnEnd(agentIdOrTaskId: string, compaction = fals
|
|
|
394
422
|
if (compaction) handle.activity.compactionCount++;
|
|
395
423
|
handle.activity.activeTools.clear();
|
|
396
424
|
handle.updatedAt = new Date().toISOString();
|
|
425
|
+
invalidateSortedLiveAgents();
|
|
397
426
|
}
|
|
398
427
|
|
|
399
428
|
/** G2: Track assistant response text. */
|
|
@@ -413,6 +442,7 @@ export function markLiveAgentCompleted(agentIdOrTaskId: string): void {
|
|
|
413
442
|
|
|
414
443
|
export function clearLiveAgentsForTest(): void {
|
|
415
444
|
liveAgents.clear();
|
|
445
|
+
invalidateSortedLiveAgents();
|
|
416
446
|
}
|
|
417
447
|
|
|
418
448
|
/** Phase 7/G4: Send an IRC message to a specific live agent (DM).
|
|
@@ -427,6 +457,7 @@ export function sendIrcMessage(targetAgentId: string, message: IrcMessage): void
|
|
|
427
457
|
}
|
|
428
458
|
handle.pendingMessages.push(message);
|
|
429
459
|
handle.updatedAt = new Date().toISOString();
|
|
460
|
+
invalidateSortedLiveAgents();
|
|
430
461
|
// G4: Try non-blocking delivery via sendCustomMessage
|
|
431
462
|
const session = handle.session as Record<string, unknown>;
|
|
432
463
|
if (typeof session.sendCustomMessage === "function") {
|
|
@@ -467,6 +498,7 @@ export function broadcastIrcMessage(fromAgentId: string, message: IrcMessage): s
|
|
|
467
498
|
}
|
|
468
499
|
handle.pendingMessages.push(message);
|
|
469
500
|
handle.updatedAt = new Date().toISOString();
|
|
501
|
+
invalidateSortedLiveAgents();
|
|
470
502
|
// G4: Try non-blocking delivery
|
|
471
503
|
const session = handle.session as Record<string, unknown>;
|
|
472
504
|
if (typeof session.sendCustomMessage === "function") {
|
|
@@ -600,6 +632,7 @@ export async function respondAsBackground(
|
|
|
600
632
|
error: `Target '${targetAgentId}' has no message channel.`,
|
|
601
633
|
};
|
|
602
634
|
handle.updatedAt = new Date().toISOString();
|
|
635
|
+
invalidateSortedLiveAgents();
|
|
603
636
|
|
|
604
637
|
if (!awaitReply) return { ok: true, corrId };
|
|
605
638
|
|
|
@@ -19,6 +19,16 @@ export function subscribeLiveControlRealtime(listener: Listener): () => void {
|
|
|
19
19
|
return () => listeners.delete(listener);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Task 26 (2026-08-24): poll-interval gate for live-session control polling.
|
|
24
|
+
* True while at least one realtime listener is registered — in that mode
|
|
25
|
+
* in-process control requests are delivered immediately and the file poll is
|
|
26
|
+
* only a cross-process fallback, so the poller can run slower.
|
|
27
|
+
*/
|
|
28
|
+
export function hasLiveControlRealtimeListeners(): boolean {
|
|
29
|
+
return listeners.size > 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
22
32
|
export function liveControlRealtimeMessage(request: LiveAgentControlRequest): LiveControlRealtimeMessage {
|
|
23
33
|
return { type: "live-control", version: 1, request };
|
|
24
34
|
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
1
|
import type { AgentConfig } from "../../agents/agent-config.ts";
|
|
4
2
|
import { resolveToolPolicy } from "../../agents/agent-config.ts";
|
|
5
3
|
import type { CrewRuntimeConfig } from "../../config/config.ts";
|
|
@@ -11,6 +9,7 @@ import type { TeamRunManifest, TeamTaskState, UsageState } from "../../state/typ
|
|
|
11
9
|
import { logInternalError } from "../../utils/internal-error.ts";
|
|
12
10
|
import { redactSecrets } from "../../utils/redaction.ts";
|
|
13
11
|
import type { WorkflowStep } from "../../workflows/workflow-config.ts";
|
|
12
|
+
import { BoundedTail } from "../compaction/compact-stages/bounded-tail.ts";
|
|
14
13
|
import { createIrcTool } from "../custom-tools/irc-tool.ts";
|
|
15
14
|
import { createSubmitResultTool } from "../custom-tools/submit-result-tool.ts";
|
|
16
15
|
import { buildMcpProxyFromSession } from "../mcp-proxy.ts";
|
|
@@ -28,7 +27,13 @@ import { readEnabledModelsPatterns } from "../model/model-scope.ts";
|
|
|
28
27
|
import { isLiveSessionRuntimeAvailable } from "../model/runtime-resolver.ts";
|
|
29
28
|
import { awaitRuntimeWarmup } from "../model/runtime-warmup.ts";
|
|
30
29
|
import { liveAgentContext, registerLiveAgentModel, unregisterLiveAgentModel } from "../model/session-model.ts";
|
|
31
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
appendBatchedJsonlLine,
|
|
32
|
+
eventToSidechainType,
|
|
33
|
+
flushPendingSidechainWrites,
|
|
34
|
+
sidechainOutputPath,
|
|
35
|
+
writeSidechainEntry,
|
|
36
|
+
} from "../output/sidechain-output.ts";
|
|
32
37
|
// NOTE: buildMemoryBlock is intentionally NOT imported here. The agent memory
|
|
33
38
|
// block is injected via renderTaskPrompt().full (the USER prompt), which is
|
|
34
39
|
// shared by both the child-pi path (no system prompt) and the live-session
|
|
@@ -60,7 +65,7 @@ import {
|
|
|
60
65
|
trackLiveAgentTurnEnd,
|
|
61
66
|
updateLiveAgentStatus,
|
|
62
67
|
} from "./live-agent-manager.ts";
|
|
63
|
-
import { subscribeLiveControlRealtime } from "./live-control-realtime.ts";
|
|
68
|
+
import { hasLiveControlRealtimeListeners, subscribeLiveControlRealtime } from "./live-control-realtime.ts";
|
|
64
69
|
import { buildExtensionBridge } from "./live-extension-bridge.ts";
|
|
65
70
|
import { collectLiveSessionHealth, formatLiveSessionDiagnostics } from "./live-session-health.ts";
|
|
66
71
|
|
|
@@ -178,20 +183,13 @@ type LiveSessionLike = {
|
|
|
178
183
|
|
|
179
184
|
function appendTranscript(filePath: string | undefined, event: unknown): void {
|
|
180
185
|
if (!filePath) return;
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
try {
|
|
189
|
-
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
|
|
190
|
-
const content = `${JSON.stringify(redactSecrets(event))}\n`;
|
|
191
|
-
await fs.promises.appendFile(filePath, content, "utf-8");
|
|
192
|
-
} catch (error) {
|
|
193
|
-
logInternalError("live-session.transcript-write-failed", error as Error, `path=${filePath}`);
|
|
194
|
-
}
|
|
186
|
+
// Task 26 (2026-08-24): serialize + redact at queue time, then route
|
|
187
|
+
// through the shared 50ms batched JSONL writer (one mkdir + appendFileSync
|
|
188
|
+
// per path per window instead of one async mkdir+appendFile PER streaming
|
|
189
|
+
// event). child-pi-transcript's batched writer is not generic over plain
|
|
190
|
+
// paths (ChildPiRunInput + artifactsRoot containment + re-redaction), so
|
|
191
|
+
// the identical map lives in sidechain-output.ts.
|
|
192
|
+
appendBatchedJsonlLine(filePath, `${JSON.stringify(redactSecrets(event))}\n`);
|
|
195
193
|
}
|
|
196
194
|
|
|
197
195
|
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
@@ -630,6 +628,9 @@ export async function runLiveSessionTask(input: LiveSessionSpawnInput): Promise<
|
|
|
630
628
|
message: event,
|
|
631
629
|
cwd: input.task.cwd,
|
|
632
630
|
});
|
|
631
|
+
// Task 26: the mock path returns before the try/finally below, so drain
|
|
632
|
+
// the batched writers here — tests read these files right after return.
|
|
633
|
+
flushPendingSidechainWrites();
|
|
633
634
|
if (isCurrent()) input.onEvent?.(event);
|
|
634
635
|
const stdout = `Mock live-session success for ${input.agent.name}${inherited}`;
|
|
635
636
|
if (isCurrent()) input.onOutput?.(stdout);
|
|
@@ -678,7 +679,13 @@ export async function runLiveSessionTask(input: LiveSessionSpawnInput): Promise<
|
|
|
678
679
|
let unsubscribe: (() => void) | undefined;
|
|
679
680
|
let unsubscribeControlRealtime: (() => void) | undefined;
|
|
680
681
|
let controlTimer: ReturnType<typeof setInterval> | undefined;
|
|
681
|
-
|
|
682
|
+
// PERF (2026-08-24): unbounded string concat re-copies the whole transcript
|
|
683
|
+
// on every chunk; bounded tail keeps whatever the consumer actually reads.
|
|
684
|
+
// Consumer sizing: stdout becomes parsedOutput.finalText + the
|
|
685
|
+
// results/{taskId}.txt artifact — the same downstream consumers the
|
|
686
|
+
// child-pi path already caps at maxCaptureBytes (512 KiB) via BoundedTail,
|
|
687
|
+
// so we match that cap for parity (this is NOT a tail-only preview).
|
|
688
|
+
const stdoutTail = new BoundedTail();
|
|
682
689
|
let jsonEvents = 0;
|
|
683
690
|
const collectedJsonEvents: Record<string, unknown>[] | undefined = yieldEnabled ? [] : undefined;
|
|
684
691
|
const maxCollectedJsonEvents = 1000;
|
|
@@ -898,9 +905,18 @@ export async function runLiveSessionTask(input: LiveSessionSpawnInput): Promise<
|
|
|
898
905
|
});
|
|
899
906
|
});
|
|
900
907
|
await pollControl();
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
908
|
+
// PERF (2026-08-24): each poll does existsSync + readFileSync + JSON.parse
|
|
909
|
+
// of the whole control JSONL. With a realtime subscription active for this
|
|
910
|
+
// agent, in-process control requests (team-tool / cross-extension RPC
|
|
911
|
+
// publish) are delivered immediately and the poll only covers cross-process
|
|
912
|
+
// writers → 1000ms. Without realtime, the file poll is the sole delivery
|
|
913
|
+
// path → keep the faster 500ms cadence.
|
|
914
|
+
controlTimer = setInterval(
|
|
915
|
+
() => {
|
|
916
|
+
if (isCurrent()) void pollControl();
|
|
917
|
+
},
|
|
918
|
+
hasLiveControlRealtimeListeners() ? 1000 : 500,
|
|
919
|
+
);
|
|
904
920
|
let turnCount = 0;
|
|
905
921
|
let softLimitReached = false;
|
|
906
922
|
const maxTurns = input.runtimeConfig?.maxTurns;
|
|
@@ -953,7 +969,7 @@ export async function runLiveSessionTask(input: LiveSessionSpawnInput): Promise<
|
|
|
953
969
|
input.onEvent?.(event);
|
|
954
970
|
const text = [...eventText(event), ...finalAssistantText(event)].join("\n");
|
|
955
971
|
if (text.trim()) {
|
|
956
|
-
|
|
972
|
+
stdoutTail.push(`${text}\n`);
|
|
957
973
|
streamOut?.write(text + "\n");
|
|
958
974
|
trackLiveAgentResponseText(agentId, text);
|
|
959
975
|
input.onOutput?.(text);
|
|
@@ -1034,7 +1050,7 @@ export async function runLiveSessionTask(input: LiveSessionSpawnInput): Promise<
|
|
|
1034
1050
|
return {
|
|
1035
1051
|
available: true,
|
|
1036
1052
|
exitCode: 1,
|
|
1037
|
-
stdout:
|
|
1053
|
+
stdout: stdoutTail.value().trim(),
|
|
1038
1054
|
stderr: msg,
|
|
1039
1055
|
jsonEvents,
|
|
1040
1056
|
error: msg,
|
|
@@ -1050,7 +1066,7 @@ export async function runLiveSessionTask(input: LiveSessionSpawnInput): Promise<
|
|
|
1050
1066
|
data: {
|
|
1051
1067
|
elapsedMs: Date.now() - promptStart,
|
|
1052
1068
|
jsonEvents,
|
|
1053
|
-
outputLength:
|
|
1069
|
+
outputLength: stdoutTail.value().length,
|
|
1054
1070
|
},
|
|
1055
1071
|
});
|
|
1056
1072
|
|
|
@@ -1171,7 +1187,7 @@ export async function runLiveSessionTask(input: LiveSessionSpawnInput): Promise<
|
|
|
1171
1187
|
return {
|
|
1172
1188
|
available: true,
|
|
1173
1189
|
exitCode: 0,
|
|
1174
|
-
stdout:
|
|
1190
|
+
stdout: stdoutTail.value().trim(),
|
|
1175
1191
|
stderr: created.modelFallbackMessage ?? "",
|
|
1176
1192
|
jsonEvents,
|
|
1177
1193
|
usage,
|
|
@@ -1205,7 +1221,7 @@ export async function runLiveSessionTask(input: LiveSessionSpawnInput): Promise<
|
|
|
1205
1221
|
return {
|
|
1206
1222
|
available: true,
|
|
1207
1223
|
exitCode: 1,
|
|
1208
|
-
stdout:
|
|
1224
|
+
stdout: stdoutTail.value().trim(),
|
|
1209
1225
|
stderr: message,
|
|
1210
1226
|
jsonEvents,
|
|
1211
1227
|
error: message,
|
|
@@ -1220,6 +1236,10 @@ export async function runLiveSessionTask(input: LiveSessionSpawnInput): Promise<
|
|
|
1220
1236
|
// H6: Unsubscribe listeners FIRST before clearing timer to prevent race
|
|
1221
1237
|
unsubscribe?.();
|
|
1222
1238
|
unsubscribeControlRealtime?.();
|
|
1239
|
+
// Task 26 (2026-08-24): drain the batched sidechain/transcript writers
|
|
1240
|
+
// now that no more session events can enqueue, so callers that read the
|
|
1241
|
+
// files right after this task settles see complete content.
|
|
1242
|
+
flushPendingSidechainWrites();
|
|
1223
1243
|
// Round 27 (BUG 4): remove the named abort listener to avoid leaking it
|
|
1224
1244
|
// on the shared AbortSignal across many live-session tasks.
|
|
1225
1245
|
if (onSignalAbort) input.signal?.removeEventListener("abort", onSignalAbort);
|