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.
- package/CHANGELOG.md +97 -0
- package/README.md +5 -5
- package/agents/analyst.md +11 -11
- package/agents/critic.md +11 -11
- package/agents/executor.md +11 -11
- package/agents/explorer.md +11 -11
- package/agents/planner.md +11 -11
- package/agents/reviewer.md +11 -11
- package/agents/security-reviewer.md +11 -11
- package/agents/test-engineer.md +11 -11
- package/agents/verifier.md +11 -11
- package/agents/writer.md +11 -11
- package/docs/next-upgrade-roadmap.md +808 -0
- package/docs/research/AGENT-EXECUTION-ARCHITECTURE.md +261 -0
- package/docs/research/AGENT-LIFECYCLE-COMPARISON.md +111 -0
- package/docs/research/AUDIT_OH_MY_PI.md +261 -0
- package/docs/research/AUDIT_PI_CREW.md +457 -0
- package/docs/research/CAVEMAN-DEEP-RESEARCH.md +281 -0
- package/docs/research/COMPARISON_OH_MY_PI_VS_PI_CREW.md +264 -0
- package/docs/research/DEEP-RESEARCH-PI-POWERBAR.md +343 -0
- package/docs/research/DEEP_RESEARCH_SUBAGENT_ARCHITECTURE.md +480 -0
- package/docs/research/GAP_CLOSURE_IMPLEMENTATION_PLAN.md +354 -0
- package/docs/research/IMPLEMENTATION_PLAN.md +385 -0
- package/docs/research/LIVE-SESSION-PRODUCTION-READY-PLAN.md +502 -0
- package/docs/research/OH-MY-PI-DEEP-RESEARCH-v14.7.6.md +266 -0
- package/docs/research/REMAINING-GAPS-PLAN.md +363 -0
- package/docs/research/SESSION-SUMMARY-2026-05-08.md +146 -0
- package/docs/research/UI-RESPONSIVENESS-AUDIT.md +173 -0
- package/docs/research-awesome-agent-skills-distillation.md +100 -0
- package/docs/research-oh-my-pi-distillation.md +369 -0
- package/docs/source-runtime-refactor-map.md +24 -0
- package/docs/usage.md +3 -3
- package/install.mjs +52 -8
- package/package.json +99 -98
- package/schema.json +10 -1
- package/skills/async-worker-recovery/SKILL.md +42 -0
- package/skills/context-artifact-hygiene/SKILL.md +52 -0
- package/skills/delegation-patterns/SKILL.md +54 -0
- package/skills/mailbox-interactive/SKILL.md +40 -0
- package/skills/model-routing-context/SKILL.md +39 -0
- package/skills/multi-perspective-review/SKILL.md +58 -0
- package/skills/observability-reliability/SKILL.md +41 -0
- package/skills/orchestration/SKILL.md +157 -0
- package/skills/ownership-session-security/SKILL.md +41 -0
- package/skills/pi-extension-lifecycle/SKILL.md +39 -0
- package/skills/requirements-to-task-packet/SKILL.md +63 -0
- package/skills/resource-discovery-config/SKILL.md +41 -0
- package/skills/runtime-state-reader/SKILL.md +44 -0
- package/skills/secure-agent-orchestration-review/SKILL.md +45 -0
- package/skills/state-mutation-locking/SKILL.md +42 -0
- package/skills/systematic-debugging/SKILL.md +67 -0
- package/skills/ui-render-performance/SKILL.md +39 -0
- package/skills/verification-before-done/SKILL.md +57 -0
- package/skills/worktree-isolation/SKILL.md +39 -0
- package/src/agents/agent-config.ts +6 -0
- package/src/agents/agent-search.ts +98 -0
- package/src/agents/agent-serializer.ts +38 -34
- package/src/agents/discover-agents.ts +29 -15
- package/src/config/config.ts +72 -24
- package/src/config/defaults.ts +25 -0
- package/src/extension/autonomous-policy.ts +26 -33
- package/src/extension/help.ts +1 -0
- package/src/extension/management.ts +5 -0
- package/src/extension/project-init.ts +62 -2
- package/src/extension/register.ts +69 -22
- package/src/extension/registration/commands.ts +64 -25
- package/src/extension/registration/compaction-guard.ts +1 -1
- package/src/extension/registration/subagent-helpers.ts +8 -0
- package/src/extension/registration/subagent-tools.ts +149 -148
- package/src/extension/registration/team-tool.ts +14 -10
- package/src/extension/run-index.ts +35 -21
- package/src/extension/run-maintenance.ts +30 -5
- package/src/extension/team-tool/api.ts +47 -9
- package/src/extension/team-tool/cancel.ts +109 -5
- package/src/extension/team-tool/context.ts +8 -0
- package/src/extension/team-tool/intent-policy.ts +42 -0
- package/src/extension/team-tool/lifecycle-actions.ts +120 -79
- package/src/extension/team-tool/parallel-dispatch.ts +156 -0
- package/src/extension/team-tool/respond.ts +46 -18
- package/src/extension/team-tool/run.ts +55 -12
- package/src/extension/team-tool/status.ts +13 -2
- package/src/extension/team-tool-types.ts +3 -0
- package/src/extension/team-tool.ts +45 -14
- package/src/hooks/registry.ts +61 -0
- package/src/hooks/types.ts +41 -0
- package/src/observability/event-to-metric.ts +8 -1
- package/src/runtime/agent-control.ts +169 -63
- package/src/runtime/async-runner.ts +3 -1
- package/src/runtime/background-runner.ts +78 -53
- package/src/runtime/cancellation-token.ts +89 -0
- package/src/runtime/cancellation.ts +61 -0
- package/src/runtime/capability-inventory.ts +116 -0
- package/src/runtime/child-pi.ts +458 -444
- package/src/runtime/code-summary.ts +247 -0
- package/src/runtime/crash-recovery.ts +182 -0
- package/src/runtime/crew-agent-records.ts +70 -10
- package/src/runtime/crew-agent-runtime.ts +1 -0
- package/src/runtime/custom-tools/irc-tool.ts +201 -0
- package/src/runtime/custom-tools/submit-result-tool.ts +90 -0
- package/src/runtime/deadletter.ts +1 -0
- package/src/runtime/delivery-coordinator.ts +48 -25
- package/src/runtime/effectiveness.ts +81 -0
- package/src/runtime/event-stream-bridge.ts +90 -0
- package/src/runtime/live-agent-control.ts +2 -1
- package/src/runtime/live-agent-manager.ts +179 -85
- package/src/runtime/live-control-realtime.ts +1 -1
- package/src/runtime/live-extension-bridge.ts +150 -0
- package/src/runtime/live-irc.ts +92 -0
- package/src/runtime/live-session-health.ts +100 -0
- package/src/runtime/live-session-runtime.ts +599 -305
- package/src/runtime/manifest-cache.ts +17 -2
- package/src/runtime/mcp-proxy.ts +113 -0
- package/src/runtime/model-fallback.ts +6 -4
- package/src/runtime/notebook-helpers.ts +90 -0
- package/src/runtime/orphan-sentinel.ts +7 -0
- package/src/runtime/output-validator.ts +187 -0
- package/src/runtime/parallel-utils.ts +57 -0
- package/src/runtime/parent-guard.ts +80 -0
- package/src/runtime/pi-args.ts +18 -3
- package/src/runtime/process-status.ts +5 -1
- package/src/runtime/prose-compressor.ts +164 -0
- package/src/runtime/result-extractor.ts +121 -0
- package/src/runtime/retry-executor.ts +81 -64
- package/src/runtime/runtime-resolver.ts +23 -10
- package/src/runtime/semaphore.ts +131 -0
- package/src/runtime/sensitive-paths.ts +92 -0
- package/src/runtime/skill-instructions.ts +222 -0
- package/src/runtime/stale-reconciler.ts +4 -14
- package/src/runtime/stream-preview.ts +177 -0
- package/src/runtime/subagent-manager.ts +6 -2
- package/src/runtime/subprocess-tool-registry.ts +67 -0
- package/src/runtime/task-output-context.ts +177 -127
- package/src/runtime/task-runner/capabilities.ts +78 -0
- package/src/runtime/task-runner/live-executor.ts +107 -101
- package/src/runtime/task-runner/prompt-builder.ts +72 -8
- package/src/runtime/task-runner/prompt-pipeline.ts +64 -0
- package/src/runtime/task-runner/run-projection.ts +104 -0
- package/src/runtime/task-runner.ts +115 -5
- package/src/runtime/team-runner.ts +134 -19
- package/src/runtime/workspace-tree.ts +298 -0
- package/src/runtime/yield-handler.ts +189 -0
- package/src/schema/config-schema.ts +7 -0
- package/src/schema/team-tool-schema.ts +14 -4
- package/src/skills/discover-skills.ts +67 -0
- package/src/state/active-run-registry.ts +167 -0
- package/src/state/artifact-store.ts +4 -1
- package/src/state/atomic-write.ts +50 -1
- package/src/state/blob-store.ts +117 -0
- package/src/state/contracts.ts +2 -1
- package/src/state/event-log-rotation.ts +158 -0
- package/src/state/event-log.ts +52 -2
- package/src/state/mailbox.ts +129 -9
- package/src/state/state-store.ts +32 -5
- package/src/state/types.ts +64 -2
- package/src/teams/team-config.ts +1 -0
- package/src/ui/agent-management-overlay.ts +144 -0
- package/src/ui/crew-widget.ts +15 -5
- package/src/ui/dashboard-panes/cancellation-pane.ts +43 -0
- package/src/ui/dashboard-panes/capability-pane.ts +60 -0
- package/src/ui/dashboard-panes/mailbox-pane.ts +35 -11
- package/src/ui/dashboard-panes/progress-pane.ts +2 -0
- package/src/ui/live-run-sidebar.ts +4 -0
- package/src/ui/powerbar-publisher.ts +77 -15
- package/src/ui/render-coalescer.ts +51 -0
- package/src/ui/run-dashboard.ts +4 -0
- package/src/ui/run-event-bus.ts +209 -0
- package/src/ui/run-snapshot-cache.ts +78 -18
- package/src/ui/snapshot-types.ts +10 -0
- package/src/ui/transcript-entries.ts +258 -0
- package/src/utils/ids.ts +5 -0
- package/src/utils/incremental-reader.ts +104 -0
- package/src/utils/paths.ts +4 -2
- package/src/utils/scan-cache.ts +137 -0
- package/src/utils/sse-parser.ts +134 -0
- package/src/utils/task-name-generator.ts +337 -0
- package/src/utils/visual.ts +33 -2
- package/src/workflows/workflow-config.ts +1 -0
- 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
|
+
}
|
package/src/state/contracts.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/state/event-log.ts
CHANGED
|
@@ -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
|
-
.
|
|
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 = {}):
|
|
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);
|
package/src/state/mailbox.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
167
|
-
const
|
|
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,
|
|
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 (
|
|
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)
|
|
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();
|