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
|
@@ -1,217 +1,217 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Event reconstructor — rebuilds task state from the append-only event log.
|
|
3
|
-
*
|
|
4
|
-
* Primary use-case: crash recovery when tasks.json is corrupted or missing.
|
|
5
|
-
* The materialized tasks.json view is the primary source of truth; this
|
|
6
|
-
* module provides a fallback reconstruction path from events.jsonl.
|
|
7
|
-
*
|
|
8
|
-
* Distilled from pi-autoresearch's append-only event log pattern.
|
|
9
|
-
*/
|
|
10
|
-
import type { TeamEvent } from "./event-log.ts";
|
|
11
|
-
import { readEvents } from "./event-log.ts";
|
|
12
|
-
|
|
13
|
-
/** Task status values that can be reconstructed from lifecycle events. */
|
|
14
|
-
const RECONSTRUCTABLE_STATUSES = new Set(["created", "queued", "running", "completed", "failed", "cancelled", "skipped", "waiting"]);
|
|
15
|
-
|
|
16
|
-
/** Event types that carry task lifecycle state transitions. */
|
|
17
|
-
const TASK_LIFECYCLE_EVENT_TYPES = new Set([
|
|
18
|
-
"task.created",
|
|
19
|
-
"task.started",
|
|
20
|
-
"task.completed",
|
|
21
|
-
"task.failed",
|
|
22
|
-
"task.skipped",
|
|
23
|
-
"task.cancelled",
|
|
24
|
-
"task.waiting",
|
|
25
|
-
"task.resumed",
|
|
26
|
-
"task.retried",
|
|
27
|
-
"task.blocked",
|
|
28
|
-
"task.progress",
|
|
29
|
-
"task.green",
|
|
30
|
-
"task.red",
|
|
31
|
-
]);
|
|
32
|
-
|
|
33
|
-
/** Terminal events that set finishedAt. */
|
|
34
|
-
const TERMINAL_EVENTS = new Set(["task.completed", "task.failed", "task.cancelled", "task.skipped"]);
|
|
35
|
-
|
|
36
|
-
/** Mapping from event type to the reconstructed task status. */
|
|
37
|
-
const EVENT_STATUS_MAP: Readonly<Record<string, string>> = {
|
|
38
|
-
"task.created": "created",
|
|
39
|
-
"task.started": "running",
|
|
40
|
-
"task.completed": "completed",
|
|
41
|
-
"task.failed": "failed",
|
|
42
|
-
"task.skipped": "skipped",
|
|
43
|
-
"task.cancelled": "cancelled",
|
|
44
|
-
"task.waiting": "waiting",
|
|
45
|
-
"task.resumed": "running",
|
|
46
|
-
"task.retried": "queued",
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
/** Task state reconstructed purely from event log entries. */
|
|
50
|
-
export interface ReconstructedTaskState {
|
|
51
|
-
/** Task identifier */
|
|
52
|
-
id: string;
|
|
53
|
-
/** Reconstructed status derived from the last lifecycle event */
|
|
54
|
-
status: string;
|
|
55
|
-
/** Timestamp of the task.started event, if observed */
|
|
56
|
-
startedAt?: string;
|
|
57
|
-
/** Timestamp of the terminal event (completed/failed/cancelled/skipped), if observed */
|
|
58
|
-
finishedAt?: string;
|
|
59
|
-
/** Error message from task.failed events */
|
|
60
|
-
error?: string;
|
|
61
|
-
/** Segment number from event data (for retry isolation) */
|
|
62
|
-
segment?: number;
|
|
63
|
-
/** Structured diagnostics from event data */
|
|
64
|
-
diagnostics?: Record<string, unknown>;
|
|
65
|
-
/** Numeric metrics from event data */
|
|
66
|
-
metrics?: Record<string, number>;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Result of reconstructing task state from events. */
|
|
70
|
-
export interface ReconstructionResult {
|
|
71
|
-
/** Map of taskId → reconstructed task state */
|
|
72
|
-
tasks: Map<string, ReconstructedTaskState>;
|
|
73
|
-
/** Total number of events processed */
|
|
74
|
-
eventCount: number;
|
|
75
|
-
/** Number of malformed/unparseable events skipped */
|
|
76
|
-
corruptedCount: number;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Input: either a file path to read events from, or an in-memory array. */
|
|
80
|
-
export type EventSource = string | TeamEvent[];
|
|
81
|
-
|
|
82
|
-
function isTaskLifecycleEvent(event: TeamEvent): boolean {
|
|
83
|
-
return TASK_LIFECYCLE_EVENT_TYPES.has(event.type);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function statusFromEventType(eventType: string): string | undefined {
|
|
87
|
-
return EVENT_STATUS_MAP[eventType];
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function safeNumber(value: unknown): number | undefined {
|
|
91
|
-
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function safeRecord(value: unknown): Record<string, unknown> | undefined {
|
|
95
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
96
|
-
return undefined;
|
|
97
|
-
}
|
|
98
|
-
return value as Record<string, unknown>;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function safeNumericRecord(value: unknown): Record<string, number> | undefined {
|
|
102
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
103
|
-
return undefined;
|
|
104
|
-
}
|
|
105
|
-
const record: Record<string, number> = {};
|
|
106
|
-
for (const [key, val] of Object.entries(value as Record<string, unknown>)) {
|
|
107
|
-
if (typeof val === "number" && Number.isFinite(val)) {
|
|
108
|
-
record[key] = val;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
if (Object.keys(record).length === 0) {
|
|
112
|
-
return undefined;
|
|
113
|
-
}
|
|
114
|
-
return record;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function parseEventLine(line: string): TeamEvent | undefined {
|
|
118
|
-
const trimmed = line.trim();
|
|
119
|
-
if (trimmed.length === 0) return undefined;
|
|
120
|
-
try {
|
|
121
|
-
const parsed = JSON.parse(trimmed);
|
|
122
|
-
if (typeof parsed !== "object" || parsed === null) return undefined;
|
|
123
|
-
if (typeof parsed.type !== "string" || typeof parsed.runId !== "string") return undefined;
|
|
124
|
-
return parsed as TeamEvent;
|
|
125
|
-
} catch {
|
|
126
|
-
return undefined;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Process a stream of validated TeamEvents into reconstructed task states.
|
|
132
|
-
* Shared logic for both file-based and line-based reconstruction.
|
|
133
|
-
*/
|
|
134
|
-
function processEvents(events: Iterable<TeamEvent>, eventCount: number, corruptedCount: number): ReconstructionResult {
|
|
135
|
-
const tasks = new Map<string, ReconstructedTaskState>();
|
|
136
|
-
|
|
137
|
-
for (const event of events) {
|
|
138
|
-
if (typeof event.taskId !== "string" || event.taskId.length === 0) continue;
|
|
139
|
-
if (!isTaskLifecycleEvent(event)) continue;
|
|
140
|
-
|
|
141
|
-
const taskId = event.taskId;
|
|
142
|
-
let task = tasks.get(taskId);
|
|
143
|
-
if (!task) {
|
|
144
|
-
task = { id: taskId, status: "created" };
|
|
145
|
-
tasks.set(taskId, task);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const newStatus = statusFromEventType(event.type);
|
|
149
|
-
if (newStatus && RECONSTRUCTABLE_STATUSES.has(newStatus)) {
|
|
150
|
-
task.status = newStatus;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (event.type === "task.started") {
|
|
154
|
-
task.startedAt = event.time;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (TERMINAL_EVENTS.has(event.type)) {
|
|
158
|
-
task.finishedAt = event.time;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
if (event.type === "task.failed" && event.message) {
|
|
162
|
-
task.error = event.message;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (event.data) {
|
|
166
|
-
const segment = safeNumber(event.data.segment);
|
|
167
|
-
if (segment !== undefined) task.segment = segment;
|
|
168
|
-
|
|
169
|
-
const diagnostics = safeRecord(event.data.diagnostics);
|
|
170
|
-
if (diagnostics !== undefined) task.diagnostics = diagnostics;
|
|
171
|
-
|
|
172
|
-
const metrics = safeNumericRecord(event.data.metrics);
|
|
173
|
-
if (metrics !== undefined) task.metrics = metrics;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return { tasks, eventCount, corruptedCount };
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Reconstruct task states from an append-only event log.
|
|
182
|
-
*
|
|
183
|
-
* @param source - Either a file path to events.jsonl, or an array of TeamEvent objects
|
|
184
|
-
* @returns Reconstruction result with task map, counts
|
|
185
|
-
*/
|
|
186
|
-
export function reconstructTasksFromEvents(source: EventSource): ReconstructionResult {
|
|
187
|
-
const events: TeamEvent[] = typeof source === "string" ? readEvents(source) : source;
|
|
188
|
-
return processEvents(events, events.length, 0);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Reconstruct task states from raw JSONL lines (string array).
|
|
193
|
-
* Useful for testing without creating files.
|
|
194
|
-
*
|
|
195
|
-
* @param lines - Array of raw JSONL lines
|
|
196
|
-
* @returns Reconstruction result
|
|
197
|
-
*/
|
|
198
|
-
export function reconstructTasksFromLines(lines: string[]): ReconstructionResult {
|
|
199
|
-
let eventCount = 0;
|
|
200
|
-
let corruptedCount = 0;
|
|
201
|
-
const parsedEvents: TeamEvent[] = [];
|
|
202
|
-
|
|
203
|
-
for (const line of lines) {
|
|
204
|
-
const trimmed = line.trim();
|
|
205
|
-
if (trimmed.length === 0) continue;
|
|
206
|
-
const event = parseEventLine(trimmed);
|
|
207
|
-
if (event === undefined) {
|
|
208
|
-
corruptedCount++;
|
|
209
|
-
eventCount++;
|
|
210
|
-
continue;
|
|
211
|
-
}
|
|
212
|
-
parsedEvents.push(event);
|
|
213
|
-
eventCount++;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return processEvents(parsedEvents, eventCount, corruptedCount);
|
|
217
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Event reconstructor — rebuilds task state from the append-only event log.
|
|
3
|
+
*
|
|
4
|
+
* Primary use-case: crash recovery when tasks.json is corrupted or missing.
|
|
5
|
+
* The materialized tasks.json view is the primary source of truth; this
|
|
6
|
+
* module provides a fallback reconstruction path from events.jsonl.
|
|
7
|
+
*
|
|
8
|
+
* Distilled from pi-autoresearch's append-only event log pattern.
|
|
9
|
+
*/
|
|
10
|
+
import type { TeamEvent } from "./event-log.ts";
|
|
11
|
+
import { readEvents } from "./event-log.ts";
|
|
12
|
+
|
|
13
|
+
/** Task status values that can be reconstructed from lifecycle events. */
|
|
14
|
+
const RECONSTRUCTABLE_STATUSES = new Set(["created", "queued", "running", "completed", "failed", "cancelled", "skipped", "waiting"]);
|
|
15
|
+
|
|
16
|
+
/** Event types that carry task lifecycle state transitions. */
|
|
17
|
+
const TASK_LIFECYCLE_EVENT_TYPES = new Set([
|
|
18
|
+
"task.created",
|
|
19
|
+
"task.started",
|
|
20
|
+
"task.completed",
|
|
21
|
+
"task.failed",
|
|
22
|
+
"task.skipped",
|
|
23
|
+
"task.cancelled",
|
|
24
|
+
"task.waiting",
|
|
25
|
+
"task.resumed",
|
|
26
|
+
"task.retried",
|
|
27
|
+
"task.blocked",
|
|
28
|
+
"task.progress",
|
|
29
|
+
"task.green",
|
|
30
|
+
"task.red",
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
/** Terminal events that set finishedAt. */
|
|
34
|
+
const TERMINAL_EVENTS = new Set(["task.completed", "task.failed", "task.cancelled", "task.skipped"]);
|
|
35
|
+
|
|
36
|
+
/** Mapping from event type to the reconstructed task status. */
|
|
37
|
+
const EVENT_STATUS_MAP: Readonly<Record<string, string>> = {
|
|
38
|
+
"task.created": "created",
|
|
39
|
+
"task.started": "running",
|
|
40
|
+
"task.completed": "completed",
|
|
41
|
+
"task.failed": "failed",
|
|
42
|
+
"task.skipped": "skipped",
|
|
43
|
+
"task.cancelled": "cancelled",
|
|
44
|
+
"task.waiting": "waiting",
|
|
45
|
+
"task.resumed": "running",
|
|
46
|
+
"task.retried": "queued",
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** Task state reconstructed purely from event log entries. */
|
|
50
|
+
export interface ReconstructedTaskState {
|
|
51
|
+
/** Task identifier */
|
|
52
|
+
id: string;
|
|
53
|
+
/** Reconstructed status derived from the last lifecycle event */
|
|
54
|
+
status: string;
|
|
55
|
+
/** Timestamp of the task.started event, if observed */
|
|
56
|
+
startedAt?: string;
|
|
57
|
+
/** Timestamp of the terminal event (completed/failed/cancelled/skipped), if observed */
|
|
58
|
+
finishedAt?: string;
|
|
59
|
+
/** Error message from task.failed events */
|
|
60
|
+
error?: string;
|
|
61
|
+
/** Segment number from event data (for retry isolation) */
|
|
62
|
+
segment?: number;
|
|
63
|
+
/** Structured diagnostics from event data */
|
|
64
|
+
diagnostics?: Record<string, unknown>;
|
|
65
|
+
/** Numeric metrics from event data */
|
|
66
|
+
metrics?: Record<string, number>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Result of reconstructing task state from events. */
|
|
70
|
+
export interface ReconstructionResult {
|
|
71
|
+
/** Map of taskId → reconstructed task state */
|
|
72
|
+
tasks: Map<string, ReconstructedTaskState>;
|
|
73
|
+
/** Total number of events processed */
|
|
74
|
+
eventCount: number;
|
|
75
|
+
/** Number of malformed/unparseable events skipped */
|
|
76
|
+
corruptedCount: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Input: either a file path to read events from, or an in-memory array. */
|
|
80
|
+
export type EventSource = string | TeamEvent[];
|
|
81
|
+
|
|
82
|
+
function isTaskLifecycleEvent(event: TeamEvent): boolean {
|
|
83
|
+
return TASK_LIFECYCLE_EVENT_TYPES.has(event.type);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function statusFromEventType(eventType: string): string | undefined {
|
|
87
|
+
return EVENT_STATUS_MAP[eventType];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function safeNumber(value: unknown): number | undefined {
|
|
91
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function safeRecord(value: unknown): Record<string, unknown> | undefined {
|
|
95
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
return value as Record<string, unknown>;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function safeNumericRecord(value: unknown): Record<string, number> | undefined {
|
|
102
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
const record: Record<string, number> = {};
|
|
106
|
+
for (const [key, val] of Object.entries(value as Record<string, unknown>)) {
|
|
107
|
+
if (typeof val === "number" && Number.isFinite(val)) {
|
|
108
|
+
record[key] = val;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (Object.keys(record).length === 0) {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
return record;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function parseEventLine(line: string): TeamEvent | undefined {
|
|
118
|
+
const trimmed = line.trim();
|
|
119
|
+
if (trimmed.length === 0) return undefined;
|
|
120
|
+
try {
|
|
121
|
+
const parsed = JSON.parse(trimmed);
|
|
122
|
+
if (typeof parsed !== "object" || parsed === null) return undefined;
|
|
123
|
+
if (typeof parsed.type !== "string" || typeof parsed.runId !== "string") return undefined;
|
|
124
|
+
return parsed as TeamEvent;
|
|
125
|
+
} catch {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Process a stream of validated TeamEvents into reconstructed task states.
|
|
132
|
+
* Shared logic for both file-based and line-based reconstruction.
|
|
133
|
+
*/
|
|
134
|
+
function processEvents(events: Iterable<TeamEvent>, eventCount: number, corruptedCount: number): ReconstructionResult {
|
|
135
|
+
const tasks = new Map<string, ReconstructedTaskState>();
|
|
136
|
+
|
|
137
|
+
for (const event of events) {
|
|
138
|
+
if (typeof event.taskId !== "string" || event.taskId.length === 0) continue;
|
|
139
|
+
if (!isTaskLifecycleEvent(event)) continue;
|
|
140
|
+
|
|
141
|
+
const taskId = event.taskId;
|
|
142
|
+
let task = tasks.get(taskId);
|
|
143
|
+
if (!task) {
|
|
144
|
+
task = { id: taskId, status: "created" };
|
|
145
|
+
tasks.set(taskId, task);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const newStatus = statusFromEventType(event.type);
|
|
149
|
+
if (newStatus && RECONSTRUCTABLE_STATUSES.has(newStatus)) {
|
|
150
|
+
task.status = newStatus;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (event.type === "task.started") {
|
|
154
|
+
task.startedAt = event.time;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (TERMINAL_EVENTS.has(event.type)) {
|
|
158
|
+
task.finishedAt = event.time;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (event.type === "task.failed" && event.message) {
|
|
162
|
+
task.error = event.message;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (event.data) {
|
|
166
|
+
const segment = safeNumber(event.data.segment);
|
|
167
|
+
if (segment !== undefined) task.segment = segment;
|
|
168
|
+
|
|
169
|
+
const diagnostics = safeRecord(event.data.diagnostics);
|
|
170
|
+
if (diagnostics !== undefined) task.diagnostics = diagnostics;
|
|
171
|
+
|
|
172
|
+
const metrics = safeNumericRecord(event.data.metrics);
|
|
173
|
+
if (metrics !== undefined) task.metrics = metrics;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return { tasks, eventCount, corruptedCount };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Reconstruct task states from an append-only event log.
|
|
182
|
+
*
|
|
183
|
+
* @param source - Either a file path to events.jsonl, or an array of TeamEvent objects
|
|
184
|
+
* @returns Reconstruction result with task map, counts
|
|
185
|
+
*/
|
|
186
|
+
export function reconstructTasksFromEvents(source: EventSource): ReconstructionResult {
|
|
187
|
+
const events: TeamEvent[] = typeof source === "string" ? readEvents(source) : source;
|
|
188
|
+
return processEvents(events, events.length, 0);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Reconstruct task states from raw JSONL lines (string array).
|
|
193
|
+
* Useful for testing without creating files.
|
|
194
|
+
*
|
|
195
|
+
* @param lines - Array of raw JSONL lines
|
|
196
|
+
* @returns Reconstruction result
|
|
197
|
+
*/
|
|
198
|
+
export function reconstructTasksFromLines(lines: string[]): ReconstructionResult {
|
|
199
|
+
let eventCount = 0;
|
|
200
|
+
let corruptedCount = 0;
|
|
201
|
+
const parsedEvents: TeamEvent[] = [];
|
|
202
|
+
|
|
203
|
+
for (const line of lines) {
|
|
204
|
+
const trimmed = line.trim();
|
|
205
|
+
if (trimmed.length === 0) continue;
|
|
206
|
+
const event = parseEventLine(trimmed);
|
|
207
|
+
if (event === undefined) {
|
|
208
|
+
corruptedCount++;
|
|
209
|
+
eventCount++;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
parsedEvents.push(event);
|
|
213
|
+
eventCount++;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return processEvents(parsedEvents, eventCount, corruptedCount);
|
|
217
|
+
}
|
|
@@ -1,82 +1,82 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import { redactJsonLine } from "../utils/redaction.ts";
|
|
3
|
-
|
|
4
|
-
export interface DrainableSource {
|
|
5
|
-
pause(): void;
|
|
6
|
-
resume(): void;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface JsonlWriteStream {
|
|
10
|
-
write(chunk: string): boolean;
|
|
11
|
-
once(event: "drain", listener: () => void): JsonlWriteStream;
|
|
12
|
-
end(callback?: () => void): void;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const DEFAULT_MAX_JSONL_BYTES = 50 * 1024 * 1024;
|
|
16
|
-
|
|
17
|
-
export interface JsonlWriterDeps {
|
|
18
|
-
createWriteStream?: (filePath: string) => JsonlWriteStream;
|
|
19
|
-
maxBytes?: number;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface JsonlWriter {
|
|
23
|
-
writeLine(line: string): void;
|
|
24
|
-
close(): Promise<void>;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function createJsonlWriter(filePath: string | undefined, source: DrainableSource, deps: JsonlWriterDeps = {}): JsonlWriter {
|
|
28
|
-
if (!filePath) {
|
|
29
|
-
return {
|
|
30
|
-
writeLine() {},
|
|
31
|
-
async close() {},
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const createWriteStream = deps.createWriteStream ?? ((targetPath: string) => fs.createWriteStream(targetPath, { flags: "a" }));
|
|
36
|
-
let stream: JsonlWriteStream | undefined;
|
|
37
|
-
try {
|
|
38
|
-
stream = createWriteStream(filePath);
|
|
39
|
-
} catch {
|
|
40
|
-
return {
|
|
41
|
-
writeLine() {},
|
|
42
|
-
async close() {},
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
let backpressured = false;
|
|
47
|
-
let closed = false;
|
|
48
|
-
let bytesWritten = 0;
|
|
49
|
-
const maxBytes = deps.maxBytes ?? DEFAULT_MAX_JSONL_BYTES;
|
|
50
|
-
|
|
51
|
-
return {
|
|
52
|
-
writeLine(line: string) {
|
|
53
|
-
if (!stream || closed || !line.trim()) return;
|
|
54
|
-
const safeLine = redactJsonLine(line);
|
|
55
|
-
const chunk = `${safeLine}\n`;
|
|
56
|
-
const chunkBytes = Buffer.byteLength(chunk, "utf-8");
|
|
57
|
-
if (bytesWritten + chunkBytes > maxBytes) return;
|
|
58
|
-
try {
|
|
59
|
-
const ok = stream.write(chunk);
|
|
60
|
-
bytesWritten += chunkBytes;
|
|
61
|
-
if (!ok && !backpressured) {
|
|
62
|
-
backpressured = true;
|
|
63
|
-
source.pause();
|
|
64
|
-
stream.once("drain", () => {
|
|
65
|
-
backpressured = false;
|
|
66
|
-
if (!closed) source.resume();
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
} catch (writeError) {
|
|
70
|
-
// Log the error — silently dropping events is dangerous.
|
|
71
|
-
process.stderr.write(`[pi-crew] jsonl-writer: write failed ${filePath}: ${writeError instanceof Error ? writeError.message : String(writeError)}\n`);
|
|
72
|
-
}
|
|
73
|
-
},
|
|
74
|
-
async close() {
|
|
75
|
-
if (!stream || closed) return;
|
|
76
|
-
closed = true;
|
|
77
|
-
const current = stream;
|
|
78
|
-
stream = undefined;
|
|
79
|
-
await new Promise<void>((resolve) => current.end(() => resolve()));
|
|
80
|
-
},
|
|
81
|
-
};
|
|
82
|
-
}
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import { redactJsonLine } from "../utils/redaction.ts";
|
|
3
|
+
|
|
4
|
+
export interface DrainableSource {
|
|
5
|
+
pause(): void;
|
|
6
|
+
resume(): void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface JsonlWriteStream {
|
|
10
|
+
write(chunk: string): boolean;
|
|
11
|
+
once(event: "drain", listener: () => void): JsonlWriteStream;
|
|
12
|
+
end(callback?: () => void): void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const DEFAULT_MAX_JSONL_BYTES = 50 * 1024 * 1024;
|
|
16
|
+
|
|
17
|
+
export interface JsonlWriterDeps {
|
|
18
|
+
createWriteStream?: (filePath: string) => JsonlWriteStream;
|
|
19
|
+
maxBytes?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface JsonlWriter {
|
|
23
|
+
writeLine(line: string): void;
|
|
24
|
+
close(): Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function createJsonlWriter(filePath: string | undefined, source: DrainableSource, deps: JsonlWriterDeps = {}): JsonlWriter {
|
|
28
|
+
if (!filePath) {
|
|
29
|
+
return {
|
|
30
|
+
writeLine() {},
|
|
31
|
+
async close() {},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const createWriteStream = deps.createWriteStream ?? ((targetPath: string) => fs.createWriteStream(targetPath, { flags: "a" }));
|
|
36
|
+
let stream: JsonlWriteStream | undefined;
|
|
37
|
+
try {
|
|
38
|
+
stream = createWriteStream(filePath);
|
|
39
|
+
} catch {
|
|
40
|
+
return {
|
|
41
|
+
writeLine() {},
|
|
42
|
+
async close() {},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let backpressured = false;
|
|
47
|
+
let closed = false;
|
|
48
|
+
let bytesWritten = 0;
|
|
49
|
+
const maxBytes = deps.maxBytes ?? DEFAULT_MAX_JSONL_BYTES;
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
writeLine(line: string) {
|
|
53
|
+
if (!stream || closed || !line.trim()) return;
|
|
54
|
+
const safeLine = redactJsonLine(line);
|
|
55
|
+
const chunk = `${safeLine}\n`;
|
|
56
|
+
const chunkBytes = Buffer.byteLength(chunk, "utf-8");
|
|
57
|
+
if (bytesWritten + chunkBytes > maxBytes) return;
|
|
58
|
+
try {
|
|
59
|
+
const ok = stream.write(chunk);
|
|
60
|
+
bytesWritten += chunkBytes;
|
|
61
|
+
if (!ok && !backpressured) {
|
|
62
|
+
backpressured = true;
|
|
63
|
+
source.pause();
|
|
64
|
+
stream.once("drain", () => {
|
|
65
|
+
backpressured = false;
|
|
66
|
+
if (!closed) source.resume();
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
} catch (writeError) {
|
|
70
|
+
// Log the error — silently dropping events is dangerous.
|
|
71
|
+
process.stderr.write(`[pi-crew] jsonl-writer: write failed ${filePath}: ${writeError instanceof Error ? writeError.message : String(writeError)}\n`);
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
async close() {
|
|
75
|
+
if (!stream || closed) return;
|
|
76
|
+
closed = true;
|
|
77
|
+
const current = stream;
|
|
78
|
+
stream = undefined;
|
|
79
|
+
await new Promise<void>((resolve) => current.end(() => resolve()));
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|