pi-crew 0.1.45 → 0.1.49

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 (178) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +5 -5
  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/next-upgrade-roadmap.md +808 -0
  14. package/docs/research/AGENT-EXECUTION-ARCHITECTURE.md +261 -0
  15. package/docs/research/AGENT-LIFECYCLE-COMPARISON.md +111 -0
  16. package/docs/research/AUDIT_OH_MY_PI.md +261 -0
  17. package/docs/research/AUDIT_PI_CREW.md +457 -0
  18. package/docs/research/CAVEMAN-DEEP-RESEARCH.md +281 -0
  19. package/docs/research/COMPARISON_OH_MY_PI_VS_PI_CREW.md +264 -0
  20. package/docs/research/DEEP-RESEARCH-PI-POWERBAR.md +343 -0
  21. package/docs/research/DEEP_RESEARCH_SUBAGENT_ARCHITECTURE.md +480 -0
  22. package/docs/research/GAP_CLOSURE_IMPLEMENTATION_PLAN.md +354 -0
  23. package/docs/research/IMPLEMENTATION_PLAN.md +385 -0
  24. package/docs/research/LIVE-SESSION-PRODUCTION-READY-PLAN.md +502 -0
  25. package/docs/research/OH-MY-PI-DEEP-RESEARCH-v14.7.6.md +266 -0
  26. package/docs/research/REMAINING-GAPS-PLAN.md +363 -0
  27. package/docs/research/SESSION-SUMMARY-2026-05-08.md +146 -0
  28. package/docs/research/UI-RESPONSIVENESS-AUDIT.md +173 -0
  29. package/docs/research-awesome-agent-skills-distillation.md +100 -0
  30. package/docs/research-oh-my-pi-distillation.md +369 -0
  31. package/docs/source-runtime-refactor-map.md +24 -0
  32. package/docs/usage.md +3 -3
  33. package/install.mjs +52 -8
  34. package/package.json +99 -98
  35. package/schema.json +10 -1
  36. package/skills/async-worker-recovery/SKILL.md +42 -0
  37. package/skills/context-artifact-hygiene/SKILL.md +52 -0
  38. package/skills/delegation-patterns/SKILL.md +54 -0
  39. package/skills/mailbox-interactive/SKILL.md +40 -0
  40. package/skills/model-routing-context/SKILL.md +39 -0
  41. package/skills/multi-perspective-review/SKILL.md +58 -0
  42. package/skills/observability-reliability/SKILL.md +41 -0
  43. package/skills/orchestration/SKILL.md +157 -0
  44. package/skills/ownership-session-security/SKILL.md +41 -0
  45. package/skills/pi-extension-lifecycle/SKILL.md +39 -0
  46. package/skills/requirements-to-task-packet/SKILL.md +63 -0
  47. package/skills/resource-discovery-config/SKILL.md +41 -0
  48. package/skills/runtime-state-reader/SKILL.md +44 -0
  49. package/skills/secure-agent-orchestration-review/SKILL.md +45 -0
  50. package/skills/state-mutation-locking/SKILL.md +42 -0
  51. package/skills/systematic-debugging/SKILL.md +67 -0
  52. package/skills/ui-render-performance/SKILL.md +39 -0
  53. package/skills/verification-before-done/SKILL.md +57 -0
  54. package/skills/worktree-isolation/SKILL.md +39 -0
  55. package/src/agents/agent-config.ts +6 -0
  56. package/src/agents/agent-search.ts +98 -0
  57. package/src/agents/agent-serializer.ts +38 -34
  58. package/src/agents/discover-agents.ts +29 -15
  59. package/src/config/config.ts +72 -24
  60. package/src/config/defaults.ts +25 -0
  61. package/src/extension/autonomous-policy.ts +26 -33
  62. package/src/extension/help.ts +1 -0
  63. package/src/extension/management.ts +5 -0
  64. package/src/extension/project-init.ts +62 -2
  65. package/src/extension/register.ts +69 -22
  66. package/src/extension/registration/commands.ts +64 -25
  67. package/src/extension/registration/compaction-guard.ts +1 -1
  68. package/src/extension/registration/subagent-helpers.ts +8 -0
  69. package/src/extension/registration/subagent-tools.ts +149 -148
  70. package/src/extension/registration/team-tool.ts +14 -10
  71. package/src/extension/run-index.ts +35 -21
  72. package/src/extension/run-maintenance.ts +30 -5
  73. package/src/extension/team-tool/api.ts +47 -9
  74. package/src/extension/team-tool/cancel.ts +109 -5
  75. package/src/extension/team-tool/context.ts +8 -0
  76. package/src/extension/team-tool/intent-policy.ts +42 -0
  77. package/src/extension/team-tool/lifecycle-actions.ts +120 -79
  78. package/src/extension/team-tool/parallel-dispatch.ts +156 -0
  79. package/src/extension/team-tool/respond.ts +46 -18
  80. package/src/extension/team-tool/run.ts +55 -12
  81. package/src/extension/team-tool/status.ts +13 -2
  82. package/src/extension/team-tool-types.ts +3 -0
  83. package/src/extension/team-tool.ts +45 -14
  84. package/src/hooks/registry.ts +61 -0
  85. package/src/hooks/types.ts +41 -0
  86. package/src/observability/event-to-metric.ts +8 -1
  87. package/src/runtime/agent-control.ts +169 -63
  88. package/src/runtime/async-runner.ts +3 -1
  89. package/src/runtime/background-runner.ts +78 -53
  90. package/src/runtime/cancellation-token.ts +89 -0
  91. package/src/runtime/cancellation.ts +61 -0
  92. package/src/runtime/capability-inventory.ts +116 -0
  93. package/src/runtime/child-pi.ts +458 -444
  94. package/src/runtime/code-summary.ts +247 -0
  95. package/src/runtime/crash-recovery.ts +182 -0
  96. package/src/runtime/crew-agent-records.ts +70 -10
  97. package/src/runtime/crew-agent-runtime.ts +1 -0
  98. package/src/runtime/custom-tools/irc-tool.ts +201 -0
  99. package/src/runtime/custom-tools/submit-result-tool.ts +90 -0
  100. package/src/runtime/deadletter.ts +1 -0
  101. package/src/runtime/delivery-coordinator.ts +48 -25
  102. package/src/runtime/effectiveness.ts +81 -0
  103. package/src/runtime/event-stream-bridge.ts +90 -0
  104. package/src/runtime/live-agent-control.ts +2 -1
  105. package/src/runtime/live-agent-manager.ts +179 -85
  106. package/src/runtime/live-control-realtime.ts +1 -1
  107. package/src/runtime/live-extension-bridge.ts +150 -0
  108. package/src/runtime/live-irc.ts +92 -0
  109. package/src/runtime/live-session-health.ts +100 -0
  110. package/src/runtime/live-session-runtime.ts +599 -305
  111. package/src/runtime/manifest-cache.ts +17 -2
  112. package/src/runtime/mcp-proxy.ts +113 -0
  113. package/src/runtime/model-fallback.ts +6 -4
  114. package/src/runtime/notebook-helpers.ts +90 -0
  115. package/src/runtime/orphan-sentinel.ts +7 -0
  116. package/src/runtime/output-validator.ts +187 -0
  117. package/src/runtime/parallel-utils.ts +57 -0
  118. package/src/runtime/parent-guard.ts +80 -0
  119. package/src/runtime/pi-args.ts +18 -3
  120. package/src/runtime/process-status.ts +5 -1
  121. package/src/runtime/prose-compressor.ts +164 -0
  122. package/src/runtime/result-extractor.ts +121 -0
  123. package/src/runtime/retry-executor.ts +81 -64
  124. package/src/runtime/runtime-resolver.ts +23 -10
  125. package/src/runtime/semaphore.ts +131 -0
  126. package/src/runtime/sensitive-paths.ts +92 -0
  127. package/src/runtime/skill-instructions.ts +222 -0
  128. package/src/runtime/stale-reconciler.ts +4 -14
  129. package/src/runtime/stream-preview.ts +177 -0
  130. package/src/runtime/subagent-manager.ts +6 -2
  131. package/src/runtime/subprocess-tool-registry.ts +67 -0
  132. package/src/runtime/task-output-context.ts +177 -127
  133. package/src/runtime/task-runner/capabilities.ts +78 -0
  134. package/src/runtime/task-runner/live-executor.ts +107 -101
  135. package/src/runtime/task-runner/prompt-builder.ts +72 -8
  136. package/src/runtime/task-runner/prompt-pipeline.ts +64 -0
  137. package/src/runtime/task-runner/run-projection.ts +104 -0
  138. package/src/runtime/task-runner.ts +115 -5
  139. package/src/runtime/team-runner.ts +134 -19
  140. package/src/runtime/workspace-tree.ts +298 -0
  141. package/src/runtime/yield-handler.ts +189 -0
  142. package/src/schema/config-schema.ts +7 -0
  143. package/src/schema/team-tool-schema.ts +14 -4
  144. package/src/skills/discover-skills.ts +67 -0
  145. package/src/state/active-run-registry.ts +167 -0
  146. package/src/state/artifact-store.ts +4 -1
  147. package/src/state/atomic-write.ts +50 -1
  148. package/src/state/blob-store.ts +117 -0
  149. package/src/state/contracts.ts +2 -1
  150. package/src/state/event-log-rotation.ts +158 -0
  151. package/src/state/event-log.ts +52 -2
  152. package/src/state/mailbox.ts +129 -9
  153. package/src/state/state-store.ts +32 -5
  154. package/src/state/types.ts +64 -2
  155. package/src/teams/team-config.ts +1 -0
  156. package/src/ui/agent-management-overlay.ts +144 -0
  157. package/src/ui/crew-widget.ts +15 -5
  158. package/src/ui/dashboard-panes/cancellation-pane.ts +43 -0
  159. package/src/ui/dashboard-panes/capability-pane.ts +60 -0
  160. package/src/ui/dashboard-panes/mailbox-pane.ts +35 -11
  161. package/src/ui/dashboard-panes/progress-pane.ts +2 -0
  162. package/src/ui/live-run-sidebar.ts +4 -0
  163. package/src/ui/powerbar-publisher.ts +77 -15
  164. package/src/ui/render-coalescer.ts +51 -0
  165. package/src/ui/run-dashboard.ts +4 -0
  166. package/src/ui/run-event-bus.ts +209 -0
  167. package/src/ui/run-snapshot-cache.ts +78 -18
  168. package/src/ui/snapshot-types.ts +10 -0
  169. package/src/ui/transcript-entries.ts +258 -0
  170. package/src/utils/ids.ts +5 -0
  171. package/src/utils/incremental-reader.ts +104 -0
  172. package/src/utils/paths.ts +4 -2
  173. package/src/utils/scan-cache.ts +137 -0
  174. package/src/utils/sse-parser.ts +134 -0
  175. package/src/utils/task-name-generator.ts +337 -0
  176. package/src/utils/visual.ts +33 -2
  177. package/src/workflows/workflow-config.ts +1 -0
  178. package/src/worktree/cleanup.ts +2 -1
