pi-crew 0.1.43 → 0.1.45
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 +27 -0
- package/agents/analyst.md +11 -11
- package/agents/critic.md +11 -11
- package/agents/executor.md +11 -11
- package/agents/explorer.md +11 -11
- package/agents/planner.md +11 -11
- package/agents/reviewer.md +11 -11
- package/agents/security-reviewer.md +11 -11
- package/agents/test-engineer.md +11 -11
- package/agents/verifier.md +11 -11
- package/agents/writer.md +11 -11
- package/docs/refactor-tasks-phase3.md +394 -394
- package/docs/refactor-tasks-phase4.md +564 -564
- package/docs/refactor-tasks-phase5.md +402 -402
- package/docs/refactor-tasks-phase6.md +662 -662
- package/docs/research-extension-examples.md +297 -297
- package/docs/research-extension-system.md +324 -324
- package/docs/research-optimization-plan.md +548 -548
- package/docs/research-phase10-distillation.md +199 -0
- package/docs/research-phase11-distillation.md +201 -0
- package/docs/research-pi-coding-agent.md +357 -357
- package/docs/research-source-pi-crew-reference.md +174 -174
- package/docs/runtime-flow.md +148 -148
- package/docs/source-runtime-refactor-map.md +83 -83
- package/index.ts +6 -6
- package/package.json +1 -1
- package/src/agents/agent-serializer.ts +34 -34
- package/src/agents/discover-agents.ts +1 -0
- package/src/config/config.ts +19 -0
- package/src/extension/cross-extension-rpc.ts +82 -82
- package/src/extension/register.ts +134 -8
- package/src/extension/registration/commands.ts +18 -2
- package/src/extension/registration/compaction-guard.ts +125 -125
- package/src/extension/registration/subagent-tools.ts +148 -148
- package/src/extension/registration/team-tool.ts +27 -8
- package/src/extension/run-bundle-schema.ts +89 -89
- package/src/extension/run-index.ts +19 -0
- package/src/extension/run-maintenance.ts +43 -43
- package/src/extension/team-tool/api.ts +1 -1
- package/src/extension/team-tool/cancel.ts +79 -4
- package/src/extension/team-tool/context.ts +2 -0
- package/src/extension/team-tool/handle-settings.ts +188 -188
- package/src/extension/team-tool/inspect.ts +41 -41
- package/src/extension/team-tool/lifecycle-actions.ts +79 -79
- package/src/extension/team-tool/plan.ts +19 -19
- package/src/extension/team-tool/respond.ts +84 -0
- package/src/extension/team-tool/run.ts +3 -2
- package/src/extension/team-tool/status.ts +7 -1
- package/src/extension/team-tool-types.ts +4 -0
- package/src/extension/team-tool.ts +2 -0
- package/src/i18n.ts +184 -184
- package/src/observability/event-to-metric.ts +6 -0
- package/src/observability/exporters/otlp-exporter.ts +77 -77
- package/src/prompt/prompt-runtime.ts +72 -72
- package/src/runtime/agent-control.ts +63 -63
- package/src/runtime/agent-memory.ts +72 -72
- package/src/runtime/agent-observability.ts +114 -114
- package/src/runtime/async-marker.ts +26 -26
- package/src/runtime/attention-events.ts +28 -28
- package/src/runtime/background-runner.ts +53 -53
- package/src/runtime/child-pi.ts +444 -444
- package/src/runtime/completion-guard.ts +87 -0
- package/src/runtime/crash-recovery.ts +30 -0
- package/src/runtime/crew-agent-records.ts +8 -0
- package/src/runtime/crew-agent-runtime.ts +2 -1
- package/src/runtime/delivery-coordinator.ts +154 -0
- package/src/runtime/direct-run.ts +35 -35
- package/src/runtime/foreground-control.ts +82 -82
- package/src/runtime/green-contract.ts +46 -46
- package/src/runtime/group-join.ts +106 -106
- package/src/runtime/heartbeat-gradient.ts +28 -28
- package/src/runtime/heartbeat-watcher.ts +124 -124
- package/src/runtime/live-agent-control.ts +87 -87
- package/src/runtime/live-agent-manager.ts +85 -85
- package/src/runtime/live-control-realtime.ts +36 -36
- package/src/runtime/live-session-runtime.ts +305 -305
- package/src/runtime/model-fallback.ts +5 -2
- package/src/runtime/overflow-recovery.ts +176 -0
- package/src/runtime/parallel-research.ts +44 -44
- package/src/runtime/pi-json-output.ts +111 -111
- package/src/runtime/policy-engine.ts +79 -79
- package/src/runtime/process-status.ts +1 -1
- package/src/runtime/progress-event-coalescer.ts +43 -43
- package/src/runtime/recovery-recipes.ts +74 -74
- package/src/runtime/retry-executor.ts +64 -64
- package/src/runtime/role-permission.ts +39 -39
- package/src/runtime/session-resources.ts +25 -0
- package/src/runtime/session-snapshot.ts +59 -0
- package/src/runtime/session-usage.ts +79 -79
- package/src/runtime/sidechain-output.ts +29 -29
- package/src/runtime/stale-reconciler.ts +199 -0
- package/src/runtime/supervisor-contact.ts +59 -0
- package/src/runtime/task-display.ts +38 -38
- package/src/runtime/task-output-context.ts +127 -127
- package/src/runtime/task-runner/live-executor.ts +101 -101
- package/src/runtime/task-runner/progress.ts +119 -119
- package/src/runtime/task-runner/result-utils.ts +14 -14
- package/src/runtime/task-runner/state-helpers.ts +22 -22
- package/src/runtime/task-runner.ts +14 -0
- package/src/runtime/team-runner.ts +19 -8
- package/src/runtime/worker-heartbeat.ts +21 -21
- package/src/runtime/worker-startup.ts +57 -57
- package/src/schema/config-schema.ts +1 -0
- package/src/schema/team-tool-schema.ts +6 -1
- package/src/state/contracts.ts +6 -2
- package/src/state/state-store.ts +43 -0
- package/src/state/task-claims.ts +44 -44
- package/src/state/types.ts +2 -0
- 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/team-serializer.ts +38 -38
- package/src/types/diff.d.ts +18 -18
- package/src/ui/crew-footer.ts +101 -101
- package/src/ui/crew-select-list.ts +111 -111
- package/src/ui/crew-widget.ts +10 -5
- package/src/ui/dashboard-panes/mailbox-pane.ts +2 -1
- package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
- package/src/ui/dynamic-border.ts +25 -25
- package/src/ui/layout-primitives.ts +106 -106
- package/src/ui/loaders.ts +158 -158
- package/src/ui/powerbar-publisher.ts +4 -4
- package/src/ui/render-diff.ts +119 -119
- package/src/ui/render-scheduler.ts +143 -143
- package/src/ui/run-snapshot-cache.ts +310 -17
- package/src/ui/snapshot-types.ts +5 -0
- package/src/ui/spinner.ts +17 -17
- package/src/ui/status-colors.ts +58 -54
- package/src/ui/syntax-highlight.ts +116 -116
- package/src/utils/atomic-write.ts +33 -0
- package/src/utils/completion-dedupe.ts +63 -63
- package/src/utils/frontmatter.ts +68 -68
- package/src/utils/git.ts +262 -262
- package/src/utils/ids.ts +12 -12
- package/src/utils/names.ts +27 -27
- package/src/utils/redaction.ts +44 -44
- package/src/utils/safe-paths.ts +47 -47
- package/src/utils/sleep.ts +32 -32
- package/src/workflows/validate-workflow.ts +40 -40
- package/src/worktree/branch-freshness.ts +45 -45
- package/teams/default.team.md +12 -12
- package/teams/fast-fix.team.md +11 -11
- package/teams/implementation.team.md +18 -18
- package/teams/parallel-research.team.md +14 -14
- package/teams/research.team.md +11 -11
- package/teams/review.team.md +12 -12
- package/workflows/default.workflow.md +29 -29
- package/workflows/fast-fix.workflow.md +22 -22
- package/workflows/implementation.workflow.md +38 -38
- package/workflows/parallel-research.workflow.md +46 -46
- package/workflows/research.workflow.md +22 -22
- package/workflows/review.workflow.md +30 -30
|
@@ -1,82 +1,82 @@
|
|
|
1
|
-
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import type { TeamToolParamsValue } from "../schema/team-tool-schema.ts";
|
|
3
|
-
import { handleTeamTool } from "./team-tool.ts";
|
|
4
|
-
import { parseLiveControlRealtimeMessage, publishLiveControlRealtime } from "../runtime/live-control-realtime.ts";
|
|
5
|
-
|
|
6
|
-
export interface EventBusLike {
|
|
7
|
-
on(event: string, handler: (data: unknown) => void): (() => void) | void;
|
|
8
|
-
emit(event: string, data: unknown): void;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export type RpcReply<T = unknown> = { success: true; data?: T } | { success: false; error: string };
|
|
12
|
-
export const PI_CREW_RPC_VERSION = 1;
|
|
13
|
-
|
|
14
|
-
export interface PiCrewRpcHandle {
|
|
15
|
-
unsubscribe(): void;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function requestId(raw: unknown): string | undefined {
|
|
19
|
-
return raw && typeof raw === "object" && !Array.isArray(raw) && typeof (raw as { requestId?: unknown }).requestId === "string" ? (raw as { requestId: string }).requestId : undefined;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function reply(events: EventBusLike, channel: string, id: string | undefined, payload: RpcReply): void {
|
|
23
|
-
if (!id) return;
|
|
24
|
-
events.emit(`${channel}:reply:${id}`, payload);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function textOf(result: Awaited<ReturnType<typeof handleTeamTool>>): string {
|
|
28
|
-
return result.content?.map((item) => item.type === "text" ? item.text : "").join("\n") ?? "";
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function on(events: EventBusLike, channel: string, handler: (raw: unknown) => void): () => void {
|
|
32
|
-
const unsub = events.on(channel, handler);
|
|
33
|
-
return typeof unsub === "function" ? unsub : () => {};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function registerPiCrewRpc(events: EventBusLike | undefined, getCtx: () => ExtensionContext | undefined): PiCrewRpcHandle | undefined {
|
|
37
|
-
if (!events) return undefined;
|
|
38
|
-
const unsubs = [
|
|
39
|
-
on(events, "pi-crew:rpc:ping", (raw) => reply(events, "pi-crew:rpc:ping", requestId(raw), { success: true, data: { version: PI_CREW_RPC_VERSION } })),
|
|
40
|
-
on(events, "pi-crew:rpc:run", async (raw) => {
|
|
41
|
-
const id = requestId(raw);
|
|
42
|
-
try {
|
|
43
|
-
const ctx = getCtx();
|
|
44
|
-
if (!ctx) throw new Error("No active pi-crew session context.");
|
|
45
|
-
const params: TeamToolParamsValue = raw && typeof raw === "object" && !Array.isArray(raw) ? { ...(raw as object), action: "run" } as TeamToolParamsValue : { action: "run" };
|
|
46
|
-
const result = await handleTeamTool(params, ctx);
|
|
47
|
-
reply(events, "pi-crew:rpc:run", id, result.isError ? { success: false, error: textOf(result) } : { success: true, data: result.details });
|
|
48
|
-
} catch (error) {
|
|
49
|
-
reply(events, "pi-crew:rpc:run", id, { success: false, error: error instanceof Error ? error.message : String(error) });
|
|
50
|
-
}
|
|
51
|
-
}),
|
|
52
|
-
on(events, "pi-crew:rpc:status", async (raw) => {
|
|
53
|
-
const id = requestId(raw);
|
|
54
|
-
try {
|
|
55
|
-
const ctx = getCtx();
|
|
56
|
-
if (!ctx) throw new Error("No active pi-crew session context.");
|
|
57
|
-
const runId = raw && typeof raw === "object" && !Array.isArray(raw) ? (raw as { runId?: string }).runId : undefined;
|
|
58
|
-
const result = await handleTeamTool({ action: "status", runId }, ctx);
|
|
59
|
-
reply(events, "pi-crew:rpc:status", id, result.isError ? { success: false, error: textOf(result) } : { success: true, data: { text: textOf(result), details: result.details } });
|
|
60
|
-
} catch (error) {
|
|
61
|
-
reply(events, "pi-crew:rpc:status", id, { success: false, error: error instanceof Error ? error.message : String(error) });
|
|
62
|
-
}
|
|
63
|
-
}),
|
|
64
|
-
on(events, "pi-crew:live-control", (raw) => {
|
|
65
|
-
const request = parseLiveControlRealtimeMessage(raw);
|
|
66
|
-
if (request) publishLiveControlRealtime(request);
|
|
67
|
-
}),
|
|
68
|
-
on(events, "pi-crew:rpc:live-control", async (raw) => {
|
|
69
|
-
const id = requestId(raw);
|
|
70
|
-
try {
|
|
71
|
-
const ctx = getCtx();
|
|
72
|
-
if (!ctx) throw new Error("No active pi-crew session context.");
|
|
73
|
-
const obj = raw && typeof raw === "object" && !Array.isArray(raw) ? raw as Record<string, unknown> : {};
|
|
74
|
-
const result = await handleTeamTool({ action: "api", runId: typeof obj.runId === "string" ? obj.runId : undefined, config: { operation: typeof obj.operation === "string" ? obj.operation : "steer-agent", agentId: obj.agentId, message: obj.message, prompt: obj.prompt } }, ctx);
|
|
75
|
-
reply(events, "pi-crew:rpc:live-control", id, result.isError ? { success: false, error: textOf(result) } : { success: true, data: { text: textOf(result), details: result.details } });
|
|
76
|
-
} catch (error) {
|
|
77
|
-
reply(events, "pi-crew:rpc:live-control", id, { success: false, error: error instanceof Error ? error.message : String(error) });
|
|
78
|
-
}
|
|
79
|
-
}),
|
|
80
|
-
];
|
|
81
|
-
return { unsubscribe: () => unsubs.forEach((unsub) => unsub()) };
|
|
82
|
-
}
|
|
1
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import type { TeamToolParamsValue } from "../schema/team-tool-schema.ts";
|
|
3
|
+
import { handleTeamTool } from "./team-tool.ts";
|
|
4
|
+
import { parseLiveControlRealtimeMessage, publishLiveControlRealtime } from "../runtime/live-control-realtime.ts";
|
|
5
|
+
|
|
6
|
+
export interface EventBusLike {
|
|
7
|
+
on(event: string, handler: (data: unknown) => void): (() => void) | void;
|
|
8
|
+
emit(event: string, data: unknown): void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type RpcReply<T = unknown> = { success: true; data?: T } | { success: false; error: string };
|
|
12
|
+
export const PI_CREW_RPC_VERSION = 1;
|
|
13
|
+
|
|
14
|
+
export interface PiCrewRpcHandle {
|
|
15
|
+
unsubscribe(): void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function requestId(raw: unknown): string | undefined {
|
|
19
|
+
return raw && typeof raw === "object" && !Array.isArray(raw) && typeof (raw as { requestId?: unknown }).requestId === "string" ? (raw as { requestId: string }).requestId : undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function reply(events: EventBusLike, channel: string, id: string | undefined, payload: RpcReply): void {
|
|
23
|
+
if (!id) return;
|
|
24
|
+
events.emit(`${channel}:reply:${id}`, payload);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function textOf(result: Awaited<ReturnType<typeof handleTeamTool>>): string {
|
|
28
|
+
return result.content?.map((item) => item.type === "text" ? item.text : "").join("\n") ?? "";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function on(events: EventBusLike, channel: string, handler: (raw: unknown) => void): () => void {
|
|
32
|
+
const unsub = events.on(channel, handler);
|
|
33
|
+
return typeof unsub === "function" ? unsub : () => {};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function registerPiCrewRpc(events: EventBusLike | undefined, getCtx: () => ExtensionContext | undefined): PiCrewRpcHandle | undefined {
|
|
37
|
+
if (!events) return undefined;
|
|
38
|
+
const unsubs = [
|
|
39
|
+
on(events, "pi-crew:rpc:ping", (raw) => reply(events, "pi-crew:rpc:ping", requestId(raw), { success: true, data: { version: PI_CREW_RPC_VERSION } })),
|
|
40
|
+
on(events, "pi-crew:rpc:run", async (raw) => {
|
|
41
|
+
const id = requestId(raw);
|
|
42
|
+
try {
|
|
43
|
+
const ctx = getCtx();
|
|
44
|
+
if (!ctx) throw new Error("No active pi-crew session context.");
|
|
45
|
+
const params: TeamToolParamsValue = raw && typeof raw === "object" && !Array.isArray(raw) ? { ...(raw as object), action: "run" } as TeamToolParamsValue : { action: "run" };
|
|
46
|
+
const result = await handleTeamTool(params, ctx);
|
|
47
|
+
reply(events, "pi-crew:rpc:run", id, result.isError ? { success: false, error: textOf(result) } : { success: true, data: result.details });
|
|
48
|
+
} catch (error) {
|
|
49
|
+
reply(events, "pi-crew:rpc:run", id, { success: false, error: error instanceof Error ? error.message : String(error) });
|
|
50
|
+
}
|
|
51
|
+
}),
|
|
52
|
+
on(events, "pi-crew:rpc:status", async (raw) => {
|
|
53
|
+
const id = requestId(raw);
|
|
54
|
+
try {
|
|
55
|
+
const ctx = getCtx();
|
|
56
|
+
if (!ctx) throw new Error("No active pi-crew session context.");
|
|
57
|
+
const runId = raw && typeof raw === "object" && !Array.isArray(raw) ? (raw as { runId?: string }).runId : undefined;
|
|
58
|
+
const result = await handleTeamTool({ action: "status", runId }, ctx);
|
|
59
|
+
reply(events, "pi-crew:rpc:status", id, result.isError ? { success: false, error: textOf(result) } : { success: true, data: { text: textOf(result), details: result.details } });
|
|
60
|
+
} catch (error) {
|
|
61
|
+
reply(events, "pi-crew:rpc:status", id, { success: false, error: error instanceof Error ? error.message : String(error) });
|
|
62
|
+
}
|
|
63
|
+
}),
|
|
64
|
+
on(events, "pi-crew:live-control", (raw) => {
|
|
65
|
+
const request = parseLiveControlRealtimeMessage(raw);
|
|
66
|
+
if (request) publishLiveControlRealtime(request);
|
|
67
|
+
}),
|
|
68
|
+
on(events, "pi-crew:rpc:live-control", async (raw) => {
|
|
69
|
+
const id = requestId(raw);
|
|
70
|
+
try {
|
|
71
|
+
const ctx = getCtx();
|
|
72
|
+
if (!ctx) throw new Error("No active pi-crew session context.");
|
|
73
|
+
const obj = raw && typeof raw === "object" && !Array.isArray(raw) ? raw as Record<string, unknown> : {};
|
|
74
|
+
const result = await handleTeamTool({ action: "api", runId: typeof obj.runId === "string" ? obj.runId : undefined, config: { operation: typeof obj.operation === "string" ? obj.operation : "steer-agent", agentId: obj.agentId, message: obj.message, prompt: obj.prompt } }, ctx);
|
|
75
|
+
reply(events, "pi-crew:rpc:live-control", id, result.isError ? { success: false, error: textOf(result) } : { success: true, data: { text: textOf(result), details: result.details } });
|
|
76
|
+
} catch (error) {
|
|
77
|
+
reply(events, "pi-crew:rpc:live-control", id, { success: false, error: error instanceof Error ? error.message : String(error) });
|
|
78
|
+
}
|
|
79
|
+
}),
|
|
80
|
+
];
|
|
81
|
+
return { unsubscribe: () => unsubs.forEach((unsub) => unsub()) };
|
|
82
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
2
4
|
import { loadConfig } from "../config/config.ts";
|
|
3
5
|
import { registerAutonomousPolicy } from "./autonomous-policy.ts";
|
|
4
6
|
import { startAsyncRunNotifier, stopAsyncRunNotifier, type AsyncNotifierState } from "./async-notifier.ts";
|
|
@@ -8,6 +10,7 @@ import { registerPiCrewRpc, type PiCrewRpcHandle } from "./cross-extension-rpc.t
|
|
|
8
10
|
import { stopCrewWidget, updateCrewWidget, type CrewWidgetState } from "../ui/crew-widget.ts";
|
|
9
11
|
import { clearPiCrewPowerbar, registerPiCrewPowerbarSegments, updatePiCrewPowerbar } from "../ui/powerbar-publisher.ts";
|
|
10
12
|
import { loadRunManifestById, updateRunStatus } from "../state/state-store.ts";
|
|
13
|
+
import type { TeamRunManifest } from "../state/types.ts";
|
|
11
14
|
import { terminateActiveChildPiProcesses } from "../subagents/spawn.ts";
|
|
12
15
|
import { SubagentManager } from "../subagents/manager.ts";
|
|
13
16
|
import { __test__subagentSpawnParams, sendAgentWakeUp, sendFollowUp } from "./registration/subagent-helpers.ts";
|
|
@@ -34,6 +37,10 @@ import { OTLPExporter } from "../observability/exporters/otlp-exporter.ts";
|
|
|
34
37
|
import { HeartbeatWatcher } from "../runtime/heartbeat-watcher.ts";
|
|
35
38
|
import { appendDeadletter } from "../runtime/deadletter.ts";
|
|
36
39
|
import { detectInterruptedRuns } from "../runtime/crash-recovery.ts";
|
|
40
|
+
import { DeliveryCoordinator } from "../runtime/delivery-coordinator.ts";
|
|
41
|
+
import { OverflowRecoveryTracker } from "../runtime/overflow-recovery.ts";
|
|
42
|
+
import { tryRegisterSessionCleanup } from "../runtime/session-resources.ts";
|
|
43
|
+
import { createSessionSnapshot } from "../runtime/session-snapshot.ts";
|
|
37
44
|
import { initI18n } from "../i18n.ts";
|
|
38
45
|
|
|
39
46
|
export { __test__subagentSpawnParams };
|
|
@@ -83,6 +90,8 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
83
90
|
let metricSink: MetricSink | undefined;
|
|
84
91
|
let heartbeatWatcher: HeartbeatWatcher | undefined;
|
|
85
92
|
let otlpExporter: OTLPExporter | undefined;
|
|
93
|
+
let deliveryCoordinator: DeliveryCoordinator | undefined;
|
|
94
|
+
let overflowTracker: OverflowRecoveryTracker | undefined;
|
|
86
95
|
const configureNotifications = (ctx: ExtensionContext): void => {
|
|
87
96
|
notificationRouter?.dispose();
|
|
88
97
|
notificationSink?.dispose();
|
|
@@ -148,6 +157,28 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
148
157
|
}
|
|
149
158
|
};
|
|
150
159
|
const autoRecoveryLast = new Map<string, number>();
|
|
160
|
+
const configureDeliveryCoordinator = (): void => {
|
|
161
|
+
deliveryCoordinator?.dispose();
|
|
162
|
+
deliveryCoordinator = undefined;
|
|
163
|
+
overflowTracker?.dispose();
|
|
164
|
+
overflowTracker = undefined;
|
|
165
|
+
deliveryCoordinator = new DeliveryCoordinator({
|
|
166
|
+
emit: (event, data) => { pi.events?.emit?.(event, data); },
|
|
167
|
+
sendFollowUp: (title, body) => { sendFollowUp(pi, [title, body].filter((line): line is string => Boolean(line)).join("\n")); },
|
|
168
|
+
sendWakeUp: (message) => { sendAgentWakeUp(pi, message); },
|
|
169
|
+
});
|
|
170
|
+
overflowTracker = new OverflowRecoveryTracker({
|
|
171
|
+
onPhaseChange: (state, previousPhase) => {
|
|
172
|
+
if (metricRegistry) {
|
|
173
|
+
metricRegistry.counter("crew.task.overflow_recovery_total", "Overflow recovery phase transitions").inc({ phase: state.phase, previous_phase: previousPhase });
|
|
174
|
+
}
|
|
175
|
+
pi.events?.emit?.("crew.task.overflow", { runId: state.runId, taskId: state.taskId, phase: state.phase, previousPhase });
|
|
176
|
+
},
|
|
177
|
+
onTimeout: (state) => {
|
|
178
|
+
notifyOperator({ id: `overflow_timeout_${state.taskId}`, severity: "warning", source: "overflow-recovery", runId: state.runId, title: `Task ${state.taskId} overflow recovery timed out`, body: `Phase: ${state.phase}, compaction_count: ${state.compactionCount}, retry_count: ${state.retryCount}. The task may be stuck.` });
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
};
|
|
151
182
|
const notifyOperator = (notification: NotificationDescriptor): void => {
|
|
152
183
|
try {
|
|
153
184
|
notificationRouter?.enqueue(notification);
|
|
@@ -207,6 +238,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
207
238
|
const foregroundControllers = new Set<AbortController>();
|
|
208
239
|
let liveSidebarRunId: string | undefined;
|
|
209
240
|
let renderScheduler: RenderScheduler | undefined;
|
|
241
|
+
let preloadTimer: ReturnType<typeof setTimeout> | undefined;
|
|
210
242
|
const stopSessionBoundSubagents = (): void => {
|
|
211
243
|
for (const controller of foregroundControllers) controller.abort();
|
|
212
244
|
foregroundControllers.clear();
|
|
@@ -314,6 +346,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
314
346
|
const cleanupRuntime = (): void => {
|
|
315
347
|
if (cleanedUp) return;
|
|
316
348
|
cleanedUp = true;
|
|
349
|
+
if (preloadTimer) { clearTimeout(preloadTimer); preloadTimer = undefined; }
|
|
317
350
|
stopSessionBoundSubagents();
|
|
318
351
|
stopAsyncRunNotifier(notifierState);
|
|
319
352
|
stopCrewWidget(currentCtx, widgetState, currentCtx ? loadConfig(currentCtx.cwd).config.ui : undefined);
|
|
@@ -328,6 +361,10 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
328
361
|
eventMetricSub = undefined;
|
|
329
362
|
otlpExporter = undefined;
|
|
330
363
|
metricRegistry = undefined;
|
|
364
|
+
deliveryCoordinator?.dispose();
|
|
365
|
+
overflowTracker?.dispose();
|
|
366
|
+
deliveryCoordinator = undefined;
|
|
367
|
+
overflowTracker = undefined;
|
|
331
368
|
manifestCache.dispose();
|
|
332
369
|
runSnapshotCache.dispose?.();
|
|
333
370
|
renderScheduler?.dispose();
|
|
@@ -360,16 +397,68 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
360
397
|
autoRecoveryLast.clear();
|
|
361
398
|
configureNotifications(ctx);
|
|
362
399
|
configureObservability(ctx);
|
|
400
|
+
configureDeliveryCoordinator();
|
|
401
|
+
const sessionId = (ctx as unknown as Record<string, unknown>).sessionId;
|
|
402
|
+
if (typeof sessionId === "string" && sessionId) deliveryCoordinator?.activate(sessionId);
|
|
403
|
+
tryRegisterSessionCleanup(pi, () => { terminateActiveChildPiProcesses(); cleanupRuntime(); });
|
|
363
404
|
registerPiCrewPowerbarSegments(pi.events, loadedConfig.config.ui);
|
|
364
405
|
startAsyncRunNotifier(ctx, notifierState, loadedConfig.config.notifierIntervalMs ?? DEFAULT_UI.notifierIntervalMs, { generation: ownerGeneration, isCurrent: (generation) => generation === sessionGeneration && currentCtx === ctx && !cleanedUp });
|
|
365
406
|
const cache = getManifestCache(ctx.cwd);
|
|
366
407
|
updateCrewWidget(ctx, widgetState, loadedConfig.config.ui, cache, getRunSnapshotCache(ctx.cwd));
|
|
367
408
|
updatePiCrewPowerbar(pi.events, ctx.cwd, loadedConfig.config.ui, cache, getRunSnapshotCache(ctx.cwd), ctx, widgetState.notificationCount ?? 0);
|
|
368
409
|
renderScheduler?.dispose();
|
|
410
|
+
// Phase 12: Async preloading — renderTick reads only a pre-computed frame
|
|
411
|
+
// from memory (zero fs I/O). Background preload refreshes the frame async.
|
|
412
|
+
let preloading = false;
|
|
413
|
+
|
|
414
|
+
let lastPreloadedConfig: ReturnType<typeof loadConfig> | undefined;
|
|
415
|
+
let lastPreloadedManifests: TeamRunManifest[] = [];
|
|
416
|
+
let lastFrameManifestCache: ReturnType<typeof createManifestCache> | undefined;
|
|
417
|
+
let lastFrameSnapshotCache: ReturnType<typeof createRunSnapshotCache> | undefined;
|
|
418
|
+
|
|
419
|
+
const buildFrame = async (): Promise<boolean> => {
|
|
420
|
+
if (!currentCtx) return false;
|
|
421
|
+
lastPreloadedConfig = loadConfig(currentCtx.cwd);
|
|
422
|
+
lastFrameManifestCache = getManifestCache(currentCtx.cwd);
|
|
423
|
+
lastFrameSnapshotCache = getRunSnapshotCache(currentCtx.cwd);
|
|
424
|
+
const manifests = lastFrameManifestCache.list(20);
|
|
425
|
+
lastPreloadedManifests = manifests;
|
|
426
|
+
const runIds = manifests.map((r) => r.runId);
|
|
427
|
+
await lastFrameSnapshotCache.preloadAllStale(runIds);
|
|
428
|
+
return true;
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
const backgroundPreload = (): void => {
|
|
432
|
+
if (!currentCtx || preloading) return;
|
|
433
|
+
preloading = true;
|
|
434
|
+
buildFrame()
|
|
435
|
+
.then((ok) => {
|
|
436
|
+
preloading = false;
|
|
437
|
+
if (ok) renderScheduler?.schedule();
|
|
438
|
+
})
|
|
439
|
+
.catch((error: unknown) => {
|
|
440
|
+
preloading = false;
|
|
441
|
+
logInternalError("register.backgroundPreload", error);
|
|
442
|
+
});
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
const startPreloadLoop = (intervalMs: number): void => {
|
|
446
|
+
if (preloadTimer) clearTimeout(preloadTimer);
|
|
447
|
+
const tick = (): void => {
|
|
448
|
+
backgroundPreload();
|
|
449
|
+
preloadTimer = setTimeout(tick, intervalMs);
|
|
450
|
+
preloadTimer.unref();
|
|
451
|
+
};
|
|
452
|
+
preloadTimer = setTimeout(tick, intervalMs);
|
|
453
|
+
preloadTimer.unref();
|
|
454
|
+
};
|
|
455
|
+
|
|
369
456
|
const renderTick = (): void => {
|
|
370
457
|
if (!currentCtx) return;
|
|
371
|
-
const config =
|
|
372
|
-
const activeCache = getManifestCache(currentCtx.cwd);
|
|
458
|
+
const config = lastPreloadedConfig?.config.ui;
|
|
459
|
+
const activeCache = lastFrameManifestCache ?? getManifestCache(currentCtx.cwd);
|
|
460
|
+
const snapshotCache = lastFrameSnapshotCache ?? getRunSnapshotCache(currentCtx.cwd);
|
|
461
|
+
const manifests = lastPreloadedManifests.length > 0 ? lastPreloadedManifests : activeCache.list(20);
|
|
373
462
|
if (liveSidebarRunId) {
|
|
374
463
|
const placement = config?.widgetPlacement ?? "aboveEditor";
|
|
375
464
|
if (widgetState.lastVisibility !== "hidden" || widgetState.lastPlacement !== placement) {
|
|
@@ -382,13 +471,18 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
382
471
|
}
|
|
383
472
|
requestRender(currentCtx);
|
|
384
473
|
} else {
|
|
385
|
-
updateCrewWidget(currentCtx, widgetState, config, activeCache,
|
|
474
|
+
updateCrewWidget(currentCtx, widgetState, config, activeCache, snapshotCache, manifests);
|
|
386
475
|
}
|
|
387
|
-
updatePiCrewPowerbar(pi.events, currentCtx.cwd, config, activeCache,
|
|
476
|
+
updatePiCrewPowerbar(pi.events, currentCtx.cwd, config, activeCache, snapshotCache, currentCtx, widgetState.notificationCount ?? 0, manifests);
|
|
477
|
+
// Health notifications: only warn about genuinely running runs
|
|
388
478
|
const now = Date.now();
|
|
389
|
-
for (const run of
|
|
479
|
+
for (const run of manifests) {
|
|
480
|
+
if (run.status !== "running") continue;
|
|
390
481
|
try {
|
|
391
|
-
const snapshot =
|
|
482
|
+
const snapshot = snapshotCache.get(run.runId);
|
|
483
|
+
if (!snapshot) continue;
|
|
484
|
+
// Skip if snapshot shows run already completed/failed (stale cache)
|
|
485
|
+
if (snapshot.manifest.status !== "running") continue;
|
|
392
486
|
const summary = summarizeHeartbeats(snapshot, { now });
|
|
393
487
|
const maybeNotifyHealth = (kind: string, count: number, title: string, body: string): void => {
|
|
394
488
|
if (count <= 0) return;
|
|
@@ -405,18 +499,46 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
405
499
|
}
|
|
406
500
|
}
|
|
407
501
|
};
|
|
502
|
+
|
|
503
|
+
const fallbackMs = loadedConfig.config.ui?.dashboardLiveRefreshMs ?? 250;
|
|
408
504
|
renderScheduler = new RenderScheduler(pi.events, renderTick, {
|
|
409
|
-
fallbackMs
|
|
505
|
+
fallbackMs,
|
|
410
506
|
onInvalidate: () => getRunSnapshotCache(ctx.cwd).invalidate(),
|
|
411
507
|
});
|
|
508
|
+
// Start async preload loop — refreshes snapshot cache in background
|
|
509
|
+
startPreloadLoop(fallbackMs);
|
|
412
510
|
});
|
|
413
511
|
pi.on("session_before_switch", () => {
|
|
414
512
|
sessionGeneration++;
|
|
513
|
+
const pendingCount = deliveryCoordinator?.getPendingCount() ?? 0;
|
|
514
|
+
try {
|
|
515
|
+
const activeRuns = currentCtx ? getManifestCache(currentCtx.cwd).list(50).filter((run) => run.status === "running" || run.status === "queued" || run.status === "blocked") : [];
|
|
516
|
+
const snapshot = createSessionSnapshot(activeRuns, pendingCount, sessionGeneration);
|
|
517
|
+
if (pendingCount > 0 || snapshot.activeRunIds.length > 0) logInternalError("register.session-before-switch", undefined, JSON.stringify(snapshot));
|
|
518
|
+
} catch (error) {
|
|
519
|
+
logInternalError("register.session-before-switch.snapshot", error);
|
|
520
|
+
}
|
|
521
|
+
if (pendingCount > 0) {
|
|
522
|
+
logInternalError("register.session-before-switch", `Switching session with ${pendingCount} pending deliveries`);
|
|
523
|
+
}
|
|
524
|
+
deliveryCoordinator?.deactivate();
|
|
415
525
|
stopAsyncRunNotifier(notifierState);
|
|
416
526
|
stopSessionBoundSubagents();
|
|
417
527
|
});
|
|
418
528
|
pi.on("session_shutdown", () => cleanupRuntime());
|
|
419
529
|
|
|
530
|
+
// Phase 11a: Dynamic resource discovery — inject pi-crew skill paths.
|
|
531
|
+
try {
|
|
532
|
+
pi.on("resources_discover", () => {
|
|
533
|
+
const skillDir = path.resolve(process.cwd(), "skills");
|
|
534
|
+
const extSkillDir = path.resolve(__dirname, "..", "..", "skills");
|
|
535
|
+
const paths: string[] = [];
|
|
536
|
+
if (fs.existsSync(extSkillDir)) paths.push(extSkillDir);
|
|
537
|
+
if (skillDir !== extSkillDir && fs.existsSync(skillDir)) paths.push(skillDir);
|
|
538
|
+
return paths.length > 0 ? { skillPaths: paths } : {};
|
|
539
|
+
});
|
|
540
|
+
} catch { /* older Pi without resources_discover */ }
|
|
541
|
+
|
|
420
542
|
registerCompactionGuard(pi, { foregroundControllers });
|
|
421
543
|
|
|
422
544
|
// Phase 1.4: Permission gate for destructive team actions.
|
|
@@ -435,7 +557,11 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
435
557
|
};
|
|
436
558
|
});
|
|
437
559
|
|
|
438
|
-
registerTeamTool(pi, { foregroundControllers, startForegroundRun, openLiveSidebar, getManifestCache, getRunSnapshotCache, getMetricRegistry: () => metricRegistry, widgetState
|
|
560
|
+
registerTeamTool(pi, { foregroundControllers, startForegroundRun, openLiveSidebar, getManifestCache, getRunSnapshotCache, getMetricRegistry: () => metricRegistry, widgetState, onJsonEvent: (taskId, runId, event) => {
|
|
561
|
+
const record = event as Record<string, unknown>;
|
|
562
|
+
const eventType = typeof record.type === "string" ? record.type : undefined;
|
|
563
|
+
if (eventType) overflowTracker?.feedEvent(taskId, runId, eventType);
|
|
564
|
+
} });
|
|
439
565
|
registerSubagentTools(pi, subagentManager, { ownerSessionGeneration: captureSessionGeneration });
|
|
440
566
|
time("register.tools");
|
|
441
567
|
|
|
@@ -151,12 +151,28 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
|
|
|
151
151
|
} });
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
pi.registerCommand("team-respond", {
|
|
155
|
+
description: "Respond to a waiting pi-crew task: <runId> <taskId|--all> <message>",
|
|
156
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
157
|
+
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
158
|
+
const runId = tokens.shift();
|
|
159
|
+
const taskToken = tokens[0] === "--all" ? tokens.shift() : tokens.shift();
|
|
160
|
+
const taskId = taskToken === "--all" ? undefined : taskToken;
|
|
161
|
+
const message = tokens.join(" ") || undefined;
|
|
162
|
+
const result = await handleTeamTool({ action: "respond", runId, taskId, message }, ctx);
|
|
163
|
+
await notifyCommandResult(ctx, commandText(result));
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
|
|
154
167
|
pi.registerCommand("team-api", {
|
|
155
168
|
description: "Run safe pi-crew API interop operations: <runId> <operation> [key=value]",
|
|
156
169
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
157
170
|
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
158
|
-
const
|
|
159
|
-
const
|
|
171
|
+
const positional = tokens.filter((token) => !token.includes("=") && !token.startsWith("--"));
|
|
172
|
+
const runIdLessOperations = new Set(["metrics-snapshot"]);
|
|
173
|
+
const first = positional[0];
|
|
174
|
+
const runId = first && runIdLessOperations.has(first) ? undefined : first;
|
|
175
|
+
const operation = runId ? (positional[1] ?? "read-manifest") : (first ?? "read-manifest");
|
|
160
176
|
const config: Record<string, unknown> = { operation };
|
|
161
177
|
for (const token of tokens.filter((item) => item.includes("="))) {
|
|
162
178
|
const [key, ...rest] = token.split("=");
|