pi-crew 0.1.44 → 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.
Files changed (142) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/agents/analyst.md +11 -11
  3. package/agents/critic.md +11 -11
  4. package/agents/executor.md +11 -11
  5. package/agents/explorer.md +11 -11
  6. package/agents/planner.md +11 -11
  7. package/agents/reviewer.md +11 -11
  8. package/agents/security-reviewer.md +11 -11
  9. package/agents/test-engineer.md +11 -11
  10. package/agents/verifier.md +11 -11
  11. package/agents/writer.md +11 -11
  12. package/docs/refactor-tasks-phase3.md +394 -394
  13. package/docs/refactor-tasks-phase4.md +564 -564
  14. package/docs/refactor-tasks-phase5.md +402 -402
  15. package/docs/refactor-tasks-phase6.md +662 -662
  16. package/docs/research-extension-examples.md +297 -297
  17. package/docs/research-extension-system.md +324 -324
  18. package/docs/research-optimization-plan.md +548 -548
  19. package/docs/research-phase10-distillation.md +198 -198
  20. package/docs/research-phase11-distillation.md +201 -201
  21. package/docs/research-pi-coding-agent.md +357 -357
  22. package/docs/research-source-pi-crew-reference.md +174 -174
  23. package/docs/runtime-flow.md +148 -148
  24. package/docs/source-runtime-refactor-map.md +83 -83
  25. package/index.ts +6 -6
  26. package/package.json +1 -1
  27. package/src/agents/agent-serializer.ts +34 -34
  28. package/src/extension/cross-extension-rpc.ts +82 -82
  29. package/src/extension/register.ts +8 -1
  30. package/src/extension/registration/commands.ts +18 -2
  31. package/src/extension/registration/compaction-guard.ts +125 -125
  32. package/src/extension/registration/subagent-tools.ts +148 -148
  33. package/src/extension/registration/team-tool.ts +26 -8
  34. package/src/extension/run-bundle-schema.ts +89 -89
  35. package/src/extension/run-maintenance.ts +43 -43
  36. package/src/extension/team-tool/cancel.ts +105 -102
  37. package/src/extension/team-tool/context.ts +1 -0
  38. package/src/extension/team-tool/handle-settings.ts +188 -188
  39. package/src/extension/team-tool/inspect.ts +41 -41
  40. package/src/extension/team-tool/lifecycle-actions.ts +79 -79
  41. package/src/extension/team-tool/plan.ts +19 -19
  42. package/src/extension/team-tool/respond.ts +83 -66
  43. package/src/extension/team-tool/run.ts +1 -0
  44. package/src/i18n.ts +184 -184
  45. package/src/observability/exporters/otlp-exporter.ts +77 -77
  46. package/src/prompt/prompt-runtime.ts +72 -72
  47. package/src/runtime/agent-control.ts +63 -63
  48. package/src/runtime/agent-memory.ts +72 -72
  49. package/src/runtime/agent-observability.ts +114 -114
  50. package/src/runtime/async-marker.ts +26 -26
  51. package/src/runtime/attention-events.ts +28 -28
  52. package/src/runtime/background-runner.ts +53 -53
  53. package/src/runtime/child-pi.ts +444 -444
  54. package/src/runtime/completion-guard.ts +190 -190
  55. package/src/runtime/crew-agent-records.ts +8 -0
  56. package/src/runtime/delivery-coordinator.ts +153 -142
  57. package/src/runtime/direct-run.ts +35 -35
  58. package/src/runtime/foreground-control.ts +82 -82
  59. package/src/runtime/green-contract.ts +46 -46
  60. package/src/runtime/group-join.ts +106 -106
  61. package/src/runtime/heartbeat-gradient.ts +28 -28
  62. package/src/runtime/heartbeat-watcher.ts +124 -124
  63. package/src/runtime/live-agent-control.ts +87 -87
  64. package/src/runtime/live-agent-manager.ts +85 -85
  65. package/src/runtime/live-control-realtime.ts +36 -36
  66. package/src/runtime/live-session-runtime.ts +305 -305
  67. package/src/runtime/overflow-recovery.ts +175 -156
  68. package/src/runtime/parallel-research.ts +44 -44
  69. package/src/runtime/pi-json-output.ts +111 -111
  70. package/src/runtime/policy-engine.ts +79 -79
  71. package/src/runtime/progress-event-coalescer.ts +43 -43
  72. package/src/runtime/recovery-recipes.ts +74 -74
  73. package/src/runtime/retry-executor.ts +64 -64
  74. package/src/runtime/role-permission.ts +39 -39
  75. package/src/runtime/session-resources.ts +25 -25
  76. package/src/runtime/session-snapshot.ts +59 -59
  77. package/src/runtime/session-usage.ts +79 -79
  78. package/src/runtime/sidechain-output.ts +29 -29
  79. package/src/runtime/stale-reconciler.ts +199 -179
  80. package/src/runtime/supervisor-contact.ts +59 -59
  81. package/src/runtime/task-display.ts +38 -38
  82. package/src/runtime/task-output-context.ts +127 -127
  83. package/src/runtime/task-runner/live-executor.ts +101 -101
  84. package/src/runtime/task-runner/progress.ts +119 -119
  85. package/src/runtime/task-runner/result-utils.ts +14 -14
  86. package/src/runtime/task-runner/state-helpers.ts +22 -22
  87. package/src/runtime/team-runner.ts +13 -4
  88. package/src/runtime/worker-heartbeat.ts +21 -21
  89. package/src/runtime/worker-startup.ts +57 -57
  90. package/src/state/state-store.ts +43 -0
  91. package/src/state/task-claims.ts +44 -44
  92. package/src/state/types.ts +2 -0
  93. package/src/state/usage.ts +29 -29
  94. package/src/subagents/async-entry.ts +1 -1
  95. package/src/subagents/index.ts +3 -3
  96. package/src/subagents/live/control.ts +1 -1
  97. package/src/subagents/live/manager.ts +1 -1
  98. package/src/subagents/live/realtime.ts +1 -1
  99. package/src/subagents/live/session-runtime.ts +1 -1
  100. package/src/subagents/manager.ts +1 -1
  101. package/src/subagents/spawn.ts +1 -1
  102. package/src/teams/team-serializer.ts +38 -38
  103. package/src/types/diff.d.ts +18 -18
  104. package/src/ui/crew-footer.ts +101 -101
  105. package/src/ui/crew-select-list.ts +111 -111
  106. package/src/ui/crew-widget.ts +5 -1
  107. package/src/ui/dashboard-panes/mailbox-pane.ts +2 -1
  108. package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
  109. package/src/ui/dynamic-border.ts +25 -25
  110. package/src/ui/layout-primitives.ts +106 -106
  111. package/src/ui/loaders.ts +158 -158
  112. package/src/ui/powerbar-publisher.ts +1 -1
  113. package/src/ui/render-diff.ts +119 -119
  114. package/src/ui/render-scheduler.ts +143 -143
  115. package/src/ui/run-snapshot-cache.ts +56 -37
  116. package/src/ui/snapshot-types.ts +5 -0
  117. package/src/ui/spinner.ts +17 -17
  118. package/src/ui/status-colors.ts +58 -58
  119. package/src/ui/syntax-highlight.ts +116 -116
  120. package/src/utils/atomic-write.ts +33 -33
  121. package/src/utils/completion-dedupe.ts +63 -63
  122. package/src/utils/frontmatter.ts +68 -68
  123. package/src/utils/git.ts +262 -262
  124. package/src/utils/ids.ts +12 -12
  125. package/src/utils/names.ts +27 -27
  126. package/src/utils/redaction.ts +44 -44
  127. package/src/utils/safe-paths.ts +47 -47
  128. package/src/utils/sleep.ts +32 -32
  129. package/src/workflows/validate-workflow.ts +40 -40
  130. package/src/worktree/branch-freshness.ts +45 -45
  131. package/teams/default.team.md +12 -12
  132. package/teams/fast-fix.team.md +11 -11
  133. package/teams/implementation.team.md +18 -18
  134. package/teams/parallel-research.team.md +14 -14
  135. package/teams/research.team.md +11 -11
  136. package/teams/review.team.md +12 -12
  137. package/workflows/default.workflow.md +29 -29
  138. package/workflows/fast-fix.workflow.md +22 -22
  139. package/workflows/implementation.workflow.md +38 -38
  140. package/workflows/parallel-research.workflow.md +46 -46
  141. package/workflows/research.workflow.md +22 -22
  142. 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