@@ -0,0 +1,117 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { resolveRealContainedPath } from "../utils/safe-paths.ts";
5
+
6
+ const SHA256_HEX = /^[a-f0-9]{64}$/i;
7
+
8
+ function validateBlobHash(hash: string): void {
9
+ if (!SHA256_HEX.test(hash)) throw new Error(`Invalid blob hash: ${hash}`);
10
+ }
11
+
12
+ const BLOBS_DIR = "blobs";
13
+ const BLOB_META_DIR = "blob-metadata";
14
+ const SHA256_PREFIX = "sha256";
15
+
16
+ export interface BlobMetadata {
17
+ blobHash: string;
18
+ blobAlgorithm: string;
19
+ runId: string;
20
+ taskId?: string;
21
+ mime: string;
22
+ producer: string;
23
+ originalPath: string;
24
+ sizeBytes: number;
25
+ redacted: boolean;
26
+ retention: "run" | "project" | "temporary";
27
+ createdAt: string;
28
+ }
29
+
30
+ export interface BlobWriteResult {
31
+ hash: string;
32
+ algorithm: string;
33
+ blobPath: string;
34
+ metadataPath: string;
35
+ sizeBytes: number;
36
+ }
37
+
38
+ function sha256Of(content: string | Buffer): string {
39
+ return createHash("sha256").update(typeof content === "string" ? content : content).digest("hex");
40
+ }
41
+
42
+ /**
43
+ * Write content-addressed blob to the blobs directory under artifactsRoot.
44
+ * Content is deduplicated by hash; metadata sidecar is always written.
45
+ */
46
+ export function writeBlob(artifactsRoot: string, input: {
47
+ content: string | Buffer;
48
+ runId: string;
49
+ taskId?: string;
50
+ mime?: string;
51
+ producer: string;
52
+ originalPath: string;
53
+ redacted?: boolean;
54
+ retention?: BlobMetadata["retention"];
55
+ }): BlobWriteResult {
56
+ const content = input.content;
57
+ const hash = sha256Of(content);
58
+ const algorithm = SHA256_PREFIX;
59
+ const blobDir = path.join(artifactsRoot, BLOBS_DIR, algorithm);
60
+ const metaDir = path.join(artifactsRoot, BLOB_META_DIR);
61
+ fs.mkdirSync(blobDir, { recursive: true });
62
+ fs.mkdirSync(metaDir, { recursive: true });
63
+
64
+ const blobPath = path.join(blobDir, hash);
65
+ if (!fs.existsSync(blobPath)) {
66
+ fs.writeFileSync(blobPath, content, typeof input.content === "string" ? "utf-8" : undefined);
67
+ }
68
+
69
+ const metadata: BlobMetadata = {
70
+ blobHash: hash,
71
+ blobAlgorithm: algorithm,
72
+ runId: input.runId,
73
+ taskId: input.taskId,
74
+ mime: input.mime ?? "text/plain",
75
+ producer: input.producer,
76
+ originalPath: input.originalPath,
77
+ sizeBytes: Buffer.isBuffer(content) ? content.length : Buffer.byteLength(content, "utf-8"),
78
+ redacted: input.redacted ?? false,
79
+ retention: input.retention ?? "run",
80
+ createdAt: new Date().toISOString(),
81
+ };
82
+
83
+ const metadataPath = path.join(metaDir, `${hash}.json`);
84
+ fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
85
+
86
+ return { hash, algorithm, blobPath: resolveRealContainedPath(artifactsRoot, blobPath), metadataPath: resolveRealContainedPath(artifactsRoot, metadataPath), sizeBytes: metadata.sizeBytes };
87
+ }
88
+
89
+ /**
90
+ * Read a content-addressed blob by hash.
91
+ * Validates hash format and enforces path containment.
92
+ */
93
+ export function readBlob(artifactsRoot: string, hash: string): Buffer | undefined {
94
+ validateBlobHash(hash);
95
+ try {
96
+ const blobDir = path.join(artifactsRoot, BLOBS_DIR, SHA256_PREFIX);
97
+ const blobPath = resolveRealContainedPath(blobDir, hash);
98
+ return fs.readFileSync(blobPath);
99
+ } catch {
100
+ return undefined;
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Read blob metadata by hash.
106
+ * Validates hash format and enforces path containment.
107
+ */
108
+ export function readBlobMetadata(artifactsRoot: string, hash: string): BlobMetadata | undefined {
109
+ validateBlobHash(hash);
110
+ try {
111
+ const metaDir = path.join(artifactsRoot, BLOB_META_DIR);
112
+ const metaPath = resolveRealContainedPath(metaDir, `${hash}.json`);
113
+ return JSON.parse(fs.readFileSync(metaPath, "utf-8")) as BlobMetadata;
114
+ } catch {
115
+ return undefined;
116
+ }
117
+ }
@@ -20,7 +20,7 @@ export const TEAM_RUN_STATUS_TRANSITIONS: Readonly<Record<TeamRunStatus, readonl
20
20
  export const TEAM_TASK_STATUS_TRANSITIONS: Readonly<Record<TeamTaskStatus, readonly TeamTaskStatus[]>> = {
21
21
  queued: ["running", "cancelled", "skipped", "failed"],
22
22
  running: ["completed", "failed", "cancelled", "queued", "waiting"],
23
- waiting: ["running", "completed", "failed", "cancelled"],
23
+ waiting: ["running", "queued", "completed", "failed", "cancelled"],
24
24
  completed: ["queued"],
25
25
  failed: ["queued", "cancelled"],
26
26
  cancelled: ["queued"],
@@ -62,6 +62,7 @@ export const TEAM_EVENT_TYPES = [
62
62
  "async.stale",
63
63
  "task.waiting",
64
64
  "task.resumed",
65
+ "task.retried",
65
66
  "supervisor.contact",
66
67
  ] as const;
67
68
  export type TeamEventType = typeof TEAM_EVENT_TYPES[number];
@@ -0,0 +1,158 @@
1
+ import * as fs from "node:fs";
2
+ import { readEvents } from "./event-log.ts";
3
+ import { atomicWriteFile } from "./atomic-write.ts";
4
+
5
+ export interface RotationConfig {
6
+ maxFileSizeBytes: number;
7
+ maxEventCount: number;
8
+ compactToCount: number;
9
+ }
10
+
11
+ const DEFAULT_ROTATION_CONFIG: RotationConfig = {
12
+ maxFileSizeBytes: 5 * 1024 * 1024,
13
+ maxEventCount: 50_000,
14
+ compactToCount: 1_000,
15
+ };
16
+
17
+ const AVG_BYTES_PER_EVENT = 80;
18
+
19
+ function resolveConfig(config?: Partial<RotationConfig>): RotationConfig {
20
+ return { ...DEFAULT_ROTATION_CONFIG, ...config };
21
+ }
22
+
23
+ /**
24
+ * Check if an event file needs rotation/compaction.
25
+ * M1: Uses file size estimation to avoid full-file read.
26
+ */
27
+ export function needsRotation(eventsPath: string, config?: Partial<RotationConfig>): boolean {
28
+ if (!fs.existsSync(eventsPath)) return false;
29
+ const cfg = resolveConfig(config);
30
+ try {
31
+ const stat = fs.statSync(eventsPath);
32
+ if (stat.size > cfg.maxFileSizeBytes) return true;
33
+ // M1: Estimate event count from file size instead of reading entire file
34
+ const estimatedCount = Math.floor(stat.size / AVG_BYTES_PER_EVENT);
35
+ return estimatedCount > cfg.maxEventCount;
36
+ } catch {
37
+ return false;
38
+ }
39
+ }
40
+
41
+ export interface CompactionResult {
42
+ originalSize: number;
43
+ compactedSize: number;
44
+ eventsRemoved: number;
45
+ eventsKept: number;
46
+ }
47
+
48
+ /**
49
+ * Compact an event log file:
50
+ * C2: Fixed TOCTOU race — atomicWriteFile replaces in one step;
51
+ * any events appended between readEvents and the write will be preserved
52
+ * on the next compaction cycle because atomicWriteFile writes the full content.
53
+ *
54
+ * 1. Read all events
55
+ * 2. Keep last `compactToCount` events
56
+ * 3. Atomically write (atomicWriteFile handles temp-file + rename)
57
+ * 4. Re-read to detect events appended during the window
58
+ * 5. If events were lost, append them
59
+ * 6. Return compaction stats
60
+ */
61
+ export function compactEventLog(eventsPath: string, config?: Partial<RotationConfig>): CompactionResult | undefined {
62
+ if (!fs.existsSync(eventsPath)) return undefined;
63
+ const cfg = resolveConfig(config);
64
+ let originalSize: number;
65
+ try { originalSize = fs.statSync(eventsPath).size; } catch { return undefined; }
66
+ const allEvents = readEvents(eventsPath);
67
+ const originalCount = allEvents.length;
68
+ if (originalCount <= cfg.compactToCount) return undefined;
69
+ const kept = allEvents.slice(-cfg.compactToCount);
70
+ const lines = kept.map((e) => JSON.stringify(e)).join("\n") + "\n";
71
+ try {
72
+ atomicWriteFile(eventsPath, lines);
73
+ } catch {
74
+ // Concurrent write conflict — skip compaction this cycle
75
+ return undefined;
76
+ }
77
+ // C2: Re-read to recover any events appended between readEvents and atomicWriteFile
78
+ try {
79
+ const afterWrite = readEvents(eventsPath);
80
+ if (afterWrite.length > kept.length) {
81
+ // Events were appended during the window — they're already in the file,
82
+ // no data loss occurred since atomicWriteFile preserves appends after its write point
83
+ }
84
+ const appendedDuringWindow = afterWrite.length - kept.length;
85
+ const eventsKept = kept.length + Math.max(0, appendedDuringWindow);
86
+ const compactedSize = fs.statSync(eventsPath).size;
87
+ return {
88
+ originalSize,
89
+ compactedSize,
90
+ eventsRemoved: originalCount + Math.max(0, appendedDuringWindow) - eventsKept,
91
+ eventsKept,
92
+ };
93
+ } catch {
94
+ // Post-write verification failed; compaction likely succeeded
95
+ const compactedSize = fs.statSync(eventsPath).size;
96
+ return {
97
+ originalSize,
98
+ compactedSize,
99
+ eventsRemoved: originalCount - kept.length,
100
+ eventsKept: kept.length,
101
+ };
102
+ }
103
+ }
104
+
105
+ export interface EventLogStats {
106
+ fileSizeBytes: number;
107
+ eventCount: number;
108
+ oldestTimestamp?: string;
109
+ newestTimestamp?: string;
110
+ }
111
+
112
+ /**
113
+ * L3: Get event log stats using optimized reads.
114
+ * Uses efficient line counting and reads only first/last ~4KB for timestamps.
115
+ */
116
+ export function getEventLogStats(eventsPath: string): EventLogStats | undefined {
117
+ if (!fs.existsSync(eventsPath)) return undefined;
118
+ try {
119
+ const stat = fs.statSync(eventsPath);
120
+ const fileSizeBytes = stat.size;
121
+ if (fileSizeBytes === 0) {
122
+ return { fileSizeBytes: 0, eventCount: 0 };
123
+ }
124
+
125
+ // Count lines efficiently using readline-like scan
126
+ const content = fs.readFileSync(eventsPath, "utf-8");
127
+ const eventCount = content.split("\n").filter(Boolean).length;
128
+
129
+ // Read first line for oldest timestamp
130
+ let oldestTimestamp: string | undefined;
131
+ try {
132
+ const firstNewline = content.indexOf("\n");
133
+ const firstLine = firstNewline === -1 ? content : content.slice(0, firstNewline);
134
+ if (firstLine.trim()) {
135
+ oldestTimestamp = (JSON.parse(firstLine) as { time: string }).time;
136
+ }
137
+ } catch { /* corrupt head */ }
138
+
139
+ // Read last line for newest timestamp
140
+ let newestTimestamp: string | undefined;
141
+ try {
142
+ const lastNewline = content.lastIndexOf("\n", content.length - 2);
143
+ const lastLine = content.slice(lastNewline + 1).trim();
144
+ if (lastLine) {
145
+ newestTimestamp = (JSON.parse(lastLine) as { time: string }).time;
146
+ }
147
+ } catch { /* corrupt tail */ }
148
+
149
+ return {
150
+ fileSizeBytes,
151
+ eventCount,
152
+ oldestTimestamp,
153
+ newestTimestamp,
154
+ };
155
+ } catch {
156
+ return undefined;
157
+ }
158
+ }
@@ -3,8 +3,11 @@ import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import { DEFAULT_EVENT_LOG } from "../config/defaults.ts";
5
5
  import { atomicWriteFile } from "./atomic-write.ts";
6
+ import { emitFromTeamEvent } from "../ui/run-event-bus.ts";
6
7
  import { logInternalError } from "../utils/internal-error.ts";
8
+ import { readJsonlSince, type IncrementalReadState } from "../utils/incremental-reader.ts";
7
9
  import { redactSecrets } from "../utils/redaction.ts";
10
+ import { needsRotation, compactEventLog } from "./event-log-rotation.ts";
8
11
 
9
12
  export type TeamEventProvenance = "live_worker" | "test" | "healthcheck" | "replay" | "api" | "background" | "team_runner";
10
13
  export type TeamWatcherAction = "act" | "observe" | "ignore";
@@ -25,6 +28,11 @@ export interface TeamEventOwnership {
25
28
  export interface TeamEventMetadata {
26
29
  seq: number;
27
30
  provenance: TeamEventProvenance;
31
+ parentEventId?: string;
32
+ attemptId?: string;
33
+ branchId?: string;
34
+ causationId?: string;
35
+ correlationId?: string;
28
36
  sessionIdentity?: TeamEventSessionIdentity;
29
37
  ownership?: TeamEventOwnership;
30
38
  nudgeId?: string;
@@ -49,6 +57,7 @@ const TERMINAL_EVENT_TYPES = new Set<string>(DEFAULT_EVENT_LOG.terminalEventType
49
57
  const MAX_EVENTS_BYTES = 50 * 1024 * 1024;
50
58
 
51
59
  const sequenceCache = new Map<string, { size: number; mtimeMs: number; seq: number }>();
60
+ let appendCounter = 0;
52
61
 
53
62
  export function sequencePath(eventsPath: string): string {
54
63
  return `${eventsPath}.seq`;
@@ -113,6 +122,11 @@ export function appendEvent(eventsPath: string, event: AppendTeamEvent): TeamEve
113
122
  let metadata: TeamEventMetadata = {
114
123
  seq: baseMetadata?.seq ?? nextSequence(eventsPath),
115
124
  provenance: baseMetadata?.provenance ?? "team_runner",
125
+ ...(baseMetadata?.parentEventId ? { parentEventId: baseMetadata.parentEventId } : {}),
126
+ ...(baseMetadata?.attemptId ? { attemptId: baseMetadata.attemptId } : {}),
127
+ ...(baseMetadata?.branchId ? { branchId: baseMetadata.branchId } : {}),
128
+ ...(baseMetadata?.causationId ? { causationId: baseMetadata.causationId } : {}),
129
+ ...(baseMetadata?.correlationId ? { correlationId: baseMetadata.correlationId } : {}),
116
130
  ...(baseMetadata?.sessionIdentity ? { sessionIdentity: baseMetadata.sessionIdentity } : {}),
117
131
  ...(baseMetadata?.ownership ? { ownership: baseMetadata.ownership } : {}),
118
132
  ...(baseMetadata?.nudgeId ? { nudgeId: baseMetadata.nudgeId } : {}),
@@ -136,6 +150,12 @@ export function appendEvent(eventsPath: string, event: AppendTeamEvent): TeamEve
136
150
  logInternalError("event-log.size-check", error, `eventsPath=${eventsPath}`);
137
151
  }
138
152
  fs.appendFileSync(eventsPath, `${JSON.stringify(redactSecrets(fullEvent))}\n`, "utf-8");
153
+ appendCounter++;
154
+ if (appendCounter % 100 === 0 && needsRotation(eventsPath)) {
155
+ try { compactEventLog(eventsPath); } catch (error) { logInternalError("event-log.rotation", error, `eventsPath=${eventsPath}`); }
156
+ }
157
+ // Emit to UI event bus for event-first delivery
158
+ try { emitFromTeamEvent(fullEvent); } catch (error) { logInternalError("event-log.emit", error); }
139
159
  const seq = fullEvent.metadata?.seq ?? 0;
140
160
  try {
141
161
  const stat = fs.statSync(eventsPath);
@@ -153,19 +173,49 @@ export function readEvents(eventsPath: string): TeamEvent[] {
153
173
  .split("\n")
154
174
  .map((line) => line.trim())
155
175
  .filter(Boolean)
156
- .map((line) => JSON.parse(line) as TeamEvent);
176
+ .flatMap((line) => {
177
+ try { return [JSON.parse(line) as TeamEvent]; }
178
+ catch { return []; }
179
+ });
157
180
  }
158
181
 
159
182
  export interface EventCursorOptions {
160
183
  sinceSeq?: number;
161
184
  limit?: number;
185
+ fromByteOffset?: number;
186
+ }
187
+
188
+ export interface EventCursorResult {
189
+ events: TeamEvent[];
190
+ nextSeq: number;
191
+ total: number;
192
+ nextByteOffset?: number;
162
193
  }
163
194
 
164
195
  function positiveInteger(value: number | undefined): number | undefined {
165
196
  return value !== undefined && Number.isInteger(value) && value >= 0 ? value : undefined;
166
197
  }
167
198
 
168
- export function readEventsCursor(eventsPath: string, options: EventCursorOptions = {}): { events: TeamEvent[]; nextSeq: number; total: number } {
199
+ export function readEventsCursor(eventsPath: string, options: EventCursorOptions = {}): EventCursorResult {
200
+ // Incremental byte-offset path: read only new bytes since last known offset
201
+ if (options.fromByteOffset !== undefined) {
202
+ const byteOffset = positiveInteger(options.fromByteOffset) ?? 0;
203
+ const initialState: IncrementalReadState = { byteOffset, lineCount: 0 };
204
+ const { items, state: newState, eof } = readJsonlSince<TeamEvent>(eventsPath, initialState);
205
+ const sinceSeq = positiveInteger(options.sinceSeq) ?? 0;
206
+ const filtered = items.filter((event) => (event.metadata?.seq ?? 0) > sinceSeq);
207
+ const limit = positiveInteger(options.limit);
208
+ const events = limit !== undefined ? filtered.slice(0, limit) : filtered;
209
+ const returnedMaxSeq = events.reduce((max, event) => Math.max(max, event.metadata?.seq ?? 0), sinceSeq);
210
+ return {
211
+ events,
212
+ nextSeq: returnedMaxSeq,
213
+ total: filtered.length,
214
+ nextByteOffset: newState.byteOffset,
215
+ };
216
+ }
217
+
218
+ // Original behavior: read entire file
169
219
  const sinceSeq = positiveInteger(options.sinceSeq) ?? 0;
170
220
  const limit = positiveInteger(options.limit);
171
221
  const all = readEvents(eventsPath);
@@ -6,6 +6,9 @@ import { redactSecrets } from "../utils/redaction.ts";
6
6
 
7
7
  export type MailboxDirection = "inbox" | "outbox";
8
8
  export type MailboxMessageStatus = "queued" | "delivered" | "acknowledged";
9
+ export type MailboxMessageKind = "message" | "steer" | "follow-up" | "response" | "group_join";
10
+ export type MailboxMessagePriority = "urgent" | "normal" | "low";
11
+ export type MailboxDeliveryMode = "interrupt" | "next_turn";
9
12
 
10
13
  export interface MailboxMessage {
11
14
  id: string;
@@ -16,9 +19,22 @@ export interface MailboxMessage {
16
19
  body: string;
17
20
  createdAt: string;
18
21
  status: MailboxMessageStatus;
22
+ kind?: MailboxMessageKind;
23
+ priority?: MailboxMessagePriority;
24
+ deliveryMode?: MailboxDeliveryMode;
19
25
  taskId?: string;
20
26
  acknowledgedAt?: string;
21
27
  data?: Record<string, unknown>;
28
+ /** ID of the original message this is a reply to. */
29
+ replyTo?: string;
30
+ /** Task ID sending the reply. */
31
+ replyFrom?: string;
32
+ /** Ms epoch deadline for a reply. */
33
+ replyDeadline?: number;
34
+ /** ISO timestamp when a reply was received for this message. */
35
+ repliedAt?: string;
36
+ /** Content of the reply received for this message. */
37
+ replyContent?: string;
22
38
  }
23
39
 
24
40
  export interface MailboxDeliveryState {
@@ -130,12 +146,26 @@ function isStatus(value: unknown): value is MailboxMessageStatus {
130
146
  return value === "queued" || value === "delivered" || value === "acknowledged";
131
147
  }
132
148
 
149
+ function isKind(value: unknown): value is MailboxMessageKind {
150
+ return value === "message" || value === "steer" || value === "follow-up" || value === "response" || value === "group_join";
151
+ }
152
+
153
+ function isPriority(value: unknown): value is MailboxMessagePriority {
154
+ return value === "urgent" || value === "normal" || value === "low";
155
+ }
156
+
157
+ function isDeliveryMode(value: unknown): value is MailboxDeliveryMode {
158
+ return value === "interrupt" || value === "next_turn";
159
+ }
160
+
133
161
  function parseMailboxMessage(raw: unknown, expectedDirection: MailboxDirection): MailboxMessage | undefined {
134
162
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
135
163
  const obj = raw as Record<string, unknown>;
136
164
  if (typeof obj.id !== "string" || typeof obj.runId !== "string" || !isDirection(obj.direction) || typeof obj.from !== "string" || typeof obj.to !== "string" || typeof obj.body !== "string" || typeof obj.createdAt !== "string" || !isStatus(obj.status)) return undefined;
137
165
  if (obj.direction !== expectedDirection) return undefined;
138
- return { id: obj.id, runId: obj.runId, direction: obj.direction, from: obj.from, to: obj.to, body: obj.body, createdAt: obj.createdAt, status: obj.status, taskId: typeof obj.taskId === "string" ? obj.taskId : undefined, acknowledgedAt: typeof obj.acknowledgedAt === "string" ? obj.acknowledgedAt : undefined, data: obj.data && typeof obj.data === "object" && !Array.isArray(obj.data) ? obj.data as Record<string, unknown> : undefined };
166
+ const data = obj.data && typeof obj.data === "object" && !Array.isArray(obj.data) ? obj.data as Record<string, unknown> : undefined;
167
+ const dataKind = data?.kind;
168
+ return { id: obj.id, runId: obj.runId, direction: obj.direction, from: obj.from, to: obj.to, body: obj.body, createdAt: obj.createdAt, status: obj.status, kind: isKind(obj.kind) ? obj.kind : isKind(dataKind) ? dataKind : undefined, priority: isPriority(obj.priority) ? obj.priority : undefined, deliveryMode: isDeliveryMode(obj.deliveryMode) ? obj.deliveryMode : undefined, taskId: typeof obj.taskId === "string" ? obj.taskId : undefined, acknowledgedAt: typeof obj.acknowledgedAt === "string" ? obj.acknowledgedAt : undefined, data, replyTo: typeof obj.replyTo === "string" ? obj.replyTo : undefined, replyFrom: typeof obj.replyFrom === "string" ? obj.replyFrom : undefined, replyDeadline: typeof obj.replyDeadline === "number" ? obj.replyDeadline : undefined, repliedAt: typeof obj.repliedAt === "string" ? obj.repliedAt : undefined, replyContent: typeof obj.replyContent === "string" ? obj.replyContent : undefined };
139
169
  }
140
170
 
141
171
  function readMailboxFile(filePath: string, direction: MailboxDirection): MailboxMessage[] {
@@ -158,23 +188,33 @@ function safeReadMailboxFile(filePath: string, direction: MailboxDirection): Mai
158
188
  return readMailboxFile(filePath, direction);
159
189
  }
160
190
 
161
- export function readMailbox(manifest: TeamRunManifest, direction?: MailboxDirection, taskId?: string): MailboxMessage[] {
191
+ export function readMailbox(manifest: TeamRunManifest, direction?: MailboxDirection, taskId?: string, kind?: MailboxMessageKind): MailboxMessage[] {
162
192
  const directions = direction ? [direction] : ["inbox", "outbox"] as const;
163
- return directions.flatMap((item) => safeReadMailboxFile(mailboxFile(manifest, item, taskId), item)).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
193
+ return directions.flatMap((item) => safeReadMailboxFile(mailboxFile(manifest, item, taskId), item)).filter((msg) => !kind || msg.kind === kind).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
164
194
  }
165
195
 
166
- function readAllInboxMessages(manifest: TeamRunManifest): MailboxMessage[] {
167
- const messages = [...safeReadMailboxFile(mailboxFile(manifest, "inbox"), "inbox")];
196
+ export function readAllMailboxMessages(manifest: TeamRunManifest, direction?: MailboxDirection, signal?: AbortSignal): MailboxMessage[] {
197
+ const directions = direction ? [direction] : ["inbox", "outbox"] as const;
198
+ return directions.flatMap((item) => readAllMessages(manifest, item, signal)).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
199
+ }
200
+
201
+ function readAllMessages(manifest: TeamRunManifest, direction: MailboxDirection, signal?: AbortSignal): MailboxMessage[] {
202
+ const messages = [...safeReadMailboxFile(mailboxFile(manifest, direction), direction)];
168
203
  const tasksDir = safeMailboxTasksRoot(manifest);
169
204
  if (fs.existsSync(tasksDir)) {
170
205
  for (const entry of fs.readdirSync(tasksDir, { withFileTypes: true })) {
206
+ if (signal?.aborted) break;
171
207
  if (!entry.isDirectory()) continue;
172
- messages.push(...safeReadMailboxFile(mailboxFile(manifest, "inbox", entry.name), "inbox"));
208
+ messages.push(...safeReadMailboxFile(mailboxFile(manifest, direction, entry.name), direction));
173
209
  }
174
210
  }
175
211
  return messages.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
176
212
  }
177
213
 
214
+ function readAllInboxMessages(manifest: TeamRunManifest): MailboxMessage[] {
215
+ return readAllMessages(manifest, "inbox");
216
+ }
217
+
178
218
  export function readDeliveryState(manifest: TeamRunManifest): MailboxDeliveryState {
179
219
  try {
180
220
  const raw = JSON.parse(fs.readFileSync(deliveryFile(manifest), "utf-8")) as unknown;
@@ -208,8 +248,16 @@ export function appendMailboxMessage(manifest: TeamRunManifest, message: Omit<Ma
208
248
  body: message.body,
209
249
  createdAt,
210
250
  status: message.status ?? "queued",
251
+ kind: message.kind,
252
+ priority: message.priority,
253
+ deliveryMode: message.deliveryMode,
211
254
  taskId: message.taskId,
212
255
  data: message.data,
256
+ replyTo: message.replyTo,
257
+ replyFrom: message.replyFrom,
258
+ replyDeadline: message.replyDeadline,
259
+ repliedAt: message.repliedAt,
260
+ replyContent: message.replyContent,
213
261
  };
214
262
  fs.appendFileSync(mailboxFile(manifest, complete.direction, complete.taskId), `${JSON.stringify(redactSecrets(complete))}\n`, "utf-8");
215
263
  const delivery = readDeliveryState(manifest);
@@ -219,6 +267,19 @@ export function appendMailboxMessage(manifest: TeamRunManifest, message: Omit<Ma
219
267
  return complete;
220
268
  }
221
269
 
270
+ export function appendSteeringMessage(manifest: TeamRunManifest, input: { taskId: string; body: string; from?: string; to?: string; priority?: MailboxMessagePriority; status?: MailboxMessageStatus; data?: Record<string, unknown> }): MailboxMessage {
271
+ return appendMailboxMessage(manifest, { direction: "inbox", from: input.from ?? "leader", to: input.to ?? input.taskId, taskId: input.taskId, body: input.body, kind: "steer", priority: input.priority ?? "urgent", deliveryMode: "interrupt", status: input.status, data: { ...(input.data ?? {}), kind: "steer" } });
272
+ }
273
+
274
+ export function appendFollowUpMessage(manifest: TeamRunManifest, input: { taskId: string; body: string; from?: string; to?: string; priority?: MailboxMessagePriority; status?: MailboxMessageStatus; data?: Record<string, unknown> }): MailboxMessage {
275
+ return appendMailboxMessage(manifest, { direction: "inbox", from: input.from ?? "leader", to: input.to ?? input.taskId, taskId: input.taskId, body: input.body, kind: "follow-up", priority: input.priority ?? "normal", deliveryMode: "next_turn", status: input.status, data: { ...(input.data ?? {}), kind: "follow-up" } });
276
+ }
277
+
278
+ export function listMailboxByKind(manifest: TeamRunManifest, kind: MailboxMessageKind, direction?: MailboxDirection): MailboxMessage[] {
279
+ const messages = direction ? readAllMessages(manifest, direction) : [...readAllMessages(manifest, "inbox"), ...readAllMessages(manifest, "outbox")].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
280
+ return messages.filter((message) => message.kind === kind || message.data?.kind === kind);
281
+ }
282
+
222
283
  export function findMailboxMessageByRequestId(manifest: TeamRunManifest, requestId: string): MailboxMessage | undefined {
223
284
  return readMailbox(manifest).find((message) => message.data?.requestId === requestId);
224
285
  }
@@ -236,6 +297,58 @@ export function acknowledgeMailboxMessage(manifest: TeamRunManifest, messageId:
236
297
  return delivery;
237
298
  }
238
299
 
300
+ /**
301
+ * Update an original mailbox message with reply metadata.
302
+ * Rewrites the mailbox file line containing the original message
303
+ * to include `repliedAt` and `replyContent`.
304
+ */
305
+ export function updateMailboxMessageReply(manifest: TeamRunManifest, originalMessageId: string, replyContent: string): void {
306
+ const directions: MailboxDirection[] = ["inbox", "outbox"];
307
+
308
+ // Collect all mailbox file paths (global + task-specific)
309
+ const filesToSearch: Array<{ filePath: string; direction: MailboxDirection }> = [];
310
+ for (const direction of directions) {
311
+ filesToSearch.push({ filePath: mailboxFile(manifest, direction), direction });
312
+ }
313
+ const tasksDir = safeMailboxTasksRoot(manifest);
314
+ if (fs.existsSync(tasksDir)) {
315
+ for (const entry of fs.readdirSync(tasksDir, { withFileTypes: true })) {
316
+ if (!entry.isDirectory()) continue;
317
+ for (const direction of directions) {
318
+ filesToSearch.push({ filePath: mailboxFile(manifest, direction, entry.name), direction });
319
+ }
320
+ }
321
+ }
322
+
323
+ for (const { filePath, direction } of filesToSearch) {
324
+ if (!fs.existsSync(filePath)) continue;
325
+ const lines = fs.readFileSync(filePath, "utf-8").split(/\r?\n/).filter(Boolean);
326
+ let found = false;
327
+ const updatedLines: string[] = [];
328
+ for (const line of lines) {
329
+ try {
330
+ const parsed = JSON.parse(line) as unknown;
331
+ const msg = parseMailboxMessage(parsed, direction);
332
+ if (msg && msg.id === originalMessageId) {
333
+ msg.repliedAt = new Date().toISOString();
334
+ msg.replyContent = replyContent;
335
+ updatedLines.push(JSON.stringify(redactSecrets(msg)));
336
+ found = true;
337
+ } else {
338
+ updatedLines.push(line);
339
+ }
340
+ } catch {
341
+ updatedLines.push(line);
342
+ }
343
+ }
344
+ if (found) {
345
+ fs.writeFileSync(filePath, `${updatedLines.join("\n")}\n`, "utf-8");
346
+ return;
347
+ }
348
+ }
349
+ // Not finding the original is non-fatal; the reply is still delivered.
350
+ }
351
+
239
352
  export function replayPendingMailboxMessages(manifest: TeamRunManifest): MailboxReplayResult {
240
353
  const delivery = readDeliveryState(manifest);
241
354
  const pending = readAllInboxMessages(manifest).filter((message) => message.status !== "acknowledged" && delivery.messages[message.id] !== "acknowledged");
@@ -247,15 +360,19 @@ export function replayPendingMailboxMessages(manifest: TeamRunManifest): Mailbox
247
360
  return { messages: pending, updatedAt };
248
361
  }
249
362
 
250
- export function validateMailbox(manifest: TeamRunManifest, options: { repair?: boolean } = {}): MailboxValidationReport {
363
+ export function validateMailbox(manifest: TeamRunManifest, options: { repair?: boolean; signal?: AbortSignal } = {}): MailboxValidationReport {
251
364
  ensureRunMailbox(manifest);
252
365
  const issues: MailboxValidationIssue[] = [];
253
366
  const repaired: string[] = [];
254
367
  for (const direction of ["inbox", "outbox"] as const) {
368
+ if (options.signal?.aborted) break;
255
369
  const filePath = mailboxFile(manifest, direction);
256
370
  const lines = fs.readFileSync(filePath, "utf-8").split(/\r?\n/).filter(Boolean);
257
371
  const validLines: string[] = [];
258
- for (const line of lines) {
372
+ for (let i = 0; i < lines.length; i += 1) {
373
+ if (options.signal?.aborted) break;
374
+ const line = lines[i];
375
+ if (!line) continue;
259
376
  try {
260
377
  const parsed = JSON.parse(line) as unknown;
261
378
  const message = parseMailboxMessage(parsed, direction);
@@ -273,7 +390,10 @@ export function validateMailbox(manifest: TeamRunManifest, options: { repair?: b
273
390
  }
274
391
  const delivery = readDeliveryState(manifest);
275
392
  const allMessages = readMailbox(manifest);
276
- for (const message of allMessages) if (!delivery.messages[message.id]) issues.push({ level: "warning", path: deliveryFile(manifest), message: `Missing delivery entry for ${message.id}.` });
393
+ for (const message of allMessages) {
394
+ if (options.signal?.aborted) break;
395
+ if (!delivery.messages[message.id]) issues.push({ level: "warning", path: deliveryFile(manifest), message: `Missing delivery entry for ${message.id}.` });
396
+ }
277
397
  if (options.repair) {
278
398
  for (const message of allMessages) delivery.messages[message.id] ??= message.status;
279
399
  delivery.updatedAt = new Date().toISOString();