pi-crew 0.1.41 → 0.1.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/README.md +51 -0
- package/agents/analyst.md +11 -11
- package/agents/critic.md +11 -11
- package/agents/executor.md +11 -11
- package/agents/explorer.md +11 -11
- package/agents/planner.md +11 -11
- package/agents/reviewer.md +11 -11
- package/agents/security-reviewer.md +11 -11
- package/agents/test-engineer.md +11 -11
- package/agents/verifier.md +11 -11
- package/agents/writer.md +11 -11
- package/docs/refactor-tasks-phase3.md +394 -394
- package/docs/refactor-tasks-phase4.md +564 -564
- package/docs/refactor-tasks-phase5.md +402 -402
- package/docs/refactor-tasks-phase6.md +662 -662
- package/docs/research-extension-examples.md +297 -297
- package/docs/research-extension-system.md +324 -324
- package/docs/research-optimization-plan.md +548 -548
- package/docs/research-pi-coding-agent.md +357 -357
- package/docs/research-source-pi-crew-reference.md +174 -174
- package/docs/runtime-flow.md +148 -148
- package/docs/source-runtime-refactor-map.md +83 -83
- package/index.ts +6 -6
- package/package.json +1 -1
- package/src/agents/agent-serializer.ts +34 -34
- package/src/agents/discover-agents.ts +4 -4
- package/src/config/config.ts +9 -4
- package/src/extension/cross-extension-rpc.ts +82 -82
- package/src/extension/management.ts +37 -8
- package/src/extension/notification-router.ts +2 -2
- package/src/extension/register.ts +3 -0
- package/src/extension/registration/commands.ts +11 -9
- package/src/extension/registration/compaction-guard.ts +125 -125
- package/src/extension/registration/subagent-tools.ts +28 -19
- package/src/extension/result-watcher.ts +4 -4
- package/src/extension/run-bundle-schema.ts +8 -4
- package/src/extension/run-import.ts +4 -0
- package/src/extension/run-index.ts +4 -1
- package/src/extension/run-maintenance.ts +43 -24
- package/src/extension/team-tool/api.ts +1 -1
- package/src/extension/team-tool/cancel.ts +31 -31
- package/src/extension/team-tool/doctor.ts +8 -1
- package/src/extension/team-tool/handle-settings.ts +188 -0
- package/src/extension/team-tool/inspect.ts +41 -41
- package/src/extension/team-tool/lifecycle-actions.ts +79 -79
- package/src/extension/team-tool/plan.ts +19 -19
- package/src/extension/team-tool/run.ts +4 -2
- package/src/extension/team-tool/status.ts +93 -93
- package/src/extension/team-tool.ts +3 -1
- package/src/i18n.ts +184 -0
- package/src/observability/correlation.ts +2 -2
- package/src/observability/event-to-metric.ts +4 -3
- package/src/observability/exporters/adapter.ts +7 -1
- package/src/observability/exporters/otlp-exporter.ts +14 -2
- package/src/observability/exporters/prometheus-exporter.ts +9 -2
- package/src/observability/metric-registry.ts +18 -3
- package/src/observability/metric-retention.ts +11 -3
- package/src/observability/metric-sink.ts +9 -4
- package/src/observability/metrics-primitives.ts +4 -3
- package/src/prompt/prompt-runtime.ts +72 -68
- package/src/runtime/agent-control.ts +63 -63
- package/src/runtime/agent-memory.ts +72 -72
- package/src/runtime/agent-observability.ts +114 -114
- package/src/runtime/async-marker.ts +26 -26
- package/src/runtime/attention-events.ts +28 -23
- package/src/runtime/background-runner.ts +53 -53
- package/src/runtime/child-pi.ts +4 -4
- package/src/runtime/completion-guard.ts +103 -99
- package/src/runtime/concurrency.ts +1 -1
- package/src/runtime/crash-recovery.ts +2 -1
- package/src/runtime/crew-agent-runtime.ts +58 -58
- package/src/runtime/deadletter.ts +14 -4
- package/src/runtime/direct-run.ts +35 -35
- package/src/runtime/foreground-control.ts +82 -82
- package/src/runtime/green-contract.ts +46 -46
- package/src/runtime/group-join.ts +106 -106
- package/src/runtime/heartbeat-gradient.ts +28 -28
- package/src/runtime/heartbeat-watcher.ts +48 -4
- package/src/runtime/live-agent-control.ts +87 -87
- package/src/runtime/live-agent-manager.ts +85 -85
- package/src/runtime/live-control-realtime.ts +36 -36
- package/src/runtime/live-session-runtime.ts +305 -305
- package/src/runtime/manifest-cache.ts +2 -2
- package/src/runtime/model-fallback.ts +269 -261
- package/src/runtime/parallel-research.ts +44 -44
- package/src/runtime/parallel-utils.ts +1 -1
- package/src/runtime/pi-json-output.ts +111 -111
- package/src/runtime/policy-engine.ts +79 -78
- package/src/runtime/post-exit-stdio-guard.ts +2 -2
- package/src/runtime/process-status.ts +56 -56
- package/src/runtime/progress-event-coalescer.ts +43 -43
- package/src/runtime/recovery-recipes.ts +74 -74
- package/src/runtime/retry-executor.ts +5 -0
- package/src/runtime/role-permission.ts +39 -39
- package/src/runtime/runtime-resolver.ts +1 -1
- package/src/runtime/session-usage.ts +79 -79
- package/src/runtime/sidechain-output.ts +29 -29
- package/src/runtime/subagent-manager.ts +3 -3
- package/src/runtime/task-display.ts +38 -38
- package/src/runtime/task-output-context.ts +127 -127
- package/src/runtime/task-runner/live-executor.ts +101 -101
- package/src/runtime/task-runner/progress.ts +119 -111
- package/src/runtime/task-runner/result-utils.ts +14 -14
- package/src/runtime/task-runner/state-helpers.ts +22 -22
- package/src/runtime/team-runner.ts +3 -6
- package/src/runtime/worker-heartbeat.ts +21 -21
- package/src/runtime/worker-startup.ts +57 -57
- package/src/schema/config-schema.ts +1 -1
- package/src/schema/team-tool-schema.ts +110 -109
- package/src/state/artifact-store.ts +4 -2
- package/src/state/atomic-write.ts +12 -4
- package/src/state/contracts.ts +105 -105
- package/src/state/event-log.ts +3 -4
- package/src/state/jsonl-writer.ts +4 -1
- package/src/state/locks.ts +9 -1
- package/src/state/task-claims.ts +44 -42
- package/src/state/usage.ts +29 -29
- package/src/subagents/async-entry.ts +1 -1
- package/src/subagents/index.ts +3 -3
- package/src/subagents/live/control.ts +1 -1
- package/src/subagents/live/manager.ts +1 -1
- package/src/subagents/live/realtime.ts +1 -1
- package/src/subagents/live/session-runtime.ts +1 -1
- package/src/subagents/manager.ts +1 -1
- package/src/subagents/spawn.ts +1 -1
- package/src/teams/discover-teams.ts +2 -2
- package/src/teams/team-serializer.ts +38 -38
- package/src/types/diff.d.ts +18 -18
- package/src/ui/crew-footer.ts +101 -101
- package/src/ui/crew-select-list.ts +111 -111
- package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
- package/src/ui/dynamic-border.ts +25 -25
- package/src/ui/layout-primitives.ts +106 -106
- package/src/ui/live-run-sidebar.ts +1 -1
- package/src/ui/loaders.ts +158 -158
- package/src/ui/mascot.ts +3 -2
- package/src/ui/powerbar-publisher.ts +4 -3
- package/src/ui/render-diff.ts +119 -119
- package/src/ui/render-scheduler.ts +54 -14
- package/src/ui/run-dashboard.ts +39 -11
- package/src/ui/run-snapshot-cache.ts +63 -37
- package/src/ui/spinner.ts +17 -17
- package/src/ui/status-colors.ts +54 -54
- package/src/ui/syntax-highlight.ts +116 -116
- package/src/ui/theme-adapter.ts +1 -1
- package/src/ui/transcript-viewer.ts +7 -2
- package/src/utils/completion-dedupe.ts +63 -63
- package/src/utils/file-coalescer.ts +5 -3
- package/src/utils/frontmatter.ts +68 -36
- package/src/utils/git.ts +262 -262
- package/src/utils/ids.ts +12 -12
- package/src/utils/internal-error.ts +1 -1
- package/src/utils/names.ts +27 -26
- package/src/utils/paths.ts +1 -1
- package/src/utils/redaction.ts +44 -41
- package/src/utils/safe-paths.ts +47 -34
- package/src/utils/sleep.ts +2 -2
- package/src/utils/timings.ts +2 -0
- package/src/utils/visual.ts +9 -1
- package/src/workflows/discover-workflows.ts +4 -1
- package/src/workflows/validate-workflow.ts +40 -40
- package/src/worktree/branch-freshness.ts +45 -45
- package/src/worktree/worktree-manager.ts +6 -1
- package/teams/default.team.md +12 -12
- package/teams/fast-fix.team.md +11 -11
- package/teams/implementation.team.md +18 -18
- package/teams/parallel-research.team.md +14 -14
- package/teams/research.team.md +11 -11
- package/teams/review.team.md +12 -12
- package/workflows/default.workflow.md +29 -29
- package/workflows/fast-fix.workflow.md +22 -22
- package/workflows/implementation.workflow.md +38 -38
- package/workflows/parallel-research.workflow.md +46 -46
- package/workflows/research.workflow.md +22 -22
- package/workflows/review.workflow.md +30 -30
|
@@ -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
|
+
}
|
|
@@ -41,7 +41,11 @@ function extensionFor(resource: "agent" | "team" | "workflow"): string {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
function backupFile(filePath: string): string {
|
|
44
|
-
|
|
44
|
+
// Include milliseconds and a short random suffix to prevent collision
|
|
45
|
+
// when multiple backups happen within the same second.
|
|
46
|
+
const ts = new Date().toISOString().replace(/[-:.TZ]/g, "");
|
|
47
|
+
const random = Math.random().toString(36).slice(2, 6);
|
|
48
|
+
const backupPath = `${filePath}.bak-${ts.slice(0, 17)}-${random}`;
|
|
45
49
|
fs.copyFileSync(filePath, backupPath);
|
|
46
50
|
return backupPath;
|
|
47
51
|
}
|
|
@@ -83,7 +87,7 @@ function parseRoles(value: unknown): { roles?: TeamRole[]; error?: string } {
|
|
|
83
87
|
agent: sanitizeName(agent.value!),
|
|
84
88
|
description: typeof obj.description === "string" ? obj.description.trim() : undefined,
|
|
85
89
|
model: typeof obj.model === "string" ? obj.model.trim() : undefined,
|
|
86
|
-
maxConcurrency: typeof obj.maxConcurrency === "number" && Number.isInteger(obj.maxConcurrency) ? obj.maxConcurrency : undefined,
|
|
90
|
+
maxConcurrency: typeof obj.maxConcurrency === "number" && Number.isInteger(obj.maxConcurrency) && obj.maxConcurrency > 0 ? obj.maxConcurrency : undefined,
|
|
87
91
|
});
|
|
88
92
|
}
|
|
89
93
|
return { roles };
|
|
@@ -131,6 +135,8 @@ function findResource(ctx: ManagementContext, resource: "agent" | "team" | "work
|
|
|
131
135
|
return allWorkflows(discoverWorkflows(ctx.cwd)).filter(sourceMatches);
|
|
132
136
|
}
|
|
133
137
|
|
|
138
|
+
// Note: only checks agent→team references and defaultWorkflow. Does not detect
|
|
139
|
+
// workflow-step→agent/team references or team name in workflow metadata.
|
|
134
140
|
function findReferences(ctx: ManagementContext, resource: "agent" | "team" | "workflow", name: string): string[] {
|
|
135
141
|
const refs: string[] = [];
|
|
136
142
|
if (resource === "agent") {
|
|
@@ -231,7 +237,7 @@ export function handleCreate(params: TeamToolParamsValue, ctx: ManagementContext
|
|
|
231
237
|
roles: parsedRoles.roles!,
|
|
232
238
|
defaultWorkflow: typeof cfg.defaultWorkflow === "string" ? sanitizeName(cfg.defaultWorkflow) : undefined,
|
|
233
239
|
workspaceMode: cfg.workspaceMode === "worktree" ? "worktree" : "single",
|
|
234
|
-
maxConcurrency: typeof cfg.maxConcurrency === "number" && Number.isInteger(cfg.maxConcurrency) ? cfg.maxConcurrency : undefined,
|
|
240
|
+
maxConcurrency: typeof cfg.maxConcurrency === "number" && Number.isInteger(cfg.maxConcurrency) && cfg.maxConcurrency > 0 ? cfg.maxConcurrency : undefined,
|
|
235
241
|
routing: parseRouting(cfg),
|
|
236
242
|
});
|
|
237
243
|
} else {
|
|
@@ -248,7 +254,11 @@ export function handleCreate(params: TeamToolParamsValue, ctx: ManagementContext
|
|
|
248
254
|
}
|
|
249
255
|
|
|
250
256
|
if (params.dryRun) return result(`[dry-run] Would create ${params.resource} '${name}' at ${filePath}:\n\n${content}`);
|
|
251
|
-
|
|
257
|
+
try {
|
|
258
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
259
|
+
} catch (writeError) {
|
|
260
|
+
return result(`Failed to create ${params.resource}: ${writeError instanceof Error ? writeError.message : String(writeError)}`, "error", true);
|
|
261
|
+
}
|
|
252
262
|
return result(`Created ${params.resource} '${name}' at ${filePath}.`);
|
|
253
263
|
}
|
|
254
264
|
|
|
@@ -301,7 +311,7 @@ export function handleUpdate(params: TeamToolParamsValue, ctx: ManagementContext
|
|
|
301
311
|
roles,
|
|
302
312
|
defaultWorkflow: hasOwn(cfg, "defaultWorkflow") ? (typeof cfg.defaultWorkflow === "string" ? sanitizeName(cfg.defaultWorkflow) : undefined) : team.defaultWorkflow,
|
|
303
313
|
workspaceMode: cfg.workspaceMode === "worktree" ? "worktree" : cfg.workspaceMode === "single" ? "single" : team.workspaceMode,
|
|
304
|
-
maxConcurrency: typeof cfg.maxConcurrency === "number" && Number.isInteger(cfg.maxConcurrency) ? cfg.maxConcurrency : team.maxConcurrency,
|
|
314
|
+
maxConcurrency: typeof cfg.maxConcurrency === "number" && Number.isInteger(cfg.maxConcurrency) && cfg.maxConcurrency > 0 ? cfg.maxConcurrency : team.maxConcurrency,
|
|
305
315
|
routing: parseRouting(cfg, team.routing),
|
|
306
316
|
});
|
|
307
317
|
} else {
|
|
@@ -327,8 +337,23 @@ export function handleUpdate(params: TeamToolParamsValue, ctx: ManagementContext
|
|
|
327
337
|
return result([`[dry-run] Would update ${params.resource} at ${current.filePath}:`, "", content, ...(referenceUpdates.length ? ["", "Would update references in:", ...referenceUpdates.map((filePath) => `- ${filePath}`)] : [])].join("\n"));
|
|
328
338
|
}
|
|
329
339
|
const backupPath = backupFile(current.filePath);
|
|
330
|
-
|
|
331
|
-
|
|
340
|
+
try {
|
|
341
|
+
if (nextPath !== current.filePath) {
|
|
342
|
+
try {
|
|
343
|
+
fs.renameSync(current.filePath, nextPath);
|
|
344
|
+
} catch (renameError) {
|
|
345
|
+
if ((renameError as NodeJS.ErrnoException).code === "EXDEV") {
|
|
346
|
+
fs.copyFileSync(current.filePath, nextPath);
|
|
347
|
+
fs.unlinkSync(current.filePath);
|
|
348
|
+
} else {
|
|
349
|
+
throw renameError;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
fs.writeFileSync(nextPath, content, "utf-8");
|
|
354
|
+
} catch (updateError) {
|
|
355
|
+
return result(`Failed to update ${params.resource}: ${updateError instanceof Error ? updateError.message : String(updateError)}`, "error", true);
|
|
356
|
+
}
|
|
332
357
|
const updatedRefs = params.updateReferences ? updateReferencesForRename(ctx, params.resource!, current.name, nextName, source, false) : [];
|
|
333
358
|
return result([`Updated ${params.resource} at ${nextPath}. Backup: ${backupPath}.`, ...(updatedRefs.length ? ["Updated references:", ...updatedRefs.map((filePath) => `- ${filePath}`)] : [])].join("\n"));
|
|
334
359
|
}
|
|
@@ -343,6 +368,10 @@ export function handleDelete(params: TeamToolParamsValue, ctx: ManagementContext
|
|
|
343
368
|
}
|
|
344
369
|
if (params.dryRun) return result(`[dry-run] Would delete ${params.resource} at ${resolved.resource!.filePath}.${refs.length ? `\nReferences:\n${refs.map((ref) => `- ${ref}`).join("\n")}` : ""}`);
|
|
345
370
|
const backupPath = backupFile(resolved.resource!.filePath);
|
|
346
|
-
|
|
371
|
+
try {
|
|
372
|
+
fs.unlinkSync(resolved.resource!.filePath);
|
|
373
|
+
} catch (deleteError) {
|
|
374
|
+
return result(`Failed to delete ${params.resource}: ${deleteError instanceof Error ? deleteError.message : String(deleteError)}`, "error", true);
|
|
375
|
+
}
|
|
347
376
|
return result(`Deleted ${params.resource} at ${resolved.resource!.filePath}. Backup: ${backupPath}.`);
|
|
348
377
|
}
|
|
@@ -66,8 +66,8 @@ export class NotificationRouter {
|
|
|
66
66
|
const withTime = { ...notification, timestamp: notification.timestamp ?? now };
|
|
67
67
|
try {
|
|
68
68
|
this.opts.sink?.(withTime);
|
|
69
|
-
} catch {
|
|
70
|
-
|
|
69
|
+
} catch (sinkError) {
|
|
70
|
+
process.stderr.write(`[pi-crew] notification-sink: ${sinkError instanceof Error ? sinkError.message : String(sinkError)}\n`);
|
|
71
71
|
}
|
|
72
72
|
const filter = this.opts.severityFilter ?? DEFAULT_SEVERITY_FILTER;
|
|
73
73
|
if (!filter.includes(withTime.severity)) return false;
|
|
@@ -34,10 +34,12 @@ import { OTLPExporter } from "../observability/exporters/otlp-exporter.ts";
|
|
|
34
34
|
import { HeartbeatWatcher } from "../runtime/heartbeat-watcher.ts";
|
|
35
35
|
import { appendDeadletter } from "../runtime/deadletter.ts";
|
|
36
36
|
import { detectInterruptedRuns } from "../runtime/crash-recovery.ts";
|
|
37
|
+
import { initI18n } from "../i18n.ts";
|
|
37
38
|
|
|
38
39
|
export { __test__subagentSpawnParams };
|
|
39
40
|
|
|
40
41
|
export function registerPiTeams(pi: ExtensionAPI): void {
|
|
42
|
+
const disposeI18n = initI18n(pi);
|
|
41
43
|
resetTimings();
|
|
42
44
|
time("register:start");
|
|
43
45
|
const globalStore = globalThis as Record<string, unknown>;
|
|
@@ -337,6 +339,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
337
339
|
notificationSink = undefined;
|
|
338
340
|
rpcHandle?.unsubscribe();
|
|
339
341
|
rpcHandle = undefined;
|
|
342
|
+
disposeI18n();
|
|
340
343
|
sessionGeneration += 1;
|
|
341
344
|
currentCtx = undefined;
|
|
342
345
|
if (globalStore[runtimeCleanupStoreKey] === cleanupRuntime) delete globalStore[runtimeCleanupStoreKey];
|
|
@@ -74,6 +74,7 @@ async function handleMailboxDashboardAction(ctx: ExtensionCommandContext, runId:
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
function depsNotify(ctx: ExtensionCommandContext, message: string, level: "info" | "warning" | "error"): void {
|
|
77
|
+
if (!ctx.hasUI) return;
|
|
77
78
|
ctx.ui.notify(message, level);
|
|
78
79
|
}
|
|
79
80
|
|
|
@@ -200,14 +201,15 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
|
|
|
200
201
|
await notifyCommandResult(ctx, commandText(result));
|
|
201
202
|
} });
|
|
202
203
|
|
|
203
|
-
pi.registerCommand("team-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
204
|
+
pi.registerCommand("team-settings", {
|
|
205
|
+
description: "View or update pi-crew settings: [list|get <key>|set <key> <value>|unset <key>|path|scope]",
|
|
206
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
207
|
+
const result = await handleTeamTool({ action: "settings", config: { args: args.trim() } }, ctx);
|
|
208
|
+
await notifyCommandResult(ctx, commandText(result));
|
|
209
|
+
},
|
|
210
|
+
});
|
|
209
211
|
|
|
210
|
-
pi.registerCommand("team-
|
|
212
|
+
pi.registerCommand("team-cleanup", { description: "Open a simple pi-crew interactive manager", handler: handleTeamManagerCommand });
|
|
211
213
|
|
|
212
214
|
pi.registerCommand("team-result", { description: "Open a pi-crew agent result viewer: <runId> [taskId]", handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
213
215
|
const [runId, rawTaskId] = args.trim().split(/\s+/).filter(Boolean);
|
|
@@ -215,8 +217,8 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
|
|
|
215
217
|
const loaded = selected ? loadRunManifestById(ctx.cwd, selected.runId) : undefined;
|
|
216
218
|
if (ctx.hasUI && loaded) {
|
|
217
219
|
const agent = readCrewAgents(loaded.manifest).find((item) => item.taskId === selected?.taskId || item.id === selected?.taskId) ?? readCrewAgents(loaded.manifest)[0];
|
|
218
|
-
const
|
|
219
|
-
await ctx.ui.custom<undefined>((_tui, theme, _keybindings, done) => new DurableTextViewer("pi-crew result", `${selected
|
|
220
|
+
const resultText = agent?.resultArtifactPath ? commandText(await handleTeamTool({ action: "api", runId: selected?.runId ?? "", config: { operation: "read-agent-output", agentId: agent.taskId, maxBytes: 64_000 } }, ctx)) : "(no result)";
|
|
221
|
+
await ctx.ui.custom<undefined>((_tui, theme, _keybindings, done) => new DurableTextViewer("pi-crew result", `${selected?.runId ?? ""}:${agent?.taskId ?? "unknown"}`, resultText.split(/\r?\n/), theme, done), { overlay: true, overlayOptions: { width: "90%", maxHeight: "85%", anchor: "center" } });
|
|
220
222
|
return;
|
|
221
223
|
}
|
|
222
224
|
const result = await handleTeamTool({ action: "api", runId, config: { operation: "read-agent-output", agentId: rawTaskId, maxBytes: 64_000 } }, ctx);
|
|
@@ -1,125 +1,125 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import { listRecentRuns } from "../run-index.ts";
|
|
3
|
-
import type { ArtifactDescriptor, TeamRunManifest } from "../../state/types.ts";
|
|
4
|
-
|
|
5
|
-
export interface RegisterCompactionGuardOptions {
|
|
6
|
-
foregroundControllers: Set<AbortController>;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const TRIGGER_RATIO = 0.75;
|
|
10
|
-
const HARD_RATIO = 0.95;
|
|
11
|
-
const DEFAULT_CONTEXT_WINDOW = 200_000;
|
|
12
|
-
const MAX_ARTIFACT_INDEX_RUNS = 10;
|
|
13
|
-
const MAX_ARTIFACT_INDEX_ITEMS = 80;
|
|
14
|
-
|
|
15
|
-
function contextWindow(ctx: { model?: { contextWindow?: number } }): number {
|
|
16
|
-
const value = ctx.model?.contextWindow;
|
|
17
|
-
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : DEFAULT_CONTEXT_WINDOW;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function usageRatio(ctx: { getContextUsage(): { tokens: number | null } | undefined; model?: { contextWindow?: number } }): number | undefined {
|
|
21
|
-
const tokens = ctx.getContextUsage()?.tokens;
|
|
22
|
-
if (tokens === null || tokens === undefined || !Number.isFinite(tokens)) return undefined;
|
|
23
|
-
return tokens / contextWindow(ctx);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
interface CrewArtifactIndexEntry {
|
|
27
|
-
runId: string;
|
|
28
|
-
status: TeamRunManifest["status"];
|
|
29
|
-
team: string;
|
|
30
|
-
workflow?: string;
|
|
31
|
-
goal: string;
|
|
32
|
-
artifact: Pick<ArtifactDescriptor, "kind" | "path" | "producer" | "sizeBytes" | "createdAt">;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function collectCrewArtifactIndex(cwd: string): CrewArtifactIndexEntry[] {
|
|
36
|
-
const entries: CrewArtifactIndexEntry[] = [];
|
|
37
|
-
for (const run of listRecentRuns(cwd, MAX_ARTIFACT_INDEX_RUNS)) {
|
|
38
|
-
for (const artifact of run.artifacts) {
|
|
39
|
-
entries.push({
|
|
40
|
-
runId: run.runId,
|
|
41
|
-
status: run.status,
|
|
42
|
-
team: run.team,
|
|
43
|
-
workflow: run.workflow,
|
|
44
|
-
goal: run.goal,
|
|
45
|
-
artifact: {
|
|
46
|
-
kind: artifact.kind,
|
|
47
|
-
path: artifact.path,
|
|
48
|
-
producer: artifact.producer,
|
|
49
|
-
sizeBytes: artifact.sizeBytes,
|
|
50
|
-
createdAt: artifact.createdAt,
|
|
51
|
-
},
|
|
52
|
-
});
|
|
53
|
-
if (entries.length >= MAX_ARTIFACT_INDEX_ITEMS) return entries;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return entries;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function formatCrewArtifactIndex(entries: CrewArtifactIndexEntry[]): string {
|
|
60
|
-
if (!entries.length) return "";
|
|
61
|
-
const lines = ["", "# pi-crew artifact index", "Preserve these run artifact references in the compaction summary:"];
|
|
62
|
-
for (const entry of entries) {
|
|
63
|
-
lines.push(`- ${entry.artifact.kind}: ${entry.artifact.path} (run=${entry.runId}, status=${entry.status}, team=${entry.team}, workflow=${entry.workflow ?? "none"}, producer=${entry.artifact.producer})`);
|
|
64
|
-
}
|
|
65
|
-
return lines.join("\n");
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function registerCompactionGuard(pi: ExtensionAPI, options: RegisterCompactionGuardOptions): void {
|
|
69
|
-
let pendingCompactReason: string | null = null;
|
|
70
|
-
let compactionInProgress = false;
|
|
71
|
-
|
|
72
|
-
const startCompact = (ctx: ExtensionContext, reason: string): void => {
|
|
73
|
-
if (compactionInProgress) return;
|
|
74
|
-
compactionInProgress = true;
|
|
75
|
-
const artifactIndex = collectCrewArtifactIndex(ctx.cwd);
|
|
76
|
-
if (artifactIndex.length > 0) {
|
|
77
|
-
pi.appendEntry("crew:artifact-index", {
|
|
78
|
-
reason,
|
|
79
|
-
createdAt: new Date().toISOString(),
|
|
80
|
-
artifacts: artifactIndex,
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
ctx.compact({
|
|
84
|
-
customInstructions: `Prioritize keeping pi-crew run state, task results, artifact references, run IDs, and next actions. Keep completed-task detail concise.${formatCrewArtifactIndex(artifactIndex)}`,
|
|
85
|
-
onComplete: () => {
|
|
86
|
-
compactionInProgress = false;
|
|
87
|
-
ctx.ui.notify(reason === "deferred" ? "Deferred compaction completed" : "Auto-compacted context during team run", "info");
|
|
88
|
-
},
|
|
89
|
-
onError: (error) => {
|
|
90
|
-
compactionInProgress = false;
|
|
91
|
-
ctx.ui.notify(`${reason === "deferred" ? "Deferred" : "Auto"} compaction failed: ${error.message}`, "error");
|
|
92
|
-
},
|
|
93
|
-
});
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
// Phase 1.2: Defer compaction during foreground runs unless context is critically full.
|
|
97
|
-
pi.on("session_before_compact", async (_event, ctx) => {
|
|
98
|
-
if (options.foregroundControllers.size === 0) return;
|
|
99
|
-
const ratio = usageRatio(ctx);
|
|
100
|
-
if (ratio !== undefined && ratio >= HARD_RATIO) {
|
|
101
|
-
ctx.ui.notify("Compaction allowed despite foreground run: context is critically full", "warning");
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
pendingCompactReason = "deferred-during-foreground-run";
|
|
105
|
-
ctx.ui.notify("Compaction deferred: foreground team run in progress", "info");
|
|
106
|
-
return { cancel: true };
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
// Phase 2.1: Proactive compaction with dynamic threshold based on model context window.
|
|
110
|
-
pi.on("turn_end", (_event, ctx) => {
|
|
111
|
-
if (compactionInProgress) return;
|
|
112
|
-
if (options.foregroundControllers.size === 0 && pendingCompactReason) {
|
|
113
|
-
pendingCompactReason = null;
|
|
114
|
-
startCompact(ctx, "deferred");
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
const ratio = usageRatio(ctx);
|
|
118
|
-
if (ratio === undefined || ratio < TRIGGER_RATIO) return;
|
|
119
|
-
if (options.foregroundControllers.size > 0) {
|
|
120
|
-
pendingCompactReason = "threshold-during-foreground-run";
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
startCompact(ctx, "threshold");
|
|
124
|
-
});
|
|
125
|
-
}
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { listRecentRuns } from "../run-index.ts";
|
|
3
|
+
import type { ArtifactDescriptor, TeamRunManifest } from "../../state/types.ts";
|
|
4
|
+
|
|
5
|
+
export interface RegisterCompactionGuardOptions {
|
|
6
|
+
foregroundControllers: Set<AbortController>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const TRIGGER_RATIO = 0.75;
|
|
10
|
+
const HARD_RATIO = 0.95;
|
|
11
|
+
const DEFAULT_CONTEXT_WINDOW = 200_000;
|
|
12
|
+
const MAX_ARTIFACT_INDEX_RUNS = 10;
|
|
13
|
+
const MAX_ARTIFACT_INDEX_ITEMS = 80;
|
|
14
|
+
|
|
15
|
+
function contextWindow(ctx: { model?: { contextWindow?: number } }): number {
|
|
16
|
+
const value = ctx.model?.contextWindow;
|
|
17
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : DEFAULT_CONTEXT_WINDOW;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function usageRatio(ctx: { getContextUsage(): { tokens: number | null } | undefined; model?: { contextWindow?: number } }): number | undefined {
|
|
21
|
+
const tokens = ctx.getContextUsage()?.tokens;
|
|
22
|
+
if (tokens === null || tokens === undefined || !Number.isFinite(tokens)) return undefined;
|
|
23
|
+
return tokens / contextWindow(ctx);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface CrewArtifactIndexEntry {
|
|
27
|
+
runId: string;
|
|
28
|
+
status: TeamRunManifest["status"];
|
|
29
|
+
team: string;
|
|
30
|
+
workflow?: string;
|
|
31
|
+
goal: string;
|
|
32
|
+
artifact: Pick<ArtifactDescriptor, "kind" | "path" | "producer" | "sizeBytes" | "createdAt">;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function collectCrewArtifactIndex(cwd: string): CrewArtifactIndexEntry[] {
|
|
36
|
+
const entries: CrewArtifactIndexEntry[] = [];
|
|
37
|
+
for (const run of listRecentRuns(cwd, MAX_ARTIFACT_INDEX_RUNS)) {
|
|
38
|
+
for (const artifact of run.artifacts) {
|
|
39
|
+
entries.push({
|
|
40
|
+
runId: run.runId,
|
|
41
|
+
status: run.status,
|
|
42
|
+
team: run.team,
|
|
43
|
+
workflow: run.workflow,
|
|
44
|
+
goal: run.goal,
|
|
45
|
+
artifact: {
|
|
46
|
+
kind: artifact.kind,
|
|
47
|
+
path: artifact.path,
|
|
48
|
+
producer: artifact.producer,
|
|
49
|
+
sizeBytes: artifact.sizeBytes,
|
|
50
|
+
createdAt: artifact.createdAt,
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
if (entries.length >= MAX_ARTIFACT_INDEX_ITEMS) return entries;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return entries;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function formatCrewArtifactIndex(entries: CrewArtifactIndexEntry[]): string {
|
|
60
|
+
if (!entries.length) return "";
|
|
61
|
+
const lines = ["", "# pi-crew artifact index", "Preserve these run artifact references in the compaction summary:"];
|
|
62
|
+
for (const entry of entries) {
|
|
63
|
+
lines.push(`- ${entry.artifact.kind}: ${entry.artifact.path} (run=${entry.runId}, status=${entry.status}, team=${entry.team}, workflow=${entry.workflow ?? "none"}, producer=${entry.artifact.producer})`);
|
|
64
|
+
}
|
|
65
|
+
return lines.join("\n");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function registerCompactionGuard(pi: ExtensionAPI, options: RegisterCompactionGuardOptions): void {
|
|
69
|
+
let pendingCompactReason: string | null = null;
|
|
70
|
+
let compactionInProgress = false;
|
|
71
|
+
|
|
72
|
+
const startCompact = (ctx: ExtensionContext, reason: string): void => {
|
|
73
|
+
if (compactionInProgress) return;
|
|
74
|
+
compactionInProgress = true;
|
|
75
|
+
const artifactIndex = collectCrewArtifactIndex(ctx.cwd);
|
|
76
|
+
if (artifactIndex.length > 0) {
|
|
77
|
+
pi.appendEntry("crew:artifact-index", {
|
|
78
|
+
reason,
|
|
79
|
+
createdAt: new Date().toISOString(),
|
|
80
|
+
artifacts: artifactIndex,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
ctx.compact({
|
|
84
|
+
customInstructions: `Prioritize keeping pi-crew run state, task results, artifact references, run IDs, and next actions. Keep completed-task detail concise.${formatCrewArtifactIndex(artifactIndex)}`,
|
|
85
|
+
onComplete: () => {
|
|
86
|
+
compactionInProgress = false;
|
|
87
|
+
ctx.ui.notify(reason === "deferred" ? "Deferred compaction completed" : "Auto-compacted context during team run", "info");
|
|
88
|
+
},
|
|
89
|
+
onError: (error) => {
|
|
90
|
+
compactionInProgress = false;
|
|
91
|
+
ctx.ui.notify(`${reason === "deferred" ? "Deferred" : "Auto"} compaction failed: ${error.message}`, "error");
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Phase 1.2: Defer compaction during foreground runs unless context is critically full.
|
|
97
|
+
pi.on("session_before_compact", async (_event, ctx) => {
|
|
98
|
+
if (options.foregroundControllers.size === 0) return;
|
|
99
|
+
const ratio = usageRatio(ctx);
|
|
100
|
+
if (ratio !== undefined && ratio >= HARD_RATIO) {
|
|
101
|
+
ctx.ui.notify("Compaction allowed despite foreground run: context is critically full", "warning");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
pendingCompactReason = "deferred-during-foreground-run";
|
|
105
|
+
ctx.ui.notify("Compaction deferred: foreground team run in progress", "info");
|
|
106
|
+
return { cancel: true };
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Phase 2.1: Proactive compaction with dynamic threshold based on model context window.
|
|
110
|
+
pi.on("turn_end", (_event, ctx) => {
|
|
111
|
+
if (compactionInProgress) return;
|
|
112
|
+
if (options.foregroundControllers.size === 0 && pendingCompactReason) {
|
|
113
|
+
pendingCompactReason = null;
|
|
114
|
+
startCompact(ctx, "deferred");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const ratio = usageRatio(ctx);
|
|
118
|
+
if (ratio === undefined || ratio < TRIGGER_RATIO) return;
|
|
119
|
+
if (options.foregroundControllers.size > 0) {
|
|
120
|
+
pendingCompactReason = "threshold-during-foreground-run";
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
startCompact(ctx, "threshold");
|
|
124
|
+
});
|
|
125
|
+
}
|