+ }
@@ -40,6 +40,7 @@ import { detectInterruptedRuns } from "../runtime/crash-recovery.ts";
40
40
  import { DeliveryCoordinator } from "../runtime/delivery-coordinator.ts";
41
41
  import { OverflowRecoveryTracker } from "../runtime/overflow-recovery.ts";
42
42
  import { tryRegisterSessionCleanup } from "../runtime/session-resources.ts";
43
+ import { createSessionSnapshot } from "../runtime/session-snapshot.ts";
43
44
  import { initI18n } from "../i18n.ts";
44
45
 
45
46
  export { __test__subagentSpawnParams };
@@ -509,8 +510,14 @@ export function registerPiTeams(pi: ExtensionAPI): void {
509
510
  });
510
511
  pi.on("session_before_switch", () => {
511
512
  sessionGeneration++;
512
- // Phase 11b: Capture state before session switch
513
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
+ }
514
521
  if (pendingCount > 0) {
515
522
  logInternalError("register.session-before-switch", `Switching session with ${pendingCount} pending deliveries`);
516
523
  }
@@ -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 runId = tokens.find((token) => !token.includes("=") && !token.startsWith("--"));
159
- const operation = tokens.find((token) => token !== runId && !token.includes("=") && !token.startsWith("--")) ?? "read-manifest";
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("=");
@@ -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
+ }