pi-crew 0.1.41 → 0.1.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +51 -0
  3. package/agents/analyst.md +11 -11
  4. package/agents/critic.md +11 -11
  5. package/agents/executor.md +11 -11
  6. package/agents/explorer.md +11 -11
  7. package/agents/planner.md +11 -11
  8. package/agents/reviewer.md +11 -11
  9. package/agents/security-reviewer.md +11 -11
  10. package/agents/test-engineer.md +11 -11
  11. package/agents/verifier.md +11 -11
  12. package/agents/writer.md +11 -11
  13. package/docs/refactor-tasks-phase3.md +394 -394
  14. package/docs/refactor-tasks-phase4.md +564 -564
  15. package/docs/refactor-tasks-phase5.md +402 -402
  16. package/docs/refactor-tasks-phase6.md +662 -662
  17. package/docs/research-extension-examples.md +297 -297
  18. package/docs/research-extension-system.md +324 -324
  19. package/docs/research-optimization-plan.md +548 -548
  20. package/docs/research-phase10-distillation.md +199 -0
  21. package/docs/research-phase11-distillation.md +201 -0
  22. package/docs/research-pi-coding-agent.md +357 -357
  23. package/docs/research-source-pi-crew-reference.md +174 -174
  24. package/docs/runtime-flow.md +148 -148
  25. package/docs/source-runtime-refactor-map.md +83 -83
  26. package/index.ts +6 -6
  27. package/package.json +1 -1
  28. package/src/agents/agent-serializer.ts +34 -34
  29. package/src/agents/discover-agents.ts +5 -4
  30. package/src/config/config.ts +28 -4
  31. package/src/extension/cross-extension-rpc.ts +82 -82
  32. package/src/extension/management.ts +37 -8
  33. package/src/extension/notification-router.ts +2 -2
  34. package/src/extension/register.ts +130 -8
  35. package/src/extension/registration/commands.ts +11 -9
  36. package/src/extension/registration/compaction-guard.ts +125 -125
  37. package/src/extension/registration/subagent-tools.ts +28 -19
  38. package/src/extension/registration/team-tool.ts +2 -1
  39. package/src/extension/result-watcher.ts +4 -4
  40. package/src/extension/run-bundle-schema.ts +8 -4
  41. package/src/extension/run-import.ts +4 -0
  42. package/src/extension/run-index.ts +23 -1
  43. package/src/extension/run-maintenance.ts +43 -24
  44. package/src/extension/team-tool/api.ts +2 -2
  45. package/src/extension/team-tool/cancel.ts +76 -4
  46. package/src/extension/team-tool/context.ts +1 -0
  47. package/src/extension/team-tool/doctor.ts +8 -1
  48. package/src/extension/team-tool/handle-settings.ts +188 -0
  49. package/src/extension/team-tool/inspect.ts +41 -41
  50. package/src/extension/team-tool/lifecycle-actions.ts +79 -79
  51. package/src/extension/team-tool/plan.ts +19 -19
  52. package/src/extension/team-tool/respond.ts +67 -0
  53. package/src/extension/team-tool/run.ts +6 -4
  54. package/src/extension/team-tool/status.ts +99 -93
  55. package/src/extension/team-tool-types.ts +4 -0
  56. package/src/extension/team-tool.ts +5 -1
  57. package/src/i18n.ts +184 -0
  58. package/src/observability/correlation.ts +2 -2
  59. package/src/observability/event-to-metric.ts +10 -3
  60. package/src/observability/exporters/adapter.ts +7 -1
  61. package/src/observability/exporters/otlp-exporter.ts +14 -2
  62. package/src/observability/exporters/prometheus-exporter.ts +9 -2
  63. package/src/observability/metric-registry.ts +18 -3
  64. package/src/observability/metric-retention.ts +11 -3
  65. package/src/observability/metric-sink.ts +9 -4
  66. package/src/observability/metrics-primitives.ts +4 -3
  67. package/src/prompt/prompt-runtime.ts +72 -68
  68. package/src/runtime/agent-control.ts +63 -63
  69. package/src/runtime/agent-memory.ts +72 -72
  70. package/src/runtime/agent-observability.ts +114 -114
  71. package/src/runtime/async-marker.ts +26 -26
  72. package/src/runtime/attention-events.ts +28 -23
  73. package/src/runtime/background-runner.ts +53 -53
  74. package/src/runtime/child-pi.ts +4 -4
  75. package/src/runtime/completion-guard.ts +95 -4
  76. package/src/runtime/concurrency.ts +1 -1
  77. package/src/runtime/crash-recovery.ts +32 -1
  78. package/src/runtime/crew-agent-runtime.ts +59 -58
  79. package/src/runtime/deadletter.ts +14 -4
  80. package/src/runtime/delivery-coordinator.ts +143 -0
  81. package/src/runtime/direct-run.ts +35 -35
  82. package/src/runtime/foreground-control.ts +82 -82
  83. package/src/runtime/green-contract.ts +46 -46
  84. package/src/runtime/group-join.ts +106 -106
  85. package/src/runtime/heartbeat-gradient.ts +28 -28
  86. package/src/runtime/heartbeat-watcher.ts +48 -4
  87. package/src/runtime/live-agent-control.ts +87 -87
  88. package/src/runtime/live-agent-manager.ts +85 -85
  89. package/src/runtime/live-control-realtime.ts +36 -36
  90. package/src/runtime/live-session-runtime.ts +305 -305
  91. package/src/runtime/manifest-cache.ts +2 -2
  92. package/src/runtime/model-fallback.ts +272 -261
  93. package/src/runtime/overflow-recovery.ts +157 -0
  94. package/src/runtime/parallel-research.ts +44 -44
  95. package/src/runtime/parallel-utils.ts +1 -1
  96. package/src/runtime/pi-json-output.ts +111 -111
  97. package/src/runtime/policy-engine.ts +79 -78
  98. package/src/runtime/post-exit-stdio-guard.ts +2 -2
  99. package/src/runtime/process-status.ts +56 -56
  100. package/src/runtime/progress-event-coalescer.ts +43 -43
  101. package/src/runtime/recovery-recipes.ts +74 -74
  102. package/src/runtime/retry-executor.ts +5 -0
  103. package/src/runtime/role-permission.ts +39 -39
  104. package/src/runtime/runtime-resolver.ts +1 -1
  105. package/src/runtime/session-resources.ts +25 -0
  106. package/src/runtime/session-snapshot.ts +59 -0
  107. package/src/runtime/session-usage.ts +79 -79
  108. package/src/runtime/sidechain-output.ts +29 -29
  109. package/src/runtime/stale-reconciler.ts +179 -0
  110. package/src/runtime/subagent-manager.ts +3 -3
  111. package/src/runtime/supervisor-contact.ts +59 -0
  112. package/src/runtime/task-display.ts +38 -38
  113. package/src/runtime/task-output-context.ts +127 -127
  114. package/src/runtime/task-runner/live-executor.ts +101 -101
  115. package/src/runtime/task-runner/progress.ts +119 -111
  116. package/src/runtime/task-runner/result-utils.ts +14 -14
  117. package/src/runtime/task-runner/state-helpers.ts +22 -22
  118. package/src/runtime/task-runner.ts +14 -0
  119. package/src/runtime/team-runner.ts +9 -10
  120. package/src/runtime/worker-heartbeat.ts +21 -21
  121. package/src/runtime/worker-startup.ts +57 -57
  122. package/src/schema/config-schema.ts +2 -1
  123. package/src/schema/team-tool-schema.ts +115 -109
  124. package/src/state/artifact-store.ts +4 -2
  125. package/src/state/atomic-write.ts +12 -4
  126. package/src/state/contracts.ts +109 -105
  127. package/src/state/event-log.ts +3 -4
  128. package/src/state/jsonl-writer.ts +4 -1
  129. package/src/state/locks.ts +9 -1
  130. package/src/state/task-claims.ts +44 -42
  131. package/src/state/usage.ts +29 -29
  132. package/src/subagents/async-entry.ts +1 -1
  133. package/src/subagents/index.ts +3 -3
  134. package/src/subagents/live/control.ts +1 -1
  135. package/src/subagents/live/manager.ts +1 -1
  136. package/src/subagents/live/realtime.ts +1 -1
  137. package/src/subagents/live/session-runtime.ts +1 -1
  138. package/src/subagents/manager.ts +1 -1
  139. package/src/subagents/spawn.ts +1 -1
  140. package/src/teams/discover-teams.ts +2 -2
  141. package/src/teams/team-serializer.ts +38 -38
  142. package/src/types/diff.d.ts +18 -18
  143. package/src/ui/crew-footer.ts +101 -101
  144. package/src/ui/crew-select-list.ts +111 -111
  145. package/src/ui/crew-widget.ts +5 -4
  146. package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
  147. package/src/ui/dynamic-border.ts +25 -25
  148. package/src/ui/layout-primitives.ts +106 -106
  149. package/src/ui/live-run-sidebar.ts +1 -1
  150. package/src/ui/loaders.ts +158 -158
  151. package/src/ui/mascot.ts +3 -2
  152. package/src/ui/powerbar-publisher.ts +7 -6
  153. package/src/ui/render-diff.ts +119 -119
  154. package/src/ui/render-scheduler.ts +54 -14
  155. package/src/ui/run-dashboard.ts +39 -11
  156. package/src/ui/run-snapshot-cache.ts +336 -36
  157. package/src/ui/spinner.ts +17 -17
  158. package/src/ui/status-colors.ts +58 -54
  159. package/src/ui/syntax-highlight.ts +116 -116
  160. package/src/ui/theme-adapter.ts +1 -1
  161. package/src/ui/transcript-viewer.ts +7 -2
  162. package/src/utils/atomic-write.ts +33 -0
  163. package/src/utils/completion-dedupe.ts +63 -63
  164. package/src/utils/file-coalescer.ts +5 -3
  165. package/src/utils/frontmatter.ts +68 -36
  166. package/src/utils/git.ts +262 -262
  167. package/src/utils/ids.ts +12 -12
  168. package/src/utils/internal-error.ts +1 -1
  169. package/src/utils/names.ts +27 -26
  170. package/src/utils/paths.ts +1 -1
  171. package/src/utils/redaction.ts +44 -41
  172. package/src/utils/safe-paths.ts +47 -34
  173. package/src/utils/sleep.ts +2 -2
  174. package/src/utils/timings.ts +2 -0
  175. package/src/utils/visual.ts +9 -1
  176. package/src/workflows/discover-workflows.ts +4 -1
  177. package/src/workflows/validate-workflow.ts +40 -40
  178. package/src/worktree/branch-freshness.ts +45 -45
  179. package/src/worktree/worktree-manager.ts +6 -1
  180. package/teams/default.team.md +12 -12
  181. package/teams/fast-fix.team.md +11 -11
  182. package/teams/implementation.team.md +18 -18
  183. package/teams/parallel-research.team.md +14 -14
  184. package/teams/research.team.md +11 -11
  185. package/teams/review.team.md +12 -12
  186. package/workflows/default.workflow.md +29 -29
  187. package/workflows/fast-fix.workflow.md +22 -22
  188. package/workflows/implementation.workflow.md +38 -38
  189. package/workflows/parallel-research.workflow.md +46 -46
  190. package/workflows/research.workflow.md +22 -22
  191. package/workflows/review.workflow.md +30 -30
