pi-crew 0.2.3 → 0.2.5
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/AGENTS.md +57 -32
- package/CHANGELOG.md +466 -448
- package/LICENSE +21 -21
- package/NOTICE.md +16 -16
- package/README.md +323 -323
- package/docs/FEATURE_INTAKE.md +126 -0
- package/docs/HARNESS.md +86 -0
- package/docs/HARNESS_BACKLOG.md +41 -0
- package/docs/TEST_MATRIX.md +49 -0
- package/docs/actions-reference.md +595 -595
- package/docs/architecture.md +180 -180
- package/docs/code-review-2026-05-11.md +592 -592
- package/docs/commands-reference.md +347 -347
- package/docs/comparison-pi-subagents-vs-pi-crew.md +303 -0
- package/docs/decisions/0001-durable-state.md +41 -0
- package/docs/decisions/0002-child-process-for-async.md +42 -0
- package/docs/decisions/0003-depth-guard.md +36 -0
- package/docs/decisions/0004-execfile-over-exec.md +34 -0
- package/docs/decisions/0005-no-parameter-properties.md +49 -0
- package/docs/decisions/0006-publish-bundled-esm.md +63 -0
- package/docs/decisions/0007-active-run-binary-index.md +54 -0
- package/docs/decisions/0008-child-pi-warm-pool.md +61 -0
- package/docs/decisions/README.md +23 -0
- package/docs/followup-review-round4-2026-05-13.md +107 -0
- package/docs/implementation-plan-top3.md +333 -0
- package/docs/live-mailbox-runtime.md +36 -36
- package/docs/next-upgrade-roadmap.md +808 -808
- package/docs/oh-my-pi-research.md +509 -0
- package/docs/perf/baseline-2026-05.md +113 -0
- package/docs/perf/final-report-2026-05.md +206 -0
- package/docs/perf/sprint-1-report.md +71 -0
- package/docs/perf/sprint-2-report.md +81 -0
- package/docs/perf/sprint-2.5-report.md +53 -0
- package/docs/perf/sprint-3-report.md +36 -0
- package/docs/perf/sprint-4-report.md +47 -0
- package/docs/perf/sprint-5-report.md +51 -0
- package/docs/perf/sprint-6-report.md +94 -0
- package/docs/perf/sprint-7-report.md +74 -0
- package/docs/perf/upgrade-plan-2026-05.md +147 -0
- package/docs/pi-subagents3-deep-analysis.md +508 -0
- package/docs/product/README.md +31 -0
- package/docs/product/platform.md +27 -0
- package/docs/product/runtime-safety.md +37 -0
- package/docs/product/team-run.md +39 -0
- package/docs/product/team-tool.md +37 -0
- package/docs/publishing.md +65 -65
- package/docs/resource-formats.md +134 -134
- package/docs/runtime-analysis-child-vs-live.md +171 -0
- package/docs/runtime-flow.md +148 -148
- package/docs/runtime-migration-in-process-analysis.md +250 -0
- package/docs/stories/README.md +30 -0
- package/docs/stories/backlog.md +36 -0
- package/docs/templates/decision.md +27 -0
- package/docs/templates/story.md +44 -0
- package/docs/templates/validation-report.md +32 -0
- package/docs/usage.md +238 -238
- package/index.ts +7 -6
- package/install.mjs +65 -65
- package/package.json +107 -100
- package/schema.json +222 -222
- package/skills/child-pi-spawning/SKILL.md +213 -0
- package/skills/context-artifact-hygiene/SKILL.md +32 -0
- package/skills/event-log-tracing/SKILL.md +299 -0
- package/skills/git-master/SKILL.md +225 -24
- package/skills/live-agent-lifecycle/SKILL.md +192 -0
- package/skills/mailbox-interactive/SKILL.md +300 -19
- package/skills/model-routing-context/SKILL.md +94 -0
- package/skills/multi-perspective-review/SKILL.md +88 -0
- package/skills/read-only-explorer/SKILL.md +250 -26
- package/skills/safe-bash/SKILL.md +307 -21
- package/skills/verification-before-done/SKILL.md +11 -2
- package/skills/widget-rendering/SKILL.md +258 -0
- package/skills/workspace-isolation/SKILL.md +202 -0
- package/skills/worktree-isolation/SKILL.md +202 -18
- package/src/adapters/claude-adapter.ts +25 -25
- package/src/adapters/codex-adapter.ts +21 -21
- package/src/adapters/cursor-adapter.ts +17 -17
- package/src/adapters/export-util.ts +137 -137
- package/src/adapters/index.ts +15 -15
- package/src/adapters/registry.ts +18 -18
- package/src/adapters/types.ts +23 -23
- package/src/agents/agent-config.ts +38 -38
- package/src/agents/agent-serializer.ts +38 -38
- package/src/agents/discover-agents.ts +121 -118
- package/src/config/config.ts +740 -858
- package/src/config/defaults.ts +96 -96
- package/src/config/drift-detector.ts +211 -211
- package/src/config/markers.ts +327 -327
- package/src/config/resilient-parser.ts +109 -108
- package/src/config/suggestions.ts +74 -74
- package/src/config/types.ts +199 -0
- package/src/extension/async-notifier.ts +123 -89
- package/src/extension/autonomous-policy.ts +169 -169
- package/src/extension/cross-extension-rpc.ts +104 -104
- package/src/extension/help.ts +47 -47
- package/src/extension/import-index.ts +69 -69
- package/src/extension/management.ts +395 -382
- package/src/extension/notification-router.ts +116 -116
- package/src/extension/notification-sink.ts +51 -51
- package/src/extension/project-init.ts +168 -168
- package/src/extension/register.ts +859 -668
- package/src/extension/registration/artifact-cleanup.ts +15 -15
- package/src/extension/registration/command-utils.ts +54 -54
- package/src/extension/registration/commands.ts +559 -452
- package/src/extension/registration/compaction-guard.ts +125 -125
- package/src/extension/registration/subagent-helpers.ts +102 -102
- package/src/extension/registration/subagent-tools.ts +220 -159
- package/src/extension/registration/team-tool.ts +159 -99
- package/src/extension/registration/viewers.ts +29 -0
- package/src/extension/result-watcher.ts +128 -128
- package/src/extension/run-bundle-schema.ts +89 -89
- package/src/extension/run-export.ts +73 -73
- package/src/extension/run-import.ts +84 -84
- package/src/extension/run-index.ts +94 -94
- package/src/extension/run-maintenance.ts +142 -142
- package/src/extension/session-summary.ts +8 -8
- package/src/extension/team-manager-command.ts +96 -96
- package/src/extension/team-recommendation.ts +188 -188
- package/src/extension/team-tool/api.ts +5 -2
- package/src/extension/team-tool/cancel.ts +224 -209
- package/src/extension/team-tool/config-patch.ts +36 -36
- package/src/extension/team-tool/context.ts +60 -60
- package/src/extension/team-tool/doctor.ts +242 -242
- package/src/extension/team-tool/handle-settings.ts +421 -195
- package/src/extension/team-tool/inspect.ts +41 -41
- package/src/extension/team-tool/lifecycle-actions.ts +139 -139
- package/src/extension/team-tool/parallel-dispatch.ts +156 -156
- package/src/extension/team-tool/plan.ts +19 -19
- package/src/extension/team-tool/respond.ts +112 -111
- package/src/extension/team-tool/run.ts +246 -229
- package/src/extension/team-tool/status.ts +110 -110
- package/src/extension/team-tool-types.ts +13 -13
- package/src/extension/team-tool.ts +344 -344
- package/src/extension/tool-result.ts +16 -16
- package/src/extension/validate-resources.ts +77 -77
- package/src/hooks/registry.ts +61 -61
- package/src/hooks/types.ts +40 -40
- package/src/i18n.ts +184 -184
- package/src/observability/correlation.ts +35 -35
- package/src/observability/event-to-metric.ts +68 -68
- package/src/observability/exporters/adapter.ts +30 -30
- package/src/observability/exporters/otlp-exporter.ts +106 -92
- package/src/observability/exporters/prometheus-exporter.ts +54 -54
- package/src/observability/metric-registry.ts +87 -87
- package/src/observability/metric-retention.ts +54 -54
- package/src/observability/metric-sink.ts +81 -56
- package/src/observability/metrics-primitives.ts +167 -167
- package/src/prompt/prompt-runtime.ts +72 -72
- package/src/runtime/adaptive-plan.ts +338 -0
- package/src/runtime/agent-control.ts +169 -169
- package/src/runtime/agent-memory.ts +72 -72
- package/src/runtime/agent-observability.ts +114 -114
- package/src/runtime/async-marker.ts +26 -26
- package/src/runtime/async-runner.ts +153 -153
- package/src/runtime/attention-events.ts +28 -28
- package/src/runtime/auto-resume.ts +100 -100
- package/src/runtime/background-runner.ts +122 -89
- package/src/runtime/cancellation.ts +61 -61
- package/src/runtime/capability-inventory.ts +116 -116
- package/src/runtime/child-pi-pool.ts +68 -0
- package/src/runtime/child-pi.ts +541 -461
- package/src/runtime/code-summary.ts +247 -247
- package/src/runtime/compaction-summary.ts +271 -271
- package/src/runtime/concurrency.ts +58 -58
- package/src/runtime/crash-recovery.ts +317 -301
- package/src/runtime/crew-agent-records.ts +379 -281
- package/src/runtime/crew-agent-runtime.ts +60 -60
- package/src/runtime/cross-extension-rpc.ts +72 -0
- package/src/runtime/custom-tools/irc-tool.ts +201 -201
- package/src/runtime/custom-tools/submit-result-tool.ts +90 -90
- package/src/runtime/deadletter.ts +47 -47
- package/src/runtime/delivery-coordinator.ts +176 -176
- package/src/runtime/delta-conflict.ts +360 -360
- package/src/runtime/diagnostic-export.ts +102 -102
- package/src/runtime/direct-run.ts +35 -35
- package/src/runtime/effectiveness.ts +82 -81
- package/src/runtime/errors/crew-errors.ts +166 -0
- package/src/runtime/event-stream-bridge.ts +92 -92
- package/src/runtime/foreground-control.ts +82 -82
- package/src/runtime/green-contract.ts +46 -46
- package/src/runtime/group-join.ts +234 -106
- package/src/runtime/heartbeat-watcher.ts +145 -124
- package/src/runtime/iteration-hooks.ts +267 -267
- package/src/runtime/live-agent-control.ts +88 -88
- package/src/runtime/live-agent-manager.ts +377 -179
- package/src/runtime/live-control-realtime.ts +36 -36
- package/src/runtime/live-session-runtime.ts +676 -600
- package/src/runtime/loop-gates.ts +129 -129
- package/src/runtime/manifest-cache.ts +263 -263
- package/src/runtime/mcp-proxy.ts +113 -113
- package/src/runtime/metric-parser.ts +40 -40
- package/src/runtime/model-fallback.ts +282 -274
- package/src/runtime/model-resolver.ts +118 -0
- package/src/runtime/output-validator.ts +187 -187
- package/src/runtime/overflow-recovery.ts +175 -175
- package/src/runtime/parallel-research.ts +44 -44
- package/src/runtime/parallel-utils.ts +156 -156
- package/src/runtime/parent-guard.ts +80 -80
- package/src/runtime/phase-progress.ts +217 -217
- package/src/runtime/pi-args.ts +165 -165
- package/src/runtime/pi-json-output.ts +111 -111
- package/src/runtime/pi-spawn.ts +167 -167
- package/src/runtime/policy-engine.ts +79 -79
- package/src/runtime/post-checks.ts +125 -125
- package/src/runtime/post-exit-stdio-guard.ts +86 -86
- package/src/runtime/process-status.ts +97 -73
- package/src/runtime/progress-event-coalescer.ts +43 -43
- package/src/runtime/recovery-recipes.ts +74 -74
- package/src/runtime/retry-executor.ts +81 -81
- package/src/runtime/role-permission.ts +39 -39
- package/src/runtime/run-tracker.ts +99 -0
- package/src/runtime/runtime-policy.ts +21 -0
- package/src/runtime/runtime-resolver.ts +94 -91
- package/src/runtime/scheduler.ts +294 -0
- package/src/runtime/semaphore.ts +131 -131
- package/src/runtime/sensitive-paths.ts +92 -92
- package/src/runtime/session-usage.ts +79 -79
- package/src/runtime/settings-store.ts +103 -0
- package/src/runtime/sidechain-output.ts +29 -29
- package/src/runtime/skill-instructions.ts +222 -222
- package/src/runtime/stale-reconciler.ts +198 -189
- package/src/runtime/streaming-output.ts +47 -0
- package/src/runtime/subagent-manager.ts +404 -400
- package/src/runtime/subprocess-tool-registry.ts +67 -67
- package/src/runtime/task-display.ts +38 -38
- package/src/runtime/task-graph-scheduler.ts +122 -122
- package/src/runtime/task-graph.ts +207 -207
- package/src/runtime/task-output-context.ts +177 -177
- package/src/runtime/task-packet.ts +93 -93
- package/src/runtime/task-quality.ts +207 -207
- package/src/runtime/task-runner/capabilities.ts +78 -78
- package/src/runtime/task-runner/live-executor.ts +131 -113
- package/src/runtime/task-runner/progress.ts +119 -119
- package/src/runtime/task-runner/prompt-builder.ts +139 -139
- package/src/runtime/task-runner/prompt-pipeline.ts +64 -64
- package/src/runtime/task-runner/result-utils.ts +14 -14
- package/src/runtime/task-runner/run-projection.ts +103 -103
- package/src/runtime/task-runner/state-helpers.ts +22 -22
- package/src/runtime/task-runner.ts +469 -459
- package/src/runtime/team-runner.ts +693 -945
- package/src/runtime/usage-tracker.ts +71 -0
- package/src/runtime/worker-heartbeat.ts +21 -21
- package/src/runtime/worker-startup.ts +57 -57
- package/src/runtime/workflow-state.ts +187 -187
- package/src/runtime/yield-handler.ts +190 -190
- package/src/schema/config-schema.ts +172 -168
- package/src/schema/team-tool-schema.ts +126 -126
- package/src/schema/validation-types.ts +151 -148
- package/src/skills/discover-skills.ts +67 -67
- package/src/skills/skill-templates.ts +374 -374
- package/src/state/active-run-registry.ts +227 -191
- package/src/state/artifact-store.ts +130 -129
- package/src/state/atomic-write.ts +262 -195
- package/src/state/blob-store.ts +116 -116
- package/src/state/contracts.ts +111 -111
- package/src/state/event-log-rotation.ts +161 -158
- package/src/state/event-log.ts +383 -303
- package/src/state/event-reconstructor.ts +217 -217
- package/src/state/jsonl-writer.ts +82 -82
- package/src/state/locks.ts +146 -146
- package/src/state/mailbox.ts +446 -405
- package/src/state/state-store.ts +364 -351
- package/src/state/task-claims.ts +44 -44
- package/src/state/types.ts +285 -285
- package/src/state/usage.ts +29 -29
- package/src/subagents/async-entry.ts +1 -1
- package/src/subagents/index.ts +3 -3
- package/src/subagents/live/control.ts +1 -1
- package/src/subagents/live/manager.ts +1 -1
- package/src/subagents/live/realtime.ts +1 -1
- package/src/subagents/live/session-runtime.ts +1 -1
- package/src/subagents/manager.ts +1 -1
- package/src/subagents/spawn.ts +1 -1
- package/src/teams/discover-teams.ts +116 -116
- package/src/teams/team-config.ts +27 -27
- package/src/teams/team-serializer.ts +38 -38
- package/src/types/diff.d.ts +18 -18
- package/src/ui/agent-management-overlay.ts +144 -144
- package/src/ui/crew-widget.ts +487 -370
- package/src/ui/dashboard-panes/agents-pane.ts +109 -28
- package/src/ui/dashboard-panes/cancellation-pane.ts +42 -42
- package/src/ui/dashboard-panes/capability-pane.ts +59 -59
- package/src/ui/dashboard-panes/health-pane.ts +30 -30
- package/src/ui/dashboard-panes/mailbox-pane.ts +35 -35
- package/src/ui/dashboard-panes/progress-pane.ts +30 -30
- package/src/ui/dashboard-panes/transcript-pane.ts +10 -10
- package/src/ui/heartbeat-aggregator.ts +63 -63
- package/src/ui/keybinding-map.ts +97 -94
- package/src/ui/live-conversation-overlay.ts +152 -0
- package/src/ui/live-run-sidebar.ts +180 -180
- package/src/ui/mascot.ts +442 -442
- package/src/ui/overlays/agent-picker-overlay.ts +57 -57
- package/src/ui/overlays/confirm-overlay.ts +58 -58
- package/src/ui/overlays/mailbox-compose-overlay.ts +144 -144
- package/src/ui/overlays/mailbox-compose-preview.ts +63 -63
- package/src/ui/overlays/mailbox-detail-overlay.ts +122 -122
- package/src/ui/pi-ui-compat.ts +57 -57
- package/src/ui/powerbar-publisher.ts +221 -197
- package/src/ui/render-scheduler.ts +216 -143
- package/src/ui/run-action-dispatcher.ts +118 -118
- package/src/ui/run-dashboard.ts +526 -464
- package/src/ui/run-event-bus.ts +208 -208
- package/src/ui/run-snapshot-cache.ts +826 -777
- package/src/ui/settings-overlay.ts +721 -0
- package/src/ui/snapshot-types.ts +86 -70
- package/src/ui/theme-adapter.ts +190 -190
- package/src/ui/tool-progress-formatter.ts +89 -0
- package/src/ui/transcript-cache.ts +94 -94
- package/src/ui/transcript-viewer.ts +335 -335
- package/src/utils/conflict-detect.ts +662 -0
- package/src/utils/file-coalescer.ts +86 -86
- package/src/utils/frontmatter.ts +68 -68
- package/src/utils/fs-watch.ts +88 -31
- package/src/utils/gh-protocol.ts +479 -0
- package/src/utils/ids.ts +17 -17
- package/src/utils/incremental-reader.ts +104 -104
- package/src/utils/internal-error.ts +6 -6
- package/src/utils/names.ts +27 -27
- package/src/utils/paths.ts +102 -63
- package/src/utils/redaction.ts +44 -44
- package/src/utils/safe-paths.ts +47 -47
- package/src/utils/scan-cache.ts +136 -136
- package/src/utils/sse-parser.ts +134 -134
- package/src/utils/task-name-generator.ts +337 -337
- package/src/utils/timings.ts +33 -33
- package/src/utils/visual.ts +243 -198
- package/src/workflows/discover-workflows.ts +139 -139
- package/src/workflows/validate-workflow.ts +40 -40
- package/src/workflows/workflow-config.ts +26 -26
- package/src/workflows/workflow-serializer.ts +32 -32
- package/src/worktree/branch-freshness.ts +45 -45
- package/src/worktree/cleanup.ts +75 -75
- package/src/worktree/worktree-manager.ts +188 -188
- package/teams/default.team.md +12 -12
- package/teams/fast-fix.team.md +11 -11
- package/teams/implementation.team.md +18 -18
- package/teams/parallel-research.team.md +14 -14
- package/teams/research.team.md +11 -11
- package/teams/review.team.md +12 -12
- package/tsconfig.json +19 -19
- package/workflows/default.workflow.md +30 -30
- package/workflows/fast-fix.workflow.md +23 -23
- package/workflows/implementation.workflow.md +43 -43
- package/workflows/parallel-research.workflow.md +46 -46
- package/workflows/research.workflow.md +22 -22
- package/workflows/review.workflow.md +30 -30
- package/skills/task-packet/SKILL.md +0 -28
- package/skills/verify-evidence/SKILL.md +0 -27
package/src/state/event-log.ts
CHANGED
|
@@ -1,303 +1,383 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import * as fs from "node:fs";
|
|
3
|
-
import * as path from "node:path";
|
|
4
|
-
import { DEFAULT_EVENT_LOG } from "../config/defaults.ts";
|
|
5
|
-
import { atomicWriteFile } from "./atomic-write.ts";
|
|
6
|
-
import { emitFromTeamEvent } from "../ui/run-event-bus.ts";
|
|
7
|
-
import { logInternalError } from "../utils/internal-error.ts";
|
|
8
|
-
import { readJsonlSince, type IncrementalReadState } from "../utils/incremental-reader.ts";
|
|
9
|
-
import { redactSecrets } from "../utils/redaction.ts";
|
|
10
|
-
import { sleepSync } from "../utils/sleep.ts";
|
|
11
|
-
import { needsRotation, compactEventLog } from "./event-log-rotation.ts";
|
|
12
|
-
|
|
13
|
-
export type TeamEventProvenance = "live_worker" | "test" | "healthcheck" | "replay" | "api" | "background" | "team_runner";
|
|
14
|
-
export type TeamWatcherAction = "act" | "observe" | "ignore";
|
|
15
|
-
|
|
16
|
-
export interface TeamEventSessionIdentity {
|
|
17
|
-
title: string;
|
|
18
|
-
workspace: string;
|
|
19
|
-
purpose: string;
|
|
20
|
-
placeholderReason?: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface TeamEventOwnership {
|
|
24
|
-
owner: string;
|
|
25
|
-
workflowScope: string;
|
|
26
|
-
watcherAction: TeamWatcherAction;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface TeamEventMetadata {
|
|
30
|
-
seq: number;
|
|
31
|
-
provenance: TeamEventProvenance;
|
|
32
|
-
parentEventId?: string;
|
|
33
|
-
attemptId?: string;
|
|
34
|
-
branchId?: string;
|
|
35
|
-
causationId?: string;
|
|
36
|
-
correlationId?: string;
|
|
37
|
-
sessionIdentity?: TeamEventSessionIdentity;
|
|
38
|
-
ownership?: TeamEventOwnership;
|
|
39
|
-
nudgeId?: string;
|
|
40
|
-
appended?: boolean;
|
|
41
|
-
fingerprint?: string;
|
|
42
|
-
confidence?: "low" | "medium" | "high";
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface TeamEvent {
|
|
46
|
-
time: string;
|
|
47
|
-
type: string;
|
|
48
|
-
runId: string;
|
|
49
|
-
taskId?: string;
|
|
50
|
-
message?: string;
|
|
51
|
-
data?: Record<string, unknown>;
|
|
52
|
-
metadata?: TeamEventMetadata;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export type AppendTeamEvent = Omit<TeamEvent, "time" | "metadata"> & { metadata?: Partial<TeamEventMetadata> };
|
|
56
|
-
|
|
57
|
-
const TERMINAL_EVENT_TYPES = new Set<string>(DEFAULT_EVENT_LOG.terminalEventTypes);
|
|
58
|
-
const MAX_EVENTS_BYTES = 50 * 1024 * 1024;
|
|
59
|
-
|
|
60
|
-
const sequenceCache = new Map<string, { size: number; mtimeMs: number; seq: number }>();
|
|
61
|
-
const MAX_SEQUENCE_CACHE_ENTRIES = 256;
|
|
62
|
-
let appendCounter = 0;
|
|
63
|
-
|
|
64
|
-
/** Simple cross-process lock for an eventsPath to prevent JSONL interleave on concurrent append.
|
|
65
|
-
* Detects stale locks by checking the owner PID written inside the lock directory.
|
|
66
|
-
*/
|
|
67
|
-
function withEventLogLockSync<T>(eventsPath: string, fn: () => T): T {
|
|
68
|
-
const lockDir = `${eventsPath}.lock`;
|
|
69
|
-
const pidFile = path.join(lockDir, "pid");
|
|
70
|
-
const start = Date.now();
|
|
71
|
-
const timeout = 5000;
|
|
72
|
-
const staleMs = 10000;
|
|
73
|
-
let acquired = false;
|
|
74
|
-
while (true) {
|
|
75
|
-
try {
|
|
76
|
-
fs.mkdirSync(lockDir);
|
|
77
|
-
try { fs.writeFileSync(pidFile, String(process.pid), "utf-8"); } catch { /* best-effort */ }
|
|
78
|
-
acquired = true;
|
|
79
|
-
break;
|
|
80
|
-
} catch {
|
|
81
|
-
if (Date.now() - start > timeout) {
|
|
82
|
-
logInternalError("event-log.lock-timeout", new Error(`Event log lock timeout for ${eventsPath}`), `lockDir=${lockDir}`);
|
|
83
|
-
break;
|
|
84
|
-
}
|
|
85
|
-
// Stale detection: if the owning process is dead, remove the stale lock.
|
|
86
|
-
try {
|
|
87
|
-
const raw = fs.readFileSync(pidFile, "utf-8").trim();
|
|
88
|
-
const ownerPid = Number.parseInt(raw, 10);
|
|
89
|
-
if (!Number.isNaN(ownerPid) && ownerPid !== process.pid) {
|
|
90
|
-
let alive = false;
|
|
91
|
-
try { process.kill(ownerPid, 0); alive = true; } catch { /* dead */ }
|
|
92
|
-
if (!alive) {
|
|
93
|
-
try {
|
|
94
|
-
const stat = fs.statSync(lockDir);
|
|
95
|
-
if (Date.now() - stat.mtimeMs > staleMs) {
|
|
96
|
-
fs.rmSync(lockDir, { recursive: true, force: true });
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
} catch { /* race — let loop sleep */ }
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
} catch { /* no pid file — fall through to sleep */ }
|
|
103
|
-
sleepSync(10);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
try {
|
|
107
|
-
return fn();
|
|
108
|
-
} finally {
|
|
109
|
-
if (acquired) {
|
|
110
|
-
try { fs.rmSync(lockDir, { recursive: true, force: true }); } catch { /* best-effort */ }
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function evictOldestSequenceCacheEntry(): void {
|
|
116
|
-
const first = sequenceCache.keys().next().value;
|
|
117
|
-
if (first !== undefined) sequenceCache.delete(first);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export function sequencePath(eventsPath: string): string {
|
|
121
|
-
return `${eventsPath}.seq`;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function parseSequence(raw: string): number | undefined {
|
|
125
|
-
const value = Number.parseInt(raw.trim(), 10);
|
|
126
|
-
return Number.isInteger(value) && value >= 0 ? value : undefined;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export function scanSequence(eventsPath: string): number {
|
|
130
|
-
if (!fs.existsSync(eventsPath)) return 0;
|
|
131
|
-
let max = 0;
|
|
132
|
-
for (const line of fs.readFileSync(eventsPath, "utf-8").split("\n")) {
|
|
133
|
-
if (!line.trim()) continue;
|
|
134
|
-
try {
|
|
135
|
-
const event = JSON.parse(line) as TeamEvent;
|
|
136
|
-
max = Math.max(max, event.metadata?.seq ?? 0);
|
|
137
|
-
} catch { /* skip corrupt lines without incrementing sequence */ }
|
|
138
|
-
}
|
|
139
|
-
return max;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function readStoredSequence(eventsPath: string): number | undefined {
|
|
143
|
-
try {
|
|
144
|
-
return parseSequence(fs.readFileSync(sequencePath(eventsPath), "utf-8"));
|
|
145
|
-
} catch {
|
|
146
|
-
return undefined;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function nextSequence(eventsPath: string): number {
|
|
151
|
-
if (!fs.existsSync(eventsPath)) return 1;
|
|
152
|
-
const stat = fs.statSync(eventsPath);
|
|
153
|
-
const cached = sequenceCache.get(eventsPath);
|
|
154
|
-
if (cached && cached.size === stat.size && cached.mtimeMs === stat.mtimeMs) {
|
|
155
|
-
return cached.seq + 1;
|
|
156
|
-
}
|
|
157
|
-
let current = readStoredSequence(eventsPath);
|
|
158
|
-
if (current === undefined || (cached && stat.size < cached.size)) {
|
|
159
|
-
current = scanSequence(eventsPath);
|
|
160
|
-
}
|
|
161
|
-
sequenceCache.set(eventsPath, { size: stat.size, mtimeMs: stat.mtimeMs, seq: current });
|
|
162
|
-
return current + 1;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function persistSequence(eventsPath: string, seq: number): void {
|
|
166
|
-
try {
|
|
167
|
-
atomicWriteFile(sequencePath(eventsPath), String(seq));
|
|
168
|
-
} catch (error) {
|
|
169
|
-
logInternalError("event-log.persist-sequence-file", error, `eventsPath=${eventsPath}`);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export function computeEventFingerprint(event: Pick<TeamEvent, "type" | "runId" | "taskId" | "data">): string {
|
|
174
|
-
return createHash("sha256").update(JSON.stringify({ type: event.type, runId: event.runId, taskId: event.taskId, data: event.data ?? null })).digest("hex").slice(0, 16);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
export function appendEvent(eventsPath: string, event: AppendTeamEvent): TeamEvent {
|
|
178
|
-
return withEventLogLockSync(eventsPath, () =>
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { DEFAULT_EVENT_LOG } from "../config/defaults.ts";
|
|
5
|
+
import { atomicWriteFile } from "./atomic-write.ts";
|
|
6
|
+
import { emitFromTeamEvent } from "../ui/run-event-bus.ts";
|
|
7
|
+
import { logInternalError } from "../utils/internal-error.ts";
|
|
8
|
+
import { readJsonlSince, type IncrementalReadState } from "../utils/incremental-reader.ts";
|
|
9
|
+
import { redactSecrets } from "../utils/redaction.ts";
|
|
10
|
+
import { sleepSync } from "../utils/sleep.ts";
|
|
11
|
+
import { needsRotation, compactEventLog } from "./event-log-rotation.ts";
|
|
12
|
+
|
|
13
|
+
export type TeamEventProvenance = "live_worker" | "test" | "healthcheck" | "replay" | "api" | "background" | "team_runner";
|
|
14
|
+
export type TeamWatcherAction = "act" | "observe" | "ignore";
|
|
15
|
+
|
|
16
|
+
export interface TeamEventSessionIdentity {
|
|
17
|
+
title: string;
|
|
18
|
+
workspace: string;
|
|
19
|
+
purpose: string;
|
|
20
|
+
placeholderReason?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface TeamEventOwnership {
|
|
24
|
+
owner: string;
|
|
25
|
+
workflowScope: string;
|
|
26
|
+
watcherAction: TeamWatcherAction;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface TeamEventMetadata {
|
|
30
|
+
seq: number;
|
|
31
|
+
provenance: TeamEventProvenance;
|
|
32
|
+
parentEventId?: string;
|
|
33
|
+
attemptId?: string;
|
|
34
|
+
branchId?: string;
|
|
35
|
+
causationId?: string;
|
|
36
|
+
correlationId?: string;
|
|
37
|
+
sessionIdentity?: TeamEventSessionIdentity;
|
|
38
|
+
ownership?: TeamEventOwnership;
|
|
39
|
+
nudgeId?: string;
|
|
40
|
+
appended?: boolean;
|
|
41
|
+
fingerprint?: string;
|
|
42
|
+
confidence?: "low" | "medium" | "high";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface TeamEvent {
|
|
46
|
+
time: string;
|
|
47
|
+
type: string;
|
|
48
|
+
runId: string;
|
|
49
|
+
taskId?: string;
|
|
50
|
+
message?: string;
|
|
51
|
+
data?: Record<string, unknown>;
|
|
52
|
+
metadata?: TeamEventMetadata;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type AppendTeamEvent = Omit<TeamEvent, "time" | "metadata"> & { metadata?: Partial<TeamEventMetadata> };
|
|
56
|
+
|
|
57
|
+
const TERMINAL_EVENT_TYPES = new Set<string>(DEFAULT_EVENT_LOG.terminalEventTypes);
|
|
58
|
+
const MAX_EVENTS_BYTES = 50 * 1024 * 1024;
|
|
59
|
+
|
|
60
|
+
const sequenceCache = new Map<string, { size: number; mtimeMs: number; seq: number }>();
|
|
61
|
+
const MAX_SEQUENCE_CACHE_ENTRIES = 256;
|
|
62
|
+
let appendCounter = 0;
|
|
63
|
+
|
|
64
|
+
/** Simple cross-process lock for an eventsPath to prevent JSONL interleave on concurrent append.
|
|
65
|
+
* Detects stale locks by checking the owner PID written inside the lock directory.
|
|
66
|
+
*/
|
|
67
|
+
function withEventLogLockSync<T>(eventsPath: string, fn: () => T): T {
|
|
68
|
+
const lockDir = `${eventsPath}.lock`;
|
|
69
|
+
const pidFile = path.join(lockDir, "pid");
|
|
70
|
+
const start = Date.now();
|
|
71
|
+
const timeout = 5000;
|
|
72
|
+
const staleMs = 10000;
|
|
73
|
+
let acquired = false;
|
|
74
|
+
while (true) {
|
|
75
|
+
try {
|
|
76
|
+
fs.mkdirSync(lockDir);
|
|
77
|
+
try { fs.writeFileSync(pidFile, String(process.pid), "utf-8"); } catch { /* best-effort */ }
|
|
78
|
+
acquired = true;
|
|
79
|
+
break;
|
|
80
|
+
} catch {
|
|
81
|
+
if (Date.now() - start > timeout) {
|
|
82
|
+
logInternalError("event-log.lock-timeout", new Error(`Event log lock timeout for ${eventsPath}`), `lockDir=${lockDir}`);
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
// Stale detection: if the owning process is dead, remove the stale lock.
|
|
86
|
+
try {
|
|
87
|
+
const raw = fs.readFileSync(pidFile, "utf-8").trim();
|
|
88
|
+
const ownerPid = Number.parseInt(raw, 10);
|
|
89
|
+
if (!Number.isNaN(ownerPid) && ownerPid !== process.pid) {
|
|
90
|
+
let alive = false;
|
|
91
|
+
try { process.kill(ownerPid, 0); alive = true; } catch { /* dead */ }
|
|
92
|
+
if (!alive) {
|
|
93
|
+
try {
|
|
94
|
+
const stat = fs.statSync(lockDir);
|
|
95
|
+
if (Date.now() - stat.mtimeMs > staleMs) {
|
|
96
|
+
fs.rmSync(lockDir, { recursive: true, force: true });
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
} catch { /* race — let loop sleep */ }
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} catch { /* no pid file — fall through to sleep */ }
|
|
103
|
+
sleepSync(10);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
return fn();
|
|
108
|
+
} finally {
|
|
109
|
+
if (acquired) {
|
|
110
|
+
try { fs.rmSync(lockDir, { recursive: true, force: true }); } catch { /* best-effort */ }
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function evictOldestSequenceCacheEntry(): void {
|
|
116
|
+
const first = sequenceCache.keys().next().value;
|
|
117
|
+
if (first !== undefined) sequenceCache.delete(first);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function sequencePath(eventsPath: string): string {
|
|
121
|
+
return `${eventsPath}.seq`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function parseSequence(raw: string): number | undefined {
|
|
125
|
+
const value = Number.parseInt(raw.trim(), 10);
|
|
126
|
+
return Number.isInteger(value) && value >= 0 ? value : undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function scanSequence(eventsPath: string): number {
|
|
130
|
+
if (!fs.existsSync(eventsPath)) return 0;
|
|
131
|
+
let max = 0;
|
|
132
|
+
for (const line of fs.readFileSync(eventsPath, "utf-8").split("\n")) {
|
|
133
|
+
if (!line.trim()) continue;
|
|
134
|
+
try {
|
|
135
|
+
const event = JSON.parse(line) as TeamEvent;
|
|
136
|
+
max = Math.max(max, event.metadata?.seq ?? 0);
|
|
137
|
+
} catch { /* skip corrupt lines without incrementing sequence */ }
|
|
138
|
+
}
|
|
139
|
+
return max;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function readStoredSequence(eventsPath: string): number | undefined {
|
|
143
|
+
try {
|
|
144
|
+
return parseSequence(fs.readFileSync(sequencePath(eventsPath), "utf-8"));
|
|
145
|
+
} catch {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function nextSequence(eventsPath: string): number {
|
|
151
|
+
if (!fs.existsSync(eventsPath)) return 1;
|
|
152
|
+
const stat = fs.statSync(eventsPath);
|
|
153
|
+
const cached = sequenceCache.get(eventsPath);
|
|
154
|
+
if (cached && cached.size === stat.size && cached.mtimeMs === stat.mtimeMs) {
|
|
155
|
+
return cached.seq + 1;
|
|
156
|
+
}
|
|
157
|
+
let current = readStoredSequence(eventsPath);
|
|
158
|
+
if (current === undefined || (cached && stat.size < cached.size)) {
|
|
159
|
+
current = scanSequence(eventsPath);
|
|
160
|
+
}
|
|
161
|
+
sequenceCache.set(eventsPath, { size: stat.size, mtimeMs: stat.mtimeMs, seq: current });
|
|
162
|
+
return current + 1;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function persistSequence(eventsPath: string, seq: number): void {
|
|
166
|
+
try {
|
|
167
|
+
atomicWriteFile(sequencePath(eventsPath), String(seq));
|
|
168
|
+
} catch (error) {
|
|
169
|
+
logInternalError("event-log.persist-sequence-file", error, `eventsPath=${eventsPath}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function computeEventFingerprint(event: Pick<TeamEvent, "type" | "runId" | "taskId" | "data">): string {
|
|
174
|
+
return createHash("sha256").update(JSON.stringify({ type: event.type, runId: event.runId, taskId: event.taskId, data: event.data ?? null })).digest("hex").slice(0, 16);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function appendEvent(eventsPath: string, event: AppendTeamEvent): TeamEvent {
|
|
178
|
+
return withEventLogLockSync(eventsPath, () => appendEventInsideLock(eventsPath, event));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Body of `appendEvent` assuming the caller already holds
|
|
183
|
+
* `withEventLogLockSync` for `eventsPath`. Used by `appendEventBuffered` to
|
|
184
|
+
* write a whole batch of pending events under a single lock acquire.
|
|
185
|
+
*/
|
|
186
|
+
function appendEventInsideLock(eventsPath: string, event: AppendTeamEvent): TeamEvent {
|
|
187
|
+
fs.mkdirSync(path.dirname(eventsPath), { recursive: true });
|
|
188
|
+
const baseMetadata = event.metadata;
|
|
189
|
+
let metadata: TeamEventMetadata = {
|
|
190
|
+
seq: baseMetadata?.seq ?? nextSequence(eventsPath),
|
|
191
|
+
provenance: baseMetadata?.provenance ?? "team_runner",
|
|
192
|
+
...(baseMetadata?.parentEventId ? { parentEventId: baseMetadata.parentEventId } : {}),
|
|
193
|
+
...(baseMetadata?.attemptId ? { attemptId: baseMetadata.attemptId } : {}),
|
|
194
|
+
...(baseMetadata?.branchId ? { branchId: baseMetadata.branchId } : {}),
|
|
195
|
+
...(baseMetadata?.causationId ? { causationId: baseMetadata.causationId } : {}),
|
|
196
|
+
...(baseMetadata?.correlationId ? { correlationId: baseMetadata.correlationId } : {}),
|
|
197
|
+
...(baseMetadata?.sessionIdentity ? { sessionIdentity: baseMetadata.sessionIdentity } : {}),
|
|
198
|
+
...(baseMetadata?.ownership ? { ownership: baseMetadata.ownership } : {}),
|
|
199
|
+
...(baseMetadata?.nudgeId ? { nudgeId: baseMetadata.nudgeId } : {}),
|
|
200
|
+
...(baseMetadata?.confidence ? { confidence: baseMetadata.confidence } : {}),
|
|
201
|
+
};
|
|
202
|
+
const fullEvent: TeamEvent = {
|
|
203
|
+
time: new Date().toISOString(),
|
|
204
|
+
...event,
|
|
205
|
+
metadata,
|
|
206
|
+
};
|
|
207
|
+
if (baseMetadata?.fingerprint || TERMINAL_EVENT_TYPES.has(fullEvent.type)) {
|
|
208
|
+
metadata = { ...metadata, fingerprint: baseMetadata?.fingerprint ?? computeEventFingerprint(fullEvent) };
|
|
209
|
+
fullEvent.metadata = metadata;
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
if (fs.existsSync(eventsPath) && fs.statSync(eventsPath).size > MAX_EVENTS_BYTES) {
|
|
213
|
+
logInternalError("event-log.size-limit", new Error(`events file ${eventsPath} exceeds ${MAX_EVENTS_BYTES} bytes`), `eventsPath=${eventsPath}`);
|
|
214
|
+
return { ...fullEvent, metadata: { ...(fullEvent.metadata ?? { seq: 0, provenance: "team_runner" }), appended: false } };
|
|
215
|
+
}
|
|
216
|
+
} catch (error) {
|
|
217
|
+
logInternalError("event-log.size-check", error, `eventsPath=${eventsPath}`);
|
|
218
|
+
}
|
|
219
|
+
fs.appendFileSync(eventsPath, `${JSON.stringify(redactSecrets(fullEvent))}\n`, "utf-8");
|
|
220
|
+
appendCounter++;
|
|
221
|
+
if (appendCounter % 100 === 0 && needsRotation(eventsPath)) {
|
|
222
|
+
try { compactEventLog(eventsPath); } catch (error) { logInternalError("event-log.rotation", error, `eventsPath=${eventsPath}`); }
|
|
223
|
+
}
|
|
224
|
+
try { emitFromTeamEvent(fullEvent); } catch (error) { logInternalError("event-log.emit", error); }
|
|
225
|
+
const seq = fullEvent.metadata?.seq ?? 0;
|
|
226
|
+
try {
|
|
227
|
+
const stat = fs.statSync(eventsPath);
|
|
228
|
+
if (sequenceCache.size >= MAX_SEQUENCE_CACHE_ENTRIES) {
|
|
229
|
+
evictOldestSequenceCacheEntry();
|
|
230
|
+
}
|
|
231
|
+
sequenceCache.set(eventsPath, { size: stat.size, mtimeMs: stat.mtimeMs, seq });
|
|
232
|
+
persistSequence(eventsPath, seq);
|
|
233
|
+
} catch (error) {
|
|
234
|
+
logInternalError("event-log.persist-sequence", error, `eventsPath=${eventsPath}`);
|
|
235
|
+
}
|
|
236
|
+
return fullEvent;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// 2.2 — Buffered append API. Caller queues events and they are flushed under
|
|
240
|
+
// a single `withEventLogLockSync` acquire after `bufferingMs` ms. The seq
|
|
241
|
+
// invariant is preserved because the flush still goes through
|
|
242
|
+
// appendEventInsideLock sequentially.
|
|
243
|
+
//
|
|
244
|
+
// Caveat: events still in the buffer at process kill -9 are lost. Callers
|
|
245
|
+
// for whom durability is critical (lifecycle terminal events) should keep
|
|
246
|
+
// using `appendEvent`. Used opportunistically for high-frequency events
|
|
247
|
+
// like `task.progress` once integration tests cover crash semantics.
|
|
248
|
+
interface BufferedAppend {
|
|
249
|
+
event: AppendTeamEvent;
|
|
250
|
+
resolve: (event: TeamEvent) => void;
|
|
251
|
+
reject: (error: unknown) => void;
|
|
252
|
+
}
|
|
253
|
+
const bufferedQueues = new Map<string, BufferedAppend[]>();
|
|
254
|
+
const bufferedTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
255
|
+
const DEFAULT_BUFFER_MS = 20;
|
|
256
|
+
|
|
257
|
+
export function appendEventBuffered(eventsPath: string, event: AppendTeamEvent, bufferMs = DEFAULT_BUFFER_MS): Promise<TeamEvent> {
|
|
258
|
+
return new Promise<TeamEvent>((resolve, reject) => {
|
|
259
|
+
const queue = bufferedQueues.get(eventsPath) ?? [];
|
|
260
|
+
queue.push({ event, resolve, reject });
|
|
261
|
+
bufferedQueues.set(eventsPath, queue);
|
|
262
|
+
if (!bufferedTimers.has(eventsPath)) {
|
|
263
|
+
const timer = setTimeout(() => flushOneEventLogBuffer(eventsPath), bufferMs);
|
|
264
|
+
timer.unref();
|
|
265
|
+
bufferedTimers.set(eventsPath, timer);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function flushOneEventLogBuffer(eventsPath: string): void {
|
|
271
|
+
const queue = bufferedQueues.get(eventsPath);
|
|
272
|
+
bufferedQueues.delete(eventsPath);
|
|
273
|
+
const timer = bufferedTimers.get(eventsPath);
|
|
274
|
+
if (timer) clearTimeout(timer);
|
|
275
|
+
bufferedTimers.delete(eventsPath);
|
|
276
|
+
if (!queue || queue.length === 0) return;
|
|
277
|
+
try {
|
|
278
|
+
withEventLogLockSync(eventsPath, () => {
|
|
279
|
+
for (const item of queue) {
|
|
280
|
+
try {
|
|
281
|
+
const ev = appendEventInsideLock(eventsPath, item.event);
|
|
282
|
+
item.resolve(ev);
|
|
283
|
+
} catch (error) {
|
|
284
|
+
item.reject(error);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
} catch (error) {
|
|
289
|
+
// Lock acquire failed — fail every queued item so callers can fall back.
|
|
290
|
+
for (const item of queue) item.reject(error);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Synchronously flush every queued buffered event across all paths. */
|
|
295
|
+
export function flushEventLogBuffer(): void {
|
|
296
|
+
for (const eventsPath of [...bufferedQueues.keys()]) flushOneEventLogBuffer(eventsPath);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* 2.2 caller-migration helper — schedule a buffered append but do not return
|
|
301
|
+
* the resulting Promise. Use only for events whose return value is ignored
|
|
302
|
+
* (high-frequency `task.progress`). Errors are logged via logInternalError.
|
|
303
|
+
*/
|
|
304
|
+
export function appendEventFireAndForget(eventsPath: string, event: AppendTeamEvent, bufferMs = DEFAULT_BUFFER_MS): void {
|
|
305
|
+
appendEventBuffered(eventsPath, event, bufferMs).catch((error) => logInternalError("event-log.fire-and-forget", error, eventsPath));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Auto-flush on process exit so buffered events do not silently leak.
|
|
309
|
+
process.on("exit", () => flushEventLogBuffer());
|
|
310
|
+
process.on("SIGTERM", () => flushEventLogBuffer());
|
|
311
|
+
process.on("SIGINT", () => flushEventLogBuffer());
|
|
312
|
+
|
|
313
|
+
export function readEvents(eventsPath: string): TeamEvent[] {
|
|
314
|
+
if (!fs.existsSync(eventsPath)) return [];
|
|
315
|
+
return fs.readFileSync(eventsPath, "utf-8")
|
|
316
|
+
.split("\n")
|
|
317
|
+
.map((line) => line.trim())
|
|
318
|
+
.filter(Boolean)
|
|
319
|
+
.flatMap((line) => {
|
|
320
|
+
try { return [JSON.parse(line) as TeamEvent]; }
|
|
321
|
+
catch { return []; }
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export interface EventCursorOptions {
|
|
326
|
+
sinceSeq?: number;
|
|
327
|
+
limit?: number;
|
|
328
|
+
fromByteOffset?: number;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export interface EventCursorResult {
|
|
332
|
+
events: TeamEvent[];
|
|
333
|
+
nextSeq: number;
|
|
334
|
+
total: number;
|
|
335
|
+
nextByteOffset?: number;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function positiveInteger(value: number | undefined): number | undefined {
|
|
339
|
+
return value !== undefined && Number.isInteger(value) && value >= 0 ? value : undefined;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function readEventsCursor(eventsPath: string, options: EventCursorOptions = {}): EventCursorResult {
|
|
343
|
+
// Incremental byte-offset path: read only new bytes since last known offset
|
|
344
|
+
if (options.fromByteOffset !== undefined) {
|
|
345
|
+
const byteOffset = positiveInteger(options.fromByteOffset) ?? 0;
|
|
346
|
+
const initialState: IncrementalReadState = { byteOffset, lineCount: 0 };
|
|
347
|
+
const { items, state: newState, eof } = readJsonlSince<TeamEvent>(eventsPath, initialState);
|
|
348
|
+
const sinceSeq = positiveInteger(options.sinceSeq) ?? 0;
|
|
349
|
+
const filtered = items.filter((event) => (event.metadata?.seq ?? 0) > sinceSeq);
|
|
350
|
+
const limit = positiveInteger(options.limit);
|
|
351
|
+
const events = limit !== undefined ? filtered.slice(0, limit) : filtered;
|
|
352
|
+
const returnedMaxSeq = events.reduce((max, event) => Math.max(max, event.metadata?.seq ?? 0), sinceSeq);
|
|
353
|
+
return {
|
|
354
|
+
events,
|
|
355
|
+
nextSeq: returnedMaxSeq,
|
|
356
|
+
total: filtered.length,
|
|
357
|
+
nextByteOffset: newState.byteOffset,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Original behavior: read entire file
|
|
362
|
+
const sinceSeq = positiveInteger(options.sinceSeq) ?? 0;
|
|
363
|
+
const limit = positiveInteger(options.limit);
|
|
364
|
+
const all = readEvents(eventsPath);
|
|
365
|
+
const filtered = all.filter((event) => (event.metadata?.seq ?? 0) > sinceSeq);
|
|
366
|
+
const events = limit !== undefined ? filtered.slice(0, limit) : filtered;
|
|
367
|
+
const returnedMaxSeq = events.reduce((max, event) => Math.max(max, event.metadata?.seq ?? 0), sinceSeq);
|
|
368
|
+
return { events, nextSeq: returnedMaxSeq, total: filtered.length };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export function dedupeTerminalEvents(events: TeamEvent[]): TeamEvent[] {
|
|
372
|
+
const seen = new Set<string>();
|
|
373
|
+
const output: TeamEvent[] = [];
|
|
374
|
+
for (const event of events) {
|
|
375
|
+
const fingerprint = event.metadata?.fingerprint;
|
|
376
|
+
if (fingerprint && TERMINAL_EVENT_TYPES.has(event.type)) {
|
|
377
|
+
if (seen.has(fingerprint)) continue;
|
|
378
|
+
seen.add(fingerprint);
|
|
379
|
+
}
|
|
380
|
+
output.push(event);
|
|
381
|
+
}
|
|
382
|
+
return output;
|
|
383
|
+
}
|