@@ -1,11 +1,13 @@
1
1
  import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
2
  import type { MetricRegistry } from "../observability/metric-registry.ts";
3
3
  import { appendEvent, scanSequence } from "../state/event-log.ts";
4
+ import { withRunLockSync } from "../state/locks.ts";
4
5
  import { loadRunManifestById, saveRunTasks, updateRunStatus } from "../state/state-store.ts";
5
6
  import type { TeamTaskState } from "../state/types.ts";
6
7
  import { isWorkerHeartbeatStale } from "./worker-heartbeat.ts";
7
8
  import type { ManifestCache } from "./manifest-cache.ts";
8
9
  import { checkProcessLiveness } from "./process-status.ts";
10
+ import { reconcileStaleRun, type ReconcileResult } from "./stale-reconciler.ts";
9
11
 
10
12
  export interface RecoveryPlan {
11
13
  runId: string;
@@ -51,6 +53,35 @@ export async function applyRecoveryPlan(plan: RecoveryPlan, ctx: Pick<ExtensionC
51
53
  export function declineRecoveryPlan(plan: RecoveryPlan, ctx: Pick<ExtensionContext, "cwd">): void {
52
54
  const loaded = loadRunManifestById(ctx.cwd, plan.runId);
53
55
  if (!loaded) throw new Error(`Run '${plan.runId}' not found.`);
54
- updateRunStatus(loaded.manifest, "cancelled", "interrupted-not-resumed");
56
+ // Log the event first — if appendEvent fails, state remains consistent.
55
57
  appendEvent(loaded.manifest.eventsPath, { type: "crew.run.recovery_declined", runId: plan.runId, message: "Interrupted run was not resumed.", data: { recoveredFromSeq: plan.lastEventSeq } });
58
+ updateRunStatus(loaded.manifest, "cancelled", "interrupted-not-resumed");
59
+ }
60
+
61
+ /**
62
+ * Run 3-phase stale reconciliation on all active runs.
63
+ * Returns results for each reconciled run.
64
+ */
65
+ export function reconcileAllStaleRuns(cwd: string, manifestCache: ManifestCache, now = Date.now()): ReconcileResult[] {
66
+ const results: ReconcileResult[] = [];
67
+ for (const manifest of manifestCache.list(50)) {
68
+ if (manifest.status !== "running") continue;
69
+ const loaded = loadRunManifestById(cwd, manifest.runId);
70
+ if (!loaded) continue;
71
+ // Use lock to prevent race with cancel/status handlers modifying the same run
72
+ withRunLockSync(loaded.manifest, () => {
73
+ // Re-read inside lock to get freshest data
74
+ const fresh = loadRunManifestById(cwd, manifest.runId);
75
+ if (!fresh || fresh.manifest.status !== "running") return;
76
+ const result = reconcileStaleRun(fresh.manifest, fresh.tasks, now);
77
+ if (result.repaired) {
78
+ updateRunStatus(fresh.manifest, "failed", `Stale run reconciled: ${result.detail}`);
79
+ appendEvent(fresh.manifest.eventsPath, { type: "crew.run.reconciled_stale", runId: manifest.runId, message: result.detail, data: { verdict: result.verdict } });
80
+ }
81
+ if (result.verdict !== "healthy") {
82
+ results.push(result);
83
+ }
84
+ });
85
+ }
86
+ return results;
56
87
  }
@@ -1,58 +1,59 @@
1
- import type { TeamTaskStatus } from "../state/contracts.ts";
2
- import type { CrewActivityState, ModelRoutingState, UsageState } from "../state/types.ts";
3
-
4
- export type CrewRuntimeKind = "scaffold" | "child-process" | "live-session";
5
- export type CrewAgentStatus = "queued" | "running" | "completed" | "failed" | "cancelled" | "stopped";
6
-
7
- export interface CrewAgentRecentTool {
8
- tool: string;
9
- args?: string;
10
- endedAt: string;
11
- }
12
-
13
- export interface CrewAgentProgress {
14
- currentTool?: string;
15
- currentToolArgs?: string;
16
- currentToolStartedAt?: string;
17
- recentTools: CrewAgentRecentTool[];
18
- recentOutput: string[];
19
- toolCount: number;
20
- tokens?: number;
21
- turns?: number;
22
- durationMs?: number;
23
- lastActivityAt?: string;
24
- activityState?: CrewActivityState;
25
- failedTool?: string;
26
- }
27
-
28
- export interface CrewAgentRecord {
29
- id: string;
30
- runId: string;
31
- taskId: string;
32
- agent: string;
33
- role: string;
34
- runtime: CrewRuntimeKind;
35
- status: CrewAgentStatus;
36
- startedAt: string;
37
- completedAt?: string;
38
- resultArtifactPath?: string;
39
- transcriptPath?: string;
40
- statusPath?: string;
41
- eventsPath?: string;
42
- outputPath?: string;
43
- toolUses?: number;
44
- jsonEvents?: number;
45
- model?: string;
46
- routing?: ModelRoutingState;
47
- usage?: UsageState;
48
- progress?: CrewAgentProgress;
49
- error?: string;
50
- }
51
-
52
- export function taskStatusToAgentStatus(status: TeamTaskStatus): CrewAgentStatus {
53
- if (status === "completed") return "completed";
54
- if (status === "failed") return "failed";
55
- if (status === "cancelled" || status === "skipped") return "cancelled";
56
- if (status === "running") return "running";
57
- return "queued";
58
- }
1
+ import type { TeamTaskStatus } from "../state/contracts.ts";
2
+ import type { CrewActivityState, ModelRoutingState, UsageState } from "../state/types.ts";
3
+
4
+ export type CrewRuntimeKind = "scaffold" | "child-process" | "live-session";
5
+ export type CrewAgentStatus = "queued" | "running" | "waiting" | "completed" | "failed" | "cancelled" | "stopped";
6
+
7
+ export interface CrewAgentRecentTool {
8
+ tool: string;
9
+ args?: string;
10
+ endedAt: string;
11
+ }
12
+
13
+ export interface CrewAgentProgress {
14
+ currentTool?: string;
15
+ currentToolArgs?: string;
16
+ currentToolStartedAt?: string;
17
+ recentTools: CrewAgentRecentTool[];
18
+ recentOutput: string[];
19
+ toolCount: number;
20
+ tokens?: number;
21
+ turns?: number;
22
+ durationMs?: number;
23
+ lastActivityAt?: string;
24
+ activityState?: CrewActivityState;
25
+ failedTool?: string;
26
+ }
27
+
28
+ export interface CrewAgentRecord {
29
+ id: string;
30
+ runId: string;
31
+ taskId: string;
32
+ agent: string;
33
+ role: string;
34
+ runtime: CrewRuntimeKind;
35
+ status: CrewAgentStatus;
36
+ startedAt: string;
37
+ completedAt?: string;
38
+ resultArtifactPath?: string;
39
+ transcriptPath?: string;
40
+ statusPath?: string;
41
+ eventsPath?: string;
42
+ outputPath?: string;
43
+ toolUses?: number;
44
+ jsonEvents?: number;
45
+ model?: string;
46
+ routing?: ModelRoutingState;
47
+ usage?: UsageState;
48
+ progress?: CrewAgentProgress;
49
+ error?: string;
50
+ }
51
+
52
+ export function taskStatusToAgentStatus(status: TeamTaskStatus): CrewAgentStatus {
53
+ if (status === "completed") return "completed";
54
+ if (status === "failed") return "failed";
55
+ if (status === "cancelled" || status === "skipped") return "cancelled";
56
+ if (status === "running") return "running";
57
+ if (status === "waiting") return "waiting";
58
+ return "queued";
59
+ }
@@ -2,6 +2,8 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import type { TeamRunManifest } from "../state/types.ts";
4
4
 
5
+ import { logInternalError } from "../utils/internal-error.ts";
6
+
5
7
  export type DeadletterReason = "max-retries" | "heartbeat-dead" | "manual";
6
8
 
7
9
  export interface DeadletterEntry {
@@ -18,14 +20,22 @@ export function deadletterPath(manifest: TeamRunManifest): string {
18
20
  }
19
21
 
20
22
  export function appendDeadletter(manifest: TeamRunManifest, entry: DeadletterEntry): void {
21
- fs.mkdirSync(manifest.stateRoot, { recursive: true });
22
- fs.appendFileSync(deadletterPath(manifest), `${JSON.stringify(entry)}\n`, "utf-8");
23
+ try {
24
+ fs.mkdirSync(manifest.stateRoot, { recursive: true });
25
+ fs.appendFileSync(deadletterPath(manifest), `${JSON.stringify(entry)}\n`, "utf-8");
26
+ } catch (error) {
27
+ logInternalError("deadletter.append", error, `taskId=${entry.taskId}`);
28
+ }
23
29
  }
24
30
 
25
- export function readDeadletter(manifest: TeamRunManifest): DeadletterEntry[] {
31
+ export function readDeadletter(manifest: TeamRunManifest, maxEntries = 1000): DeadletterEntry[] {
26
32
  const filePath = deadletterPath(manifest);
27
33
  if (!fs.existsSync(filePath)) return [];
28
- return fs.readFileSync(filePath, "utf-8").split(/\r?\n/).filter(Boolean).flatMap((line) => {
34
+ // Read last maxEntries lines only to limit memory.
35
+ const raw = fs.readFileSync(filePath, "utf-8");
36
+ const lines = raw.split(/\r?\n/).filter(Boolean);
37
+ const tail = lines.slice(-maxEntries);
38
+ return tail.flatMap((line) => {
29
39
  try {
30
40
  const parsed = JSON.parse(line) as DeadletterEntry;
31
41
  return parsed && typeof parsed.taskId === "string" && typeof parsed.runId === "string" ? [parsed] : [];
@@ -0,0 +1,143 @@
1
+ import type { NotificationDescriptor } from "../extension/notification-router.ts";
2
+ import { logInternalError } from "../utils/internal-error.ts";
3
+
4
+ export interface PendingDelivery {
5
+ runId: string;
6
+ payload: unknown;
7
+ timestamp: number;
8
+ type: "result" | "notification" | "steer";
9
+ }
10
+
11
+ export interface DeliveryCoordinatorDeps {
12
+ /** Emit an event to the active Pi event bus. */
13
+ emit?: (event: string, data: unknown) => void;
14
+ /** Send a follow-up message to the active session (for notifications). */
15
+ sendFollowUp?: (title: string, body: string) => void;
16
+ /** Send a wake-up message to the active session (for async results). */
17
+ sendWakeUp?: (message: string) => void;
18
+ }
19
+
20
+ const PENDING_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
21
+
22
+ export class DeliveryCoordinator {
23
+ private ownerSessionId: string | undefined;
24
+ private active = false;
25
+ private pending: PendingDelivery[] = [];
26
+ private readonly deps: DeliveryCoordinatorDeps;
27
+ private ttlTimer: ReturnType<typeof setInterval> | undefined;
28
+
29
+ constructor(deps: DeliveryCoordinatorDeps) {
30
+ this.deps = deps;
31
+ this.ttlTimer = setInterval(() => this.evictExpired(), 60_000);
32
+ this.ttlTimer.unref();
33
+ }
34
+
35
+ activate(sessionId: string): void {
36
+ this.ownerSessionId = sessionId;
37
+ this.active = true;
38
+ this.flushQueuedResults();
39
+ }
40
+
41
+ deactivate(): void {
42
+ this.active = false;
43
+ this.ownerSessionId = undefined;
44
+ }
45
+
46
+ isActive(): boolean {
47
+ return this.active;
48
+ }
49
+
50
+ getPendingCount(): number {
51
+ return this.pending.length;
52
+ }
53
+
54
+ deliverResult(runId: string, result: unknown): void {
55
+ if (this.active && this.deps.emit) {
56
+ try {
57
+ this.deps.emit("pi-crew:run-result", result);
58
+ } catch (error) {
59
+ logInternalError("delivery-coordinator.deliverResult", error, `runId=${runId}`);
60
+ }
61
+ return;
62
+ }
63
+ this.enqueue({ runId, payload: result, timestamp: Date.now(), type: "result" });
64
+ }
65
+
66
+ deliverNotification(notification: NotificationDescriptor): void {
67
+ if (this.active && this.deps.sendFollowUp) {
68
+ try {
69
+ this.deps.sendFollowUp(notification.title, notification.body ?? "");
70
+ } catch (error) {
71
+ logInternalError("delivery-coordinator.deliverNotification", error, `id=${notification.id}`);
72
+ }
73
+ if (this.deps.emit) {
74
+ try {
75
+ this.deps.emit("pi-crew:notification", notification);
76
+ } catch { /* secondary delivery, ignore errors */ }
77
+ }
78
+ return;
79
+ }
80
+ this.enqueue({ runId: notification.runId ?? "", payload: notification, timestamp: Date.now(), type: "notification" });
81
+ }
82
+
83
+ deliverSteer(runId: string, message: string): void {
84
+ if (this.active && this.deps.sendWakeUp) {
85
+ try {
86
+ this.deps.sendWakeUp(message);
87
+ } catch (error) {
88
+ logInternalError("delivery-coordinator.deliverSteer", error, `runId=${runId}`);
89
+ }
90
+ return;
91
+ }
92
+ this.enqueue({ runId, payload: message, timestamp: Date.now(), type: "steer" });
93
+ }
94
+
95
+ flushQueuedResults(): void {
96
+ if (!this.active || this.pending.length === 0) return;
97
+ const batch = this.pending.splice(0);
98
+ for (const delivery of batch) {
99
+ try {
100
+ switch (delivery.type) {
101
+ case "result":
102
+ this.deliverResult(delivery.runId, delivery.payload);
103
+ break;
104
+ case "notification": {
105
+ const notification = delivery.payload as NotificationDescriptor;
106
+ this.deliverNotification(notification);
107
+ break;
108
+ }
109
+ case "steer": {
110
+ const message = typeof delivery.payload === "string" ? delivery.payload : String(delivery.payload);
111
+ this.deliverSteer(delivery.runId, message);
112
+ break;
113
+ }
114
+ }
115
+ } catch (error) {
116
+ logInternalError("delivery-coordinator.flush", error, `runId=${delivery.runId} type=${delivery.type}`);
117
+ }
118
+ }
119
+ }
120
+
121
+ dispose(): void {
122
+ this.deactivate();
123
+ this.pending.length = 0;
124
+ if (this.ttlTimer) {
125
+ clearInterval(this.ttlTimer);
126
+ this.ttlTimer = undefined;
127
+ }
128
+ }
129
+
130
+ private enqueue(delivery: PendingDelivery): void {
131
+ this.pending.push(delivery);
132
+ }
133
+
134
+ private evictExpired(): void {
135
+ const cutoff = Date.now() - PENDING_TTL_MS;
136
+ const before = this.pending.length;
137
+ this.pending = this.pending.filter((d) => d.timestamp > cutoff);
138
+ const evicted = before - this.pending.length;
139
+ if (evicted > 0) {
140
+ logInternalError("delivery-coordinator.evict", undefined, `evicted=${evicted} remaining=${this.pending.length}`);
141
+ }
142
+ }
143
+ }
@@ -1,35 +1,35 @@
1
- import type { AgentConfig } from "../agents/agent-config.ts";
2
- import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
3
- import type { TeamConfig } from "../teams/team-config.ts";
4
- import type { WorkflowConfig } from "../workflows/workflow-config.ts";
5
-
6
- export function isDirectRun(manifest: Pick<TeamRunManifest, "team" | "workflow">): boolean {
7
- return manifest.workflow === "direct-agent";
8
- }
9
-
10
- export function directTeamAndWorkflowFromRun(manifest: TeamRunManifest, tasks: TeamTaskState[], agents: AgentConfig[]): { team: TeamConfig; workflow: WorkflowConfig } | undefined {
11
- if (!isDirectRun(manifest)) return undefined;
12
- const firstTask = tasks[0];
13
- const agentName = firstTask?.agent ?? (manifest.team.replace(/^direct-/, "") || "executor");
14
- const agent = agents.find((candidate) => candidate.name === agentName);
15
- const role = firstTask?.role ?? "agent";
16
- const stepId = firstTask?.stepId ?? "01_agent";
17
- return {
18
- team: {
19
- name: manifest.team,
20
- description: `Direct subagent run for ${agentName}`,
21
- source: "builtin",
22
- filePath: "<generated>",
23
- roles: [{ name: role, agent: agentName, description: agent?.description }],
24
- defaultWorkflow: "direct-agent",
25
- workspaceMode: manifest.workspaceMode,
26
- },
27
- workflow: {
28
- name: manifest.workflow ?? "direct-agent",
29
- description: `Direct task for ${agentName}`,
30
- source: "builtin",
31
- filePath: "<generated>",
32
- steps: [{ id: stepId, role, task: "{goal}", model: firstTask?.model }],
33
- },
34
- };
35
- }
1
+ import type { AgentConfig } from "../agents/agent-config.ts";
2
+ import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
3
+ import type { TeamConfig } from "../teams/team-config.ts";
4
+ import type { WorkflowConfig } from "../workflows/workflow-config.ts";
5
+
6
+ export function isDirectRun(manifest: Pick<TeamRunManifest, "team" | "workflow">): boolean {
7
+ return manifest.workflow === "direct-agent";
8
+ }
9
+
10
+ export function directTeamAndWorkflowFromRun(manifest: TeamRunManifest, tasks: TeamTaskState[], agents: AgentConfig[]): { team: TeamConfig; workflow: WorkflowConfig } | undefined {
11
+ if (!isDirectRun(manifest)) return undefined;
12
+ const firstTask = tasks[0];
13
+ const agentName = firstTask?.agent ?? (manifest.team.replace(/^direct-/, "") || "executor");
14
+ const agent = agents.find((candidate) => candidate.name === agentName);
15
+ const role = firstTask?.role ?? "agent";
16
+ const stepId = firstTask?.stepId ?? "01_agent";
17
+ return {
18
+ team: {
19
+ name: manifest.team,
20
+ description: `Direct subagent run for ${agentName}`,
21
+ source: "builtin",
22
+ filePath: "<generated>",
23
+ roles: [{ name: role, agent: agentName, description: agent?.description }],
24
+ defaultWorkflow: "direct-agent",
25
+ workspaceMode: manifest.workspaceMode,
26
+ },
27
+ workflow: {
28
+ name: manifest.workflow ?? "direct-agent",
29
+ description: `Direct task for ${agentName}`,
30
+ source: "builtin",
31
+ filePath: "<generated>",
32
+ steps: [{ id: stepId, role, task: "{goal}", model: firstTask?.model }],
33
+ },
34
+ };
35
+ }
@@ -1,82 +1,82 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
- import { appendEvent } from "../state/event-log.ts";
4
- import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
5
- import { checkProcessLiveness, isActiveRunStatus } from "./process-status.ts";
6
- import { readCrewAgents } from "./crew-agent-records.ts";
7
-
8
- export type ForegroundControlRequestType = "interrupt" | "status";
9
-
10
- export interface ForegroundControlStatus {
11
- runId: string;
12
- status: TeamRunManifest["status"];
13
- active: boolean;
14
- asyncPid?: number;
15
- asyncAlive?: boolean;
16
- runningTasks: string[];
17
- runningAgents: string[];
18
- controlPath: string;
19
- lastRequest?: ForegroundControlRequest;
20
- }
21
-
22
- export interface ForegroundControlRequest {
23
- id: string;
24
- type: ForegroundControlRequestType;
25
- createdAt: string;
26
- reason: string;
27
- acknowledged: boolean;
28
- }
29
-
30
- export function foregroundControlPath(manifest: TeamRunManifest): string {
31
- return path.join(manifest.stateRoot, "foreground-control.json");
32
- }
33
-
34
- function readLastRequest(controlPath: string): ForegroundControlRequest | undefined {
35
- if (!fs.existsSync(controlPath)) return undefined;
36
- try {
37
- const parsed = JSON.parse(fs.readFileSync(controlPath, "utf-8")) as { requests?: ForegroundControlRequest[] };
38
- return parsed.requests?.at(-1);
39
- } catch {
40
- return undefined;
41
- }
42
- }
43
-
44
- export function readForegroundControlStatus(manifest: TeamRunManifest, tasks: TeamTaskState[]): ForegroundControlStatus {
45
- const controlPath = foregroundControlPath(manifest);
46
- const asyncAlive = manifest.async?.pid !== undefined ? checkProcessLiveness(manifest.async.pid).alive : undefined;
47
- return {
48
- runId: manifest.runId,
49
- status: manifest.status,
50
- active: isActiveRunStatus(manifest.status),
51
- asyncPid: manifest.async?.pid,
52
- asyncAlive,
53
- runningTasks: tasks.filter((task) => task.status === "running").map((task) => task.id),
54
- runningAgents: readCrewAgents(manifest).filter((agent) => agent.status === "running").map((agent) => agent.id),
55
- controlPath,
56
- lastRequest: readLastRequest(controlPath),
57
- };
58
- }
59
-
60
- export function writeForegroundInterruptRequest(manifest: TeamRunManifest, reason = "User requested foreground interrupt."): ForegroundControlRequest {
61
- const controlPath = foregroundControlPath(manifest);
62
- let requests: ForegroundControlRequest[] = [];
63
- if (fs.existsSync(controlPath)) {
64
- try {
65
- const parsed = JSON.parse(fs.readFileSync(controlPath, "utf-8")) as { requests?: ForegroundControlRequest[] };
66
- requests = Array.isArray(parsed.requests) ? parsed.requests : [];
67
- } catch {
68
- requests = [];
69
- }
70
- }
71
- const request: ForegroundControlRequest = {
72
- id: `fg_${Date.now().toString(36)}_${Math.random().toString(16).slice(2, 10)}`,
73
- type: "interrupt",
74
- createdAt: new Date().toISOString(),
75
- reason,
76
- acknowledged: false,
77
- };
78
- fs.mkdirSync(path.dirname(controlPath), { recursive: true });
79
- fs.writeFileSync(controlPath, `${JSON.stringify({ requests: [...requests, request] }, null, 2)}\n`, "utf-8");
80
- appendEvent(manifest.eventsPath, { type: "foreground.interrupt_requested", runId: manifest.runId, message: reason, data: { requestId: request.id, controlPath } });
81
- return request;
82
- }
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { appendEvent } from "../state/event-log.ts";
4
+ import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
5
+ import { checkProcessLiveness, isActiveRunStatus } from "./process-status.ts";
6
+ import { readCrewAgents } from "./crew-agent-records.ts";
7
+
8
+ export type ForegroundControlRequestType = "interrupt" | "status";
9
+
10
+ export interface ForegroundControlStatus {
11
+ runId: string;
12
+ status: TeamRunManifest["status"];
13
+ active: boolean;
14
+ asyncPid?: number;
15
+ asyncAlive?: boolean;
16
+ runningTasks: string[];
17
+ runningAgents: string[];
18
+ controlPath: string;
19
+ lastRequest?: ForegroundControlRequest;
20
+ }
21
+
22
+ export interface ForegroundControlRequest {
23
+ id: string;
24
+ type: ForegroundControlRequestType;
25
+ createdAt: string;
26
+ reason: string;
27
+ acknowledged: boolean;
28
+ }
29
+
30
+ export function foregroundControlPath(manifest: TeamRunManifest): string {
31
+ return path.join(manifest.stateRoot, "foreground-control.json");
32
+ }
33
+
34
+ function readLastRequest(controlPath: string): ForegroundControlRequest | undefined {
35
+ if (!fs.existsSync(controlPath)) return undefined;
36
+ try {
37
+ const parsed = JSON.parse(fs.readFileSync(controlPath, "utf-8")) as { requests?: ForegroundControlRequest[] };
38
+ return parsed.requests?.at(-1);
39
+ } catch {
40
+ return undefined;
41
+ }
42
+ }
43
+
44
+ export function readForegroundControlStatus(manifest: TeamRunManifest, tasks: TeamTaskState[]): ForegroundControlStatus {
45
+ const controlPath = foregroundControlPath(manifest);
46
+ const asyncAlive = manifest.async?.pid !== undefined ? checkProcessLiveness(manifest.async.pid).alive : undefined;
47
+ return {
48
+ runId: manifest.runId,
49
+ status: manifest.status,
50
+ active: isActiveRunStatus(manifest.status),
51
+ asyncPid: manifest.async?.pid,
52
+ asyncAlive,
53
+ runningTasks: tasks.filter((task) => task.status === "running").map((task) => task.id),
54
+ runningAgents: readCrewAgents(manifest).filter((agent) => agent.status === "running").map((agent) => agent.id),
55
+ controlPath,
56
+ lastRequest: readLastRequest(controlPath),
57
+ };
58
+ }
59
+
60
+ export function writeForegroundInterruptRequest(manifest: TeamRunManifest, reason = "User requested foreground interrupt."): ForegroundControlRequest {
61
+ const controlPath = foregroundControlPath(manifest);
62
+ let requests: ForegroundControlRequest[] = [];
63
+ if (fs.existsSync(controlPath)) {
64
+ try {
65
+ const parsed = JSON.parse(fs.readFileSync(controlPath, "utf-8")) as { requests?: ForegroundControlRequest[] };
66
+ requests = Array.isArray(parsed.requests) ? parsed.requests : [];
67
+ } catch {
68
+ requests = [];
69
+ }
70
+ }
71
+ const request: ForegroundControlRequest = {
72
+ id: `fg_${Date.now().toString(36)}_${Math.random().toString(16).slice(2, 10)}`,
73
+ type: "interrupt",
74
+ createdAt: new Date().toISOString(),
75
+ reason,
76
+ acknowledged: false,
77
+ };
78
+ fs.mkdirSync(path.dirname(controlPath), { recursive: true });
79
+ fs.writeFileSync(controlPath, `${JSON.stringify({ requests: [...requests, request] }, null, 2)}\n`, "utf-8");
80
+ appendEvent(manifest.eventsPath, { type: "foreground.interrupt_requested", runId: manifest.runId, message: reason, data: { requestId: request.id, controlPath } });
81
+ return request;
82
+ }