pi-crew 0.9.68 → 0.10.2
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 +222 -0
- package/NOTICE.md +21 -0
- package/README.md +44 -2
- package/agents/analyst.md +1 -1
- package/agents/cold-verifier.md +3 -1
- package/agents/critic.md +1 -1
- package/agents/executor.md +1 -1
- package/agents/explorer.md +1 -1
- package/agents/planner.md +1 -1
- package/agents/reviewer.md +1 -1
- package/agents/security-reviewer.md +1 -1
- package/agents/test-engineer.md +1 -1
- package/agents/verifier.md +1 -1
- package/agents/writer.md +1 -1
- package/dist/index.mjs +68113 -60774
- package/docs/README.md +2 -0
- package/docs/actions-reference.md +31 -0
- package/docs/commands-reference.md +17 -6
- package/docs/resource-formats.md +13 -0
- package/package.json +4 -2
- package/schema.json +503 -91
- package/scripts/resource-sampler.mjs +36 -2
- package/skills/requirements-to-task-packet/SKILL.md +26 -0
- package/skills/widget-rendering/SKILL.md +7 -7
- package/src/agents/agent-config.ts +2 -1
- package/src/agents/discover-agents.ts +23 -14
- package/src/config/config-merge.ts +183 -0
- package/src/config/config-validation.ts +687 -0
- package/src/config/config.ts +22 -864
- package/src/config/defaults.ts +43 -2
- package/src/config/drift-detector.ts +1 -1
- package/src/config/env-vars.ts +691 -0
- package/src/config/role-tools.ts +11 -9
- package/src/config/sanitize-project-config.ts +172 -0
- package/src/config/types.ts +49 -1
- package/src/extension/async-notifier.ts +25 -2
- package/src/extension/crew-cleanup.ts +13 -0
- package/src/extension/crew-vibes/config.ts +2 -1
- package/src/extension/crew-vibes/footer.ts +19 -0
- package/src/extension/crew-vibes/index.ts +11 -1
- package/src/extension/plan-orchestrate.ts +132 -0
- package/src/extension/register.ts +8 -0
- package/src/extension/registration/command-registration.ts +1 -0
- package/src/extension/registration/commands/dashboard.ts +158 -0
- package/src/extension/registration/commands/index.ts +35 -0
- package/src/extension/registration/commands/manage.ts +303 -0
- package/src/extension/registration/commands/run.ts +228 -0
- package/src/extension/registration/commands/shared.ts +639 -0
- package/src/extension/registration/commands/status.ts +60 -0
- package/src/extension/registration/commands.ts +13 -1224
- package/src/extension/registration/foreground-run-controller.ts +10 -2
- package/src/extension/registration/lifecycle-handlers.ts +178 -17
- package/src/extension/registration/runtime-cleanup.ts +23 -5
- package/src/extension/registration/subagent-tools.ts +218 -9
- package/src/extension/registration/team-tool.ts +5 -1
- package/src/extension/registration/ui.ts +5 -4
- package/src/extension/rpc-hmac.ts +5 -3
- package/src/extension/team-tool/api/heartbeat.ts +47 -10
- package/src/extension/team-tool/api/plan-approval.ts +9 -0
- package/src/extension/team-tool/api/task-claims.ts +109 -40
- package/src/extension/team-tool/cancel.ts +84 -50
- package/src/extension/team-tool/dispatch/index.ts +1 -0
- package/src/extension/team-tool/dispatch/run.ts +4 -1
- package/src/extension/team-tool/doctor.ts +103 -1
- package/src/extension/team-tool/orchestrate.ts +66 -1
- package/src/extension/team-tool/plans.ts +192 -0
- package/src/extension/team-tool/respond.ts +197 -65
- package/src/extension/team-tool/run-deadline.ts +35 -3
- package/src/extension/team-tool/run-intent.ts +63 -0
- package/src/extension/team-tool/run.ts +74 -20
- package/src/extension/team-tool/status.ts +84 -26
- package/src/extension/team-tool.ts +11 -2
- package/src/hooks/registry.ts +1 -6
- package/src/i18n.ts +9 -0
- package/src/prompt/prompt-runtime.ts +521 -2
- package/src/prompt/worker-events-channel.ts +173 -0
- package/src/runtime/README.md +8 -8
- package/src/runtime/async-runner.ts +7 -3
- package/src/runtime/background-runner.ts +42 -14
- package/src/runtime/broker/broker-issuer.ts +9 -2
- package/src/runtime/broker/crew-broker-tokens.ts +43 -6
- package/src/runtime/broker/crew-broker.ts +838 -10
- package/src/runtime/broker/wait-status-cache.ts +157 -0
- package/src/runtime/budget-enforcement.ts +281 -0
- package/src/runtime/child-pi/child-pi-constants.ts +8 -0
- package/src/runtime/child-pi/child-pi-spawn.ts +60 -14
- package/src/runtime/child-pi/child-pi-streams.ts +21 -1
- package/src/runtime/child-pi/child-pi-timers.ts +324 -0
- package/src/runtime/child-pi/child-pi.ts +97 -201
- package/src/runtime/child-pi/mock-fixtures.ts +16 -2
- package/src/runtime/crew-agent-records.ts +259 -14
- package/src/runtime/delegate-spawn.ts +148 -0
- package/src/runtime/detached-run-results.ts +90 -0
- package/src/runtime/deterministic-ast.ts +2 -1
- package/src/runtime/dispatch-batch.ts +945 -0
- package/src/runtime/finalize-run.ts +557 -0
- package/src/runtime/goal-workflow/adaptive-plan.ts +116 -15
- package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +8 -3
- package/src/runtime/goal-workflow/goal-state-store.ts +1 -1
- package/src/runtime/group-join.ts +11 -125
- package/src/runtime/live-session/live-session-runtime.ts +26 -1
- package/src/runtime/merge-gate.ts +32 -10
- package/src/runtime/merge-loop.ts +130 -0
- package/src/runtime/model/model-budget-summary.ts +53 -0
- package/src/runtime/model/model-fallback.ts +36 -2
- package/src/runtime/model/pi-args.ts +10 -0
- package/src/runtime/model/provider-extensions.ts +10 -0
- package/src/runtime/orphan-worker-registry.ts +1 -1
- package/src/runtime/output/output-validator.ts +45 -0
- package/src/runtime/parent-guard.ts +3 -1
- package/src/runtime/peer-dep.ts +2 -1
- package/src/runtime/per-write-validator.ts +0 -5
- package/src/runtime/pi-spawn.ts +61 -15
- package/src/runtime/plan-approval.ts +125 -0
- package/src/runtime/plan-replan.ts +151 -0
- package/src/runtime/process-status.ts +16 -1
- package/src/runtime/recovery/checkpoint.ts +0 -18
- package/src/runtime/recovery/crash-recovery.ts +111 -46
- package/src/runtime/run-tracker.ts +77 -10
- package/src/runtime/scheduler-context.ts +98 -0
- package/src/runtime/scheduling/coalesce-tasks.ts +5 -0
- package/src/runtime/scheduling/global-worker-cap.ts +2 -1
- package/src/runtime/scheduling/nested-slots.ts +70 -0
- package/src/runtime/scheduling/run-coalesced-task-group.ts +64 -13
- package/src/runtime/scheduling/task-graph-scheduler.ts +0 -10
- package/src/runtime/settings-store.ts +219 -0
- package/src/runtime/spawn-policy.ts +217 -0
- package/src/runtime/stale-reconciler.ts +87 -6
- package/src/runtime/subagent-manager.ts +25 -1
- package/src/runtime/task-output-context.ts +230 -9
- package/src/runtime/task-packet.ts +23 -1
- package/src/runtime/task-runner/child-executor.ts +106 -7
- package/src/runtime/task-runner/post-execution.ts +125 -1
- package/src/runtime/task-runner/pre-execution.ts +39 -1
- package/src/runtime/task-runner/prompt-builder.ts +51 -1
- package/src/runtime/task-runner/retrieval-orchestrator.ts +72 -18
- package/src/runtime/task-runner/spec-evidence.ts +403 -0
- package/src/runtime/task-runner/state-helpers.ts +26 -24
- package/src/runtime/task-runner.ts +11 -0
- package/src/runtime/team-runner.ts +132 -1673
- package/src/runtime/verification/spec-sandbox.ts +255 -0
- package/src/runtime/verification/verification-gates.ts +3 -2
- package/src/runtime/verification/verification-worktree.ts +2 -1
- package/src/runtime/workflow-phase-advance.ts +100 -0
- package/src/runtime/workspace-tree.ts +9 -0
- package/src/schema/config-schema.ts +66 -26
- package/src/schema/sensitive-config-paths.ts +64 -0
- package/src/schema/team-tool-schema.ts +13 -3
- package/src/state/README.md +4 -10
- package/src/state/atomic-write.ts +20 -3
- package/src/state/contracts.ts +38 -0
- package/src/state/coordination/mailbox.ts +12 -2
- package/src/state/event-log/cursor.ts +223 -0
- package/src/state/event-log/event-log-rotation.ts +12 -4
- package/src/state/event-log/event-log.ts +152 -369
- package/src/state/event-log/sequence-cache.ts +373 -0
- package/src/state/event-log/worker-atomic-writer.ts +2 -1
- package/src/state/stores/active-run-registry.ts +3 -2
- package/src/state/stores/manifest-io.ts +237 -0
- package/src/state/stores/ownership-map.ts +162 -0
- package/src/state/stores/plan-store.ts +241 -0
- package/src/state/stores/run-cache.ts +0 -90
- package/src/state/stores/spec-store.ts +189 -0
- package/src/state/stores/state-store.ts +139 -232
- package/src/state/types.ts +199 -0
- package/src/ui/dashboard-panes/plan-pane.ts +136 -0
- package/src/ui/dashboard-panes/progress-pane.ts +6 -0
- package/src/ui/dashboard-panes/transcript-pane.ts +31 -0
- package/src/ui/dock-footer.ts +49 -0
- package/src/ui/heartbeat-aggregator.ts +9 -1
- package/src/ui/inline-panel/agent-pane.ts +375 -0
- package/src/ui/inline-panel/agent-transcript.ts +338 -0
- package/src/ui/inline-panel/agent-view-overlay.ts +225 -0
- package/src/ui/inline-panel/crew-editor.ts +192 -0
- package/src/ui/inline-panel/index.ts +290 -0
- package/src/ui/inline-panel/panel-rows.ts +37 -0
- package/src/ui/inline-panel/panel-selection.ts +157 -0
- package/src/ui/inline-panel/panel-store.ts +111 -0
- package/src/ui/inline-panel/view-session-store.ts +36 -0
- package/src/ui/keybinding-map.ts +54 -13
- package/src/ui/pi-ui-compat.ts +9 -0
- package/src/ui/powerbar-publisher.ts +52 -1
- package/src/ui/run-dashboard.ts +31 -5
- package/src/ui/run-snapshot-cache.ts +57 -30
- package/src/ui/snapshot-types.ts +6 -1
- package/src/ui/widget/index.ts +176 -22
- package/src/ui/widget/task-list.ts +198 -0
- package/src/ui/widget/widget-formatters.ts +240 -4
- package/src/ui/widget/widget-renderer.ts +243 -38
- package/src/ui/widget/widget-types.ts +11 -0
- package/src/utils/child-process-shield.ts +106 -0
- package/src/utils/file-coalescer.ts +0 -4
- package/src/utils/fs-errno.ts +66 -0
- package/src/utils/fs-watch.ts +1 -1
- package/src/utils/internal-error.ts +3 -1
- package/src/utils/paths.ts +11 -3
- package/src/utils/redaction.ts +7 -0
- package/src/utils/safe-abort.ts +45 -0
- package/src/utils/task-name-generator.ts +1 -8
- package/src/workflows/discover-workflows.ts +20 -2
- package/src/workflows/validate-workflow.ts +7 -1
- package/src/workflows/workflow-config.ts +17 -0
- package/src/workflows/workflow-serializer.ts +3 -0
- package/src/worktree/worktree-manager.ts +22 -0
- package/workflows/default.workflow.md +36 -26
- package/workflows/strict-fast-fix.workflow.md +26 -0
- package/src/agents/agent-search.ts +0 -98
- package/src/benchmark/benchmark-runner.ts +0 -313
- package/src/benchmark/feedback-loop.ts +0 -73
- package/src/config/resilient-parser.ts +0 -117
- package/src/extension/crew-vibes/cat-frames.ts +0 -18
- package/src/extension/result-watcher.ts +0 -139
- package/src/observability/exporters/prometheus-exporter.ts +0 -54
- package/src/observability/metric-retention.ts +0 -64
- package/src/runtime/compaction/compaction-summary.ts +0 -278
- package/src/runtime/errors/crew-errors.ts +0 -162
- package/src/runtime/live-session/intercom-bridge.ts +0 -187
- package/src/runtime/loop-gates.ts +0 -128
- package/src/runtime/metric-parser.ts +0 -36
- package/src/runtime/output/stream-preview.ts +0 -184
- package/src/runtime/output/tool-progress.ts +0 -278
- package/src/runtime/phase-tracker.ts +0 -385
- package/src/runtime/pipeline-runner.ts +0 -523
- package/src/runtime/process/process-lifecycle.ts +0 -491
- package/src/runtime/recovery/retry-runner.ts +0 -330
- package/src/runtime/run-drift.ts +0 -219
- package/src/runtime/task-quality.ts +0 -199
- package/src/runtime/task-runner/run-projection.ts +0 -128
- package/src/runtime/verification/post-checks.ts +0 -142
- package/src/state/coordination/schedule.ts +0 -166
- package/src/state/event-log/jsonl-writer.ts +0 -115
- package/src/state/hook-instinct-bridge.ts +0 -94
- package/src/state/hook-integrations.ts +0 -51
- package/src/state/session-state-map.ts +0 -51
- package/src/state/stores/blob-store.ts +0 -308
- package/src/state/stores/instinct-store.ts +0 -275
- package/src/state/stores/observation-store.ts +0 -176
- package/src/state/tiered-eval.ts +0 -480
- package/src/state/types-eval.ts +0 -58
- package/src/tools/safe-bash-extension.ts +0 -54
- package/src/tools/safe-bash.ts +0 -505
- package/src/ui/agent-management-overlay.ts +0 -160
- package/src/ui/crew-footer.ts +0 -102
- package/src/ui/crew-select-list.ts +0 -114
- package/src/ui/dashboard-panes/capability-pane.ts +0 -77
- package/src/ui/transcript-entries.ts +0 -256
- package/src/utils/conflict-detect.ts +0 -721
- package/src/utils/fingerprint.ts +0 -180
- package/src/utils/gh-protocol.ts +0 -556
- package/src/utils/project-detector.ts +0 -160
- package/src/utils/sse-parser.ts +0 -131
- package/src/workflows/cost-estimator.ts +0 -34
- package/src/workflows/intermediate-store.ts +0 -166
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { PiTeamsConfigSchema } from "./config-schema.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Schema-driven sensitive-config discovery (Phase 5.1, ADR
|
|
5
|
+
* docs/decisions/2026-08-15-schema-driven-sanitize.md).
|
|
6
|
+
*
|
|
7
|
+
* Sensitive fields are marked in `config-schema.ts` via TypeBox Options
|
|
8
|
+
* metadata: `Type.Boolean({ sensitive: true })`. In TypeBox 0.34 the
|
|
9
|
+
* constructor spreads unknown option keys verbatim onto the emitted schema
|
|
10
|
+
* object (`build/cjs/type/create/type.js` CreateType: `options !== undefined
|
|
11
|
+
* ? { ...options, ...schema } : schema`), and `Type.Optional` is a flat
|
|
12
|
+
* spread modifier (`{ ...schema, [OptionalKind]: 'Optional' }`), so the mark
|
|
13
|
+
* lands directly on `properties[key]` and survives Clone/Decode.
|
|
14
|
+
*
|
|
15
|
+
* The walk below collects the dotted paths of every marked property. Marked
|
|
16
|
+
* properties are TERMINAL: Record/Object-valued marked props
|
|
17
|
+
* (`agents.overrides`, `otlp.headers`, `runtime.isolationPolicy`) collapse
|
|
18
|
+
* to a single dotted path instead of being recursed into.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
type SchemaLike = {
|
|
22
|
+
sensitive?: unknown;
|
|
23
|
+
properties?: Record<string, SchemaLike>;
|
|
24
|
+
anyOf?: SchemaLike[];
|
|
25
|
+
} & Record<string, unknown>;
|
|
26
|
+
|
|
27
|
+
function isMarkedSensitive(property: SchemaLike): boolean {
|
|
28
|
+
if (property.sensitive === true) return true;
|
|
29
|
+
// Defensive: a mark placed on a Union *member* rather than the outermost
|
|
30
|
+
// property schema (the recommended form) still counts. Not used by the
|
|
31
|
+
// current config schema; guards future editors.
|
|
32
|
+
const members = property.anyOf;
|
|
33
|
+
if (Array.isArray(members) && members.some((member) => member.sensitive === true)) return true;
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function walkProperties(schema: SchemaLike, prefix: string, out: string[]): void {
|
|
38
|
+
const properties = schema.properties;
|
|
39
|
+
if (!properties || typeof properties !== "object") return;
|
|
40
|
+
for (const [key, property] of Object.entries(properties)) {
|
|
41
|
+
if (!property || typeof property !== "object") continue;
|
|
42
|
+
const dotted = prefix === "" ? key : `${prefix}.${key}`;
|
|
43
|
+
if (isMarkedSensitive(property)) {
|
|
44
|
+
out.push(dotted);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
// `Type.Optional` needs no unwrapping in 0.34 (flat spread — the mark
|
|
48
|
+
// and `.properties` sit on the property schema itself). Union members
|
|
49
|
+
// are visited defensively for nested Object schemas.
|
|
50
|
+
const branches: SchemaLike[] = Array.isArray(property.anyOf) ? property.anyOf : [property];
|
|
51
|
+
for (const branch of branches) {
|
|
52
|
+
if (branch && typeof branch === "object" && branch.properties) walkProperties(branch, dotted, out);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Dotted paths of every `sensitive: true`-marked field in the config schema
|
|
58
|
+
* (top-level keys unprefixed, nested keys as `section.key`). Order follows
|
|
59
|
+
* schema property declaration order. */
|
|
60
|
+
export function collectSensitiveConfigPaths(schema: SchemaLike = PiTeamsConfigSchema as SchemaLike): string[] {
|
|
61
|
+
const out: string[] = [];
|
|
62
|
+
walkProperties(schema, "", out);
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
@@ -235,7 +235,13 @@ const sharedFields = {
|
|
|
235
235
|
description: "Path to a markdown plan document for orchestration.",
|
|
236
236
|
}),
|
|
237
237
|
),
|
|
238
|
-
subAction: Type.Optional(
|
|
238
|
+
subAction: Type.Optional(
|
|
239
|
+
Type.String({ description: "Sub-action. For action='plans': get (default) | list | diff | approve | reject." }),
|
|
240
|
+
),
|
|
241
|
+
// T2/R4 (ADR-4 §7): plans action params.
|
|
242
|
+
rev: Type.Optional(Type.Number({ description: "plans get: pin a specific plan revision (default: current)." })),
|
|
243
|
+
a: Type.Optional(Type.Number({ description: "plans diff: left revision number." })),
|
|
244
|
+
b: Type.Optional(Type.Number({ description: "plans diff: right revision number." })),
|
|
239
245
|
jobId: Type.Optional(Type.String({ description: "Job ID for schedule management actions." })),
|
|
240
246
|
cron: Type.Optional(
|
|
241
247
|
Type.String({
|
|
@@ -378,11 +384,11 @@ const sharedFields = {
|
|
|
378
384
|
),
|
|
379
385
|
};
|
|
380
386
|
|
|
381
|
-
// ─── Domain action unions (
|
|
387
|
+
// ─── Domain action unions (10+16+7+16+6 = 55 actions) ──────────────────────
|
|
382
388
|
|
|
383
389
|
const ACTION_DESCRIPTION = "Team action. Defaults to 'list' when omitted.";
|
|
384
390
|
|
|
385
|
-
const RUN_ACTIONS = ["run", "parallel", "plan", "orchestrate", "resume", "retry", "wait", "steer", "goal"] as const;
|
|
391
|
+
const RUN_ACTIONS = ["run", "parallel", "plan", "plans", "orchestrate", "resume", "retry", "wait", "steer", "goal"] as const;
|
|
386
392
|
const runActions = Type.Optional(buildStringEnum(RUN_ACTIONS, ACTION_DESCRIPTION));
|
|
387
393
|
|
|
388
394
|
const STATUS_ACTIONS = [
|
|
@@ -545,6 +551,10 @@ export interface TeamToolParamsValue {
|
|
|
545
551
|
// schedule sub-actions (removal/toggle/update of an existing job)
|
|
546
552
|
subAction?: string;
|
|
547
553
|
jobId?: string;
|
|
554
|
+
// T2/R4 (ADR-4 §7): plans action params.
|
|
555
|
+
rev?: number;
|
|
556
|
+
a?: number;
|
|
557
|
+
b?: number;
|
|
548
558
|
/** Mark certain bash commands as excludeFromContext to reduce context tokens (default: false). */
|
|
549
559
|
excludeContextBash?: boolean;
|
|
550
560
|
/** Total token budget for the run. When set, enables budget tracking (minimum 1000). */
|
package/src/state/README.md
CHANGED
|
@@ -8,15 +8,10 @@ responsibility (Phase 7 reorg).
|
|
|
8
8
|
| File | Responsibility |
|
|
9
9
|
|------|---------------|
|
|
10
10
|
| `types.ts` | Core state types (`TeamRunManifest`, `TeamTaskState`, schema versions) |
|
|
11
|
-
| `types-eval.ts` | Evaluation-related types |
|
|
12
11
|
| `contracts.ts` | Status transitions, task-status enums |
|
|
13
|
-
| `tiered-eval.ts` | Tiered evaluation logic |
|
|
14
12
|
| `crew-init.ts` | Crew initialization / bootstrap |
|
|
15
13
|
| `decision-ledger.ts` | Decision recording |
|
|
16
14
|
| `gitignore-manager.ts` | `.gitignore` management for crew dirs |
|
|
17
|
-
| `hook-instinct-bridge.ts` | Bridge between hooks and instinct store |
|
|
18
|
-
| `hook-integrations.ts` | Hook integration points |
|
|
19
|
-
| `session-state-map.ts` | Session → state mapping |
|
|
20
15
|
| `usage.ts` | Token/cost usage tracking |
|
|
21
16
|
| `atomic-write.ts` | Atomic file writes (widely used — kept at root) |
|
|
22
17
|
|
|
@@ -26,19 +21,18 @@ Event append/read, rotation/compaction, JSONL writing, and worker-thread
|
|
|
26
21
|
atomic writes.
|
|
27
22
|
|
|
28
23
|
- `event-log.ts`, `event-log-rotation.ts`, `event-reconstructor.ts`,
|
|
29
|
-
`
|
|
24
|
+
`worker-atomic-writer.ts`
|
|
30
25
|
|
|
31
26
|
## `stores/` — Persisted data stores
|
|
32
27
|
|
|
33
28
|
Manifest, artifact, blob, observation, health, instinct, run-graph/metrics
|
|
34
29
|
stores, and the active-run registry.
|
|
35
30
|
|
|
36
|
-
- `state-store.ts`, `run-cache.ts`, `artifact-store.ts`,
|
|
37
|
-
`
|
|
38
|
-
`active-run-registry.ts`, `run-graph.ts`, `run-metrics.ts`
|
|
31
|
+
- `state-store.ts`, `run-cache.ts`, `artifact-store.ts`,
|
|
32
|
+
`health-store.ts`, `active-run-registry.ts`, `run-graph.ts`, `run-metrics.ts`
|
|
39
33
|
|
|
40
34
|
## `coordination/` — Concurrency & IPC
|
|
41
35
|
|
|
42
36
|
File locks, mailbox messaging, task scheduling, and task claims.
|
|
43
37
|
|
|
44
|
-
- `locks.ts`, `mailbox.ts`, `
|
|
38
|
+
- `locks.ts`, `mailbox.ts`, `task-claims.ts`
|
|
@@ -962,12 +962,29 @@ function cancelPendingCoalescedWrite(filePath: string): void {
|
|
|
962
962
|
}
|
|
963
963
|
}
|
|
964
964
|
|
|
965
|
-
/**
|
|
966
|
-
|
|
965
|
+
/**
|
|
966
|
+
* Flush every queued coalesced write synchronously. Safe to call any time.
|
|
967
|
+
*
|
|
968
|
+
* R10-2: pass `filePath` to flush ONLY the pending coalesced entry for that
|
|
969
|
+
* exact file. Hot read paths that know which file they are about to read
|
|
970
|
+
* (e.g. readCrewAgents) no longer wait on unrelated coalesced writes for
|
|
971
|
+
* OTHER files — previously this drained the entire process-wide queue on
|
|
972
|
+
* every such read. Omitted → exact previous global-drain behavior
|
|
973
|
+
* (backward compatible: merge-loop / budget-enforcement / finalize-run /
|
|
974
|
+
* state-helpers / process-exit handlers all rely on the global drain).
|
|
975
|
+
* Re-entrancy guard (`flushInProgress`) applies to both modes: a scoped flush
|
|
976
|
+
* triggered while a global flush is running is a no-op (the global flush
|
|
977
|
+
* already covers this file), and vice versa.
|
|
978
|
+
*/
|
|
979
|
+
export function flushPendingAtomicWrites(filePath?: string): void {
|
|
967
980
|
if (flushInProgress > 0) return;
|
|
968
981
|
flushInProgress++;
|
|
969
982
|
try {
|
|
970
|
-
|
|
983
|
+
if (filePath === undefined) {
|
|
984
|
+
for (const pending of [...pendingAtomicWrites.keys()]) flushOnePendingAtomicWrite(pending);
|
|
985
|
+
} else if (pendingAtomicWrites.has(filePath)) {
|
|
986
|
+
flushOnePendingAtomicWrite(filePath);
|
|
987
|
+
}
|
|
971
988
|
} finally {
|
|
972
989
|
flushInProgress--;
|
|
973
990
|
}
|
package/src/state/contracts.ts
CHANGED
|
@@ -53,9 +53,16 @@ export const TEAM_EVENT_TYPES = [
|
|
|
53
53
|
"run.completed",
|
|
54
54
|
"run.failed",
|
|
55
55
|
"run.cancelled",
|
|
56
|
+
"run.terminal_preserved",
|
|
56
57
|
"task.created",
|
|
57
58
|
"task.started",
|
|
58
59
|
"task.progress",
|
|
60
|
+
// T4/R6 (ADR-6 + erratum): spec-system events
|
|
61
|
+
"spec.frozen",
|
|
62
|
+
"spec.freeze_failed",
|
|
63
|
+
"spec.strict_platform_warning",
|
|
64
|
+
"spec.check_failed",
|
|
65
|
+
"task.spec_gate",
|
|
59
66
|
"hook.pre_step_started",
|
|
60
67
|
"hook.pre_step_completed",
|
|
61
68
|
"hook.pre_step_failed",
|
|
@@ -88,7 +95,38 @@ export const TEAM_EVENT_TYPES = [
|
|
|
88
95
|
"task.waiting",
|
|
89
96
|
"task.resumed",
|
|
90
97
|
"task.retried",
|
|
98
|
+
// WP-2/R2 waiting-producer (ADR-0 2026-08-17-waiting-producer-ask item 10):
|
|
99
|
+
// `ask` tool lifecycle — requested on park acceptance, answered on delivery
|
|
100
|
+
// (mailbox or requeue+inject), timedout on deadline expiry (both the
|
|
101
|
+
// alive-in-tool and dead-requeue outcomes).
|
|
102
|
+
"ask.requested",
|
|
103
|
+
"ask.answered",
|
|
104
|
+
"ask.timedout",
|
|
91
105
|
"supervisor.contact",
|
|
106
|
+
// T2/R4 first-class Plan object (ADR-4 docs/decisions/2026-08-17-plan-object.md §9):
|
|
107
|
+
// plan-store revision/approval mutations. `plan.approved` and `plan.cancelled`
|
|
108
|
+
// formalize emitters that api/plan-approval.ts:66-67,144-145 already wrote
|
|
109
|
+
// unregistered (pre-existing gap closed by the ADR). The scheduler's
|
|
110
|
+
// items[].taskIds linkage writes append NO event (task dispatch logs its own).
|
|
111
|
+
"plan.created",
|
|
112
|
+
"plan.revised",
|
|
113
|
+
"plan.approved",
|
|
114
|
+
"plan.rejected",
|
|
115
|
+
"plan.cancelled",
|
|
116
|
+
"plan.item.dropped",
|
|
117
|
+
// plan-approval.ts ensurePlanApprovalRequested emits this when the gate
|
|
118
|
+
// lights up — pre-existing unregistered emitter formalized with the rest.
|
|
119
|
+
"plan.approval_required",
|
|
120
|
+
// T3/R5 (ADR-5): governed-nesting delegate lifecycle. Emitted by the
|
|
121
|
+
// broker's delegate.request handler (crew-broker.ts) — every rejection
|
|
122
|
+
// leaves a delegate.rejected trace (never silent), usage roll-up lands as
|
|
123
|
+
// delegate.rolled_up on the parent task record.
|
|
124
|
+
"delegate.requested",
|
|
125
|
+
"delegate.admitted",
|
|
126
|
+
"delegate.rejected",
|
|
127
|
+
"delegate.completed",
|
|
128
|
+
"delegate.timed_out",
|
|
129
|
+
"delegate.rolled_up",
|
|
92
130
|
// Budget tracking events
|
|
93
131
|
"budget.initialized",
|
|
94
132
|
"budget.warning",
|
|
@@ -67,6 +67,10 @@ export interface MailboxMessage {
|
|
|
67
67
|
priority?: MailboxMessagePriority;
|
|
68
68
|
deliveryMode?: MailboxDeliveryMode;
|
|
69
69
|
taskId?: string;
|
|
70
|
+
/** WP-2/R2 (ADR-0 item 11/F11): ask-response correlation id (randomUUID).
|
|
71
|
+
* Carried by `kind:"response"` entries; matches task.waiting.questionId —
|
|
72
|
+
* matching must be exact equality (never prefix/substring). */
|
|
73
|
+
questionId?: string;
|
|
70
74
|
acknowledgedAt?: string;
|
|
71
75
|
data?: Record<string, unknown>;
|
|
72
76
|
/** ID of the original message this is a reply to. */
|
|
@@ -294,6 +298,7 @@ function parseMailboxMessage(raw: unknown, expectedDirection: MailboxDirection):
|
|
|
294
298
|
priority: isPriority(obj.priority) ? obj.priority : undefined,
|
|
295
299
|
deliveryMode: isDeliveryMode(obj.deliveryMode) ? obj.deliveryMode : undefined,
|
|
296
300
|
taskId: typeof obj.taskId === "string" ? obj.taskId : undefined,
|
|
301
|
+
questionId: typeof obj.questionId === "string" ? obj.questionId : undefined,
|
|
297
302
|
acknowledgedAt: typeof obj.acknowledgedAt === "string" ? obj.acknowledgedAt : undefined,
|
|
298
303
|
data,
|
|
299
304
|
replyTo: typeof obj.replyTo === "string" ? obj.replyTo : undefined,
|
|
@@ -487,8 +492,11 @@ export function readDeliveryState(manifest: TeamRunManifest): MailboxDeliverySta
|
|
|
487
492
|
}
|
|
488
493
|
// NEW-R4: prominent (ungated) error so corrupt-delivery re-delivery risk is
|
|
489
494
|
// visible even without PI_TEAMS_DEBUG — messages may be re-delivered.
|
|
490
|
-
|
|
491
|
-
|
|
495
|
+
logInternalError(
|
|
496
|
+
"mailbox.readDeliveryState",
|
|
497
|
+
error,
|
|
498
|
+
`corrupt delivery.json quarantined to ${quarantinePath} — delivery state reset to empty; messages may be re-delivered`,
|
|
499
|
+
"error",
|
|
492
500
|
);
|
|
493
501
|
deliveryCache.delete(filePath);
|
|
494
502
|
return { messages: {}, updatedAt: new Date().toISOString() };
|
|
@@ -565,6 +573,7 @@ export function appendMailboxMessage(
|
|
|
565
573
|
priority: message.priority,
|
|
566
574
|
deliveryMode: message.deliveryMode,
|
|
567
575
|
taskId: message.taskId,
|
|
576
|
+
questionId: message.questionId,
|
|
568
577
|
data: message.data,
|
|
569
578
|
replyTo: message.replyTo,
|
|
570
579
|
replyFrom: message.replyFrom,
|
|
@@ -690,6 +699,7 @@ export async function appendMailboxMessageAsync(
|
|
|
690
699
|
priority: message.priority,
|
|
691
700
|
deliveryMode: message.deliveryMode,
|
|
692
701
|
taskId: message.taskId,
|
|
702
|
+
questionId: message.questionId,
|
|
693
703
|
data: message.data,
|
|
694
704
|
replyTo: message.replyTo,
|
|
695
705
|
replyFrom: message.replyFrom,
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { type IncrementalReadState, readJsonlSince, readJsonlTail } from "../../utils/incremental-reader.ts";
|
|
4
|
+
import { logInternalError } from "../../utils/internal-error.ts";
|
|
5
|
+
import type { TeamEvent } from "./event-log.ts";
|
|
6
|
+
import { currentGeneration } from "./event-log-rotation.ts";
|
|
7
|
+
|
|
8
|
+
// --- R18 / R16-B1 effect 2 (Phase 3.6): archive-tail readers ----------------
|
|
9
|
+
// Rotation stranding: a sync append that was mid-appendFileSync holding an fd
|
|
10
|
+
// on the RENAMED inode (ST-8 rename+create) lands in the archive file, not the
|
|
11
|
+
// live file. Round 18 measured 5.43% stranded events under max-contention
|
|
12
|
+
// zero-lock repro. Previously readers saw ONLY the live file, so stranded
|
|
13
|
+
// events were invisible — and sweepOldArchives unlinked them after 7 days
|
|
14
|
+
// (gone forever). Fix (Round 18 option (a)): mirror the mailbox
|
|
15
|
+
// safeReadMailboxFile archive-walk — readers also drain archive TAILS,
|
|
16
|
+
// deduped by seq, merged ahead of the live file's events.
|
|
17
|
+
|
|
18
|
+
/** List `<eventsPath>.<ts>.archive.jsonl` siblings (matches rotateEventLogUnlocked's
|
|
19
|
+
* naming), sorted by name (timestamp) so older generations come first. */
|
|
20
|
+
function listEventArchivePaths(eventsPath: string): string[] {
|
|
21
|
+
const dir = path.dirname(eventsPath);
|
|
22
|
+
const base = path.basename(eventsPath);
|
|
23
|
+
try {
|
|
24
|
+
return fs
|
|
25
|
+
.readdirSync(dir)
|
|
26
|
+
.filter((entry) => entry.startsWith(`${base}.`) && entry.endsWith(".archive.jsonl"))
|
|
27
|
+
.sort()
|
|
28
|
+
.map((entry) => path.join(dir, entry));
|
|
29
|
+
} catch {
|
|
30
|
+
// Directory missing — nothing to read.
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Parse a JSONL file into TeamEvents, skipping corrupt/blank lines (mirrors
|
|
36
|
+
* the legacy readEvents parser). Returns [] when the file is unreadable. */
|
|
37
|
+
function parseJsonlEvents(filePath: string): TeamEvent[] {
|
|
38
|
+
let raw: string;
|
|
39
|
+
try {
|
|
40
|
+
raw = fs.readFileSync(filePath, "utf-8");
|
|
41
|
+
} catch {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
const events: TeamEvent[] = [];
|
|
45
|
+
for (const line of raw.split("\n")) {
|
|
46
|
+
const trimmed = line.trim();
|
|
47
|
+
if (!trimmed) continue;
|
|
48
|
+
try {
|
|
49
|
+
events.push(JSON.parse(trimmed) as TeamEvent);
|
|
50
|
+
} catch {
|
|
51
|
+
/* skip corrupt lines */
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return events;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const ARCHIVE_TAIL_BYTES = 4 * 1024 * 1024; // 4 MB — mirrors readEventsCursor's TAIL_BYTES
|
|
58
|
+
|
|
59
|
+
/** Drain archive TAILS: events with seq > sinceSeq from every archive of
|
|
60
|
+
* eventsPath, deduped by seq, sorted by seq. Stranded in-flight-fd appends
|
|
61
|
+
* land at the END of the archive (the renamed pre-rotation inode), so a
|
|
62
|
+
* bounded tail read per archive is sufficient and keeps this O(tail bytes). */
|
|
63
|
+
function readArchiveTailEvents(eventsPath: string, sinceSeq: number): TeamEvent[] {
|
|
64
|
+
const archives = listEventArchivePaths(eventsPath);
|
|
65
|
+
if (archives.length === 0) return [];
|
|
66
|
+
const bySeq = new Map<number, TeamEvent>();
|
|
67
|
+
for (const archivePath of archives) {
|
|
68
|
+
const tail = readJsonlTail<TeamEvent>(archivePath, ARCHIVE_TAIL_BYTES);
|
|
69
|
+
for (const event of tail.items) {
|
|
70
|
+
const seq = event.metadata?.seq;
|
|
71
|
+
if (typeof seq !== "number" || seq <= sinceSeq) continue;
|
|
72
|
+
if (!bySeq.has(seq)) bySeq.set(seq, event);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return [...bySeq.values()].sort((a, b) => (a.metadata?.seq ?? 0) - (b.metadata?.seq ?? 0));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Merge drained archive-tail events with live-file events: dedup by seq
|
|
79
|
+
* (live wins on collision — it is the persisted survivor), seq-sorted. */
|
|
80
|
+
function mergeArchiveTailEvents(archiveEvents: TeamEvent[], liveEvents: TeamEvent[]): TeamEvent[] {
|
|
81
|
+
if (archiveEvents.length === 0) return liveEvents;
|
|
82
|
+
const bySeq = new Map<number, TeamEvent>();
|
|
83
|
+
for (const event of archiveEvents) bySeq.set(event.metadata?.seq ?? 0, event);
|
|
84
|
+
for (const event of liveEvents) bySeq.set(event.metadata?.seq ?? 0, event);
|
|
85
|
+
return [...bySeq.values()].sort((a, b) => (a.metadata?.seq ?? 0) - (b.metadata?.seq ?? 0));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function readEvents(eventsPath: string): TeamEvent[] {
|
|
89
|
+
// R18 / R16-B1 effect 2 (Phase 3.6): FULL-HISTORY semantics — merge every
|
|
90
|
+
// archive (pre-rotation generations, including stranded in-flight appends)
|
|
91
|
+
// with the live file, dedup by seq, ordered by seq. Callers needing the
|
|
92
|
+
// full history (team-runner recovery, run-export, inspect, read, and the
|
|
93
|
+
// event-reconstructor, whose only read entry is this function) now see
|
|
94
|
+
// archived events too — this is the intended fix for "no event stranded".
|
|
95
|
+
const archives = listEventArchivePaths(eventsPath);
|
|
96
|
+
const events: TeamEvent[] = [];
|
|
97
|
+
const seenSeqs = new Set<number>();
|
|
98
|
+
const push = (event: TeamEvent): void => {
|
|
99
|
+
const seq = event.metadata?.seq;
|
|
100
|
+
if (typeof seq === "number" && seq > 0) {
|
|
101
|
+
if (seenSeqs.has(seq)) return;
|
|
102
|
+
seenSeqs.add(seq);
|
|
103
|
+
}
|
|
104
|
+
events.push(event);
|
|
105
|
+
};
|
|
106
|
+
for (const archivePath of archives) {
|
|
107
|
+
for (const event of parseJsonlEvents(archivePath)) push(event);
|
|
108
|
+
}
|
|
109
|
+
if (fs.existsSync(eventsPath)) {
|
|
110
|
+
for (const event of parseJsonlEvents(eventsPath)) push(event);
|
|
111
|
+
}
|
|
112
|
+
return events.sort((a, b) => (a.metadata?.seq ?? 0) - (b.metadata?.seq ?? 0));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface EventCursorOptions {
|
|
116
|
+
sinceSeq?: number;
|
|
117
|
+
limit?: number;
|
|
118
|
+
fromByteOffset?: number;
|
|
119
|
+
/** R-03: generation the caller captured on its previous read. When set, a
|
|
120
|
+
* mismatch with the live generation signals the file was rotated/truncated
|
|
121
|
+
* and the byte offset is stale — the cursor resets to 0 so the new file is
|
|
122
|
+
* re-read from its start instead of missing post-rotation events. */
|
|
123
|
+
generation?: number;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface EventCursorResult {
|
|
127
|
+
events: TeamEvent[];
|
|
128
|
+
nextSeq: number;
|
|
129
|
+
total: number;
|
|
130
|
+
nextByteOffset?: number;
|
|
131
|
+
/** R-03: live generation of the events file at read time. Callers doing
|
|
132
|
+
* streaming byte-offset reads should echo this back as `generation` on the
|
|
133
|
+
* next call so rotation is detected and the cursor resets. */
|
|
134
|
+
generation?: number;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function positiveInteger(value: number | undefined): number | undefined {
|
|
138
|
+
return value !== undefined && Number.isInteger(value) && value >= 0 ? value : undefined;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function readEventsCursor(eventsPath: string, options: EventCursorOptions = {}): EventCursorResult {
|
|
142
|
+
// Incremental byte-offset path: read only new bytes since last known offset
|
|
143
|
+
if (options.fromByteOffset !== undefined) {
|
|
144
|
+
// R-03: detect file rotation/truncation via the generation sidecar BEFORE
|
|
145
|
+
// reusing the byte offset. If the file was rotated since the caller last
|
|
146
|
+
// read, it was truncated to empty (pre-rotation content archived to
|
|
147
|
+
// `<eventsPath>.<ts>.archive.jsonl`) and is growing again from 0 — the
|
|
148
|
+
// caller's offset now points past EOF, so post-rotation events would be
|
|
149
|
+
// silently missed. Reset to offset 0 to re-read the current file from its
|
|
150
|
+
// start.
|
|
151
|
+
// R18 (Phase 3.6): re-reading from 0 re-delivers no previously-returned
|
|
152
|
+
// events FROM THE LIVE FILE — but events STRANDED into the archive by the
|
|
153
|
+
// rotation (in-flight fd appends on the renamed inode) were never
|
|
154
|
+
// delivered and, before this fix, were lost forever once sweepOldArchives
|
|
155
|
+
// unlinked them. On a detected generation bump we therefore FIRST drain
|
|
156
|
+
// the previous generations' archive TAILS (events with seq > sinceSeq,
|
|
157
|
+
// deduped by seq — mailbox safeReadMailboxFile archive-walk pattern)
|
|
158
|
+
// ahead of the fresh live file's events.
|
|
159
|
+
const liveGen = currentGeneration(eventsPath);
|
|
160
|
+
const staleCursor = options.generation !== undefined && options.generation !== liveGen;
|
|
161
|
+
const sinceSeq = positiveInteger(options.sinceSeq) ?? 0;
|
|
162
|
+
const archiveEvents = staleCursor ? readArchiveTailEvents(eventsPath, sinceSeq) : [];
|
|
163
|
+
const byteOffset = staleCursor ? 0 : (positiveInteger(options.fromByteOffset) ?? 0);
|
|
164
|
+
const initialState: IncrementalReadState = { byteOffset, lineCount: 0 };
|
|
165
|
+
const { items, state: newState, eof } = readJsonlSince<TeamEvent>(eventsPath, initialState);
|
|
166
|
+
const filtered = items.filter((event) => (event.metadata?.seq ?? 0) > sinceSeq);
|
|
167
|
+
const merged = mergeArchiveTailEvents(archiveEvents, filtered);
|
|
168
|
+
const limit = positiveInteger(options.limit);
|
|
169
|
+
const events = limit !== undefined ? merged.slice(0, limit) : merged;
|
|
170
|
+
const returnedMaxSeq = events.reduce((max, event) => Math.max(max, event.metadata?.seq ?? 0), sinceSeq);
|
|
171
|
+
return {
|
|
172
|
+
events,
|
|
173
|
+
nextSeq: returnedMaxSeq,
|
|
174
|
+
total: merged.length,
|
|
175
|
+
nextByteOffset: newState.byteOffset,
|
|
176
|
+
generation: liveGen,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// FIND-05 default path: byte-level tail read (last 4MB) instead of
|
|
181
|
+
// full-file read. Bounds CPU to O(tail bytes) instead of O(total
|
|
182
|
+
// events). The legacy readEvents() full parse path is preserved for
|
|
183
|
+
// callers that explicitly need the full history (e.g. tests that
|
|
184
|
+
// assert exact contents) and as a small-file fallback.
|
|
185
|
+
//
|
|
186
|
+
// The 5000-event tail cap and the "event-log.cursor-full-read"
|
|
187
|
+
// warning are preserved. A separate cursor-tail-truncated warning is
|
|
188
|
+
// emitted whenever the file exceeds the 4MB tail budget, signalling
|
|
189
|
+
// that a prefix was dropped and callers should pass fromByteOffset for
|
|
190
|
+
// streaming reads.
|
|
191
|
+
const TAIL_BYTES = 4 * 1024 * 1024; // 4 MB
|
|
192
|
+
const TAIL_EVENT_CAP = 5000;
|
|
193
|
+
const sinceSeq = positiveInteger(options.sinceSeq) ?? 0;
|
|
194
|
+
const limit = positiveInteger(options.limit);
|
|
195
|
+
|
|
196
|
+
const tail = readJsonlTail<TeamEvent>(eventsPath, TAIL_BYTES);
|
|
197
|
+
let all = tail.items;
|
|
198
|
+
if (tail.truncated) {
|
|
199
|
+
logInternalError("event-log.cursor-tail-truncated", {
|
|
200
|
+
eventsPath,
|
|
201
|
+
returned: all.length,
|
|
202
|
+
tailBytes: TAIL_BYTES,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
if (all.length > TAIL_EVENT_CAP) {
|
|
206
|
+
logInternalError(
|
|
207
|
+
"event-log.cursor-full-read",
|
|
208
|
+
new Error(`readEventsCursor tail read dropped events from a larger log; pass fromByteOffset for incremental reads`),
|
|
209
|
+
`eventsPath=${eventsPath}`,
|
|
210
|
+
);
|
|
211
|
+
all = all.slice(-TAIL_EVENT_CAP);
|
|
212
|
+
}
|
|
213
|
+
const filtered = all.filter((event) => (event.metadata?.seq ?? 0) > sinceSeq);
|
|
214
|
+
// R18 (Phase 3.6): rotation stranding — prepend archive-tail events (seq >
|
|
215
|
+
// sinceSeq, deduped, seq-sorted) ahead of the live tail slice, so events
|
|
216
|
+
// stranded into an archive by a rotation are still delivered to sinceSeq
|
|
217
|
+
// streaming consumers. No-rotation case: no archives exist → behavior is
|
|
218
|
+
// byte-identical to before (mergeArchiveTailEvents returns liveEvents).
|
|
219
|
+
const merged = mergeArchiveTailEvents(readArchiveTailEvents(eventsPath, sinceSeq), filtered);
|
|
220
|
+
const events = limit !== undefined ? merged.slice(0, limit) : merged;
|
|
221
|
+
const returnedMaxSeq = events.reduce((max, event) => Math.max(max, event.metadata?.seq ?? 0), sinceSeq);
|
|
222
|
+
return { events, nextSeq: returnedMaxSeq, total: merged.length };
|
|
223
|
+
}
|
|
@@ -340,8 +340,13 @@ function bumpGenerationUnlocked(eventsPath: string): number {
|
|
|
340
340
|
* Rotate an event log file by archiving it with a timestamp.
|
|
341
341
|
* The current file is renamed to `<eventsPath>.<timestamp>.archive.jsonl`
|
|
342
342
|
* and a fresh empty file is created in its place.
|
|
343
|
-
* Readers using `readEvents`
|
|
344
|
-
*
|
|
343
|
+
* Readers using `readEvents`/`readEventsCursor` see the new file; archive
|
|
344
|
+
* files are read back by the archive-tail readers in event-log.ts
|
|
345
|
+
* (R18 / R16-B1 effect 2, Phase 3.6): on a detected `.gen` generation bump
|
|
346
|
+
* the cursor drains the previous generations' archive TAILS (events with
|
|
347
|
+
* seq > sinceSeq, deduped by seq) ahead of the fresh live file, so events
|
|
348
|
+
* stranded into an archive by an in-flight fd append during the rename are
|
|
349
|
+
* still delivered instead of being swept away after the retention window.
|
|
345
350
|
*/
|
|
346
351
|
export function rotateEventLog(eventsPath: string): boolean {
|
|
347
352
|
if (!fs.existsSync(eventsPath)) return false;
|
|
@@ -423,7 +428,7 @@ export function rotateEventLogUnlocked(eventsPath: string): boolean {
|
|
|
423
428
|
// our rename and here, we leave their data intact (EEXIST → skip).
|
|
424
429
|
fs.renameSync(eventsPath, archivePath);
|
|
425
430
|
try {
|
|
426
|
-
const fd = fs.openSync(eventsPath, "wx",
|
|
431
|
+
const fd = fs.openSync(eventsPath, "wx", 0o600);
|
|
427
432
|
fs.closeSync(fd);
|
|
428
433
|
} catch (err) {
|
|
429
434
|
if ((err as NodeJS.ErrnoException).code !== "EEXIST") throw err;
|
|
@@ -438,7 +443,10 @@ export function rotateEventLogUnlocked(eventsPath: string): boolean {
|
|
|
438
443
|
sweepOldArchives(eventsPath);
|
|
439
444
|
return true;
|
|
440
445
|
} catch (error) {
|
|
441
|
-
|
|
446
|
+
// R17-S1 (Phase 3.8): severity "error" (was default "debug", PI_TEAMS_DEBUG-
|
|
447
|
+
// gated) — a failed rotation leaves the file over the size limit and the
|
|
448
|
+
// next non-terminal appends are silently skipped; the chain must signal.
|
|
449
|
+
logInternalError("event-log.rotate", error, `eventsPath=${eventsPath}`, "error");
|
|
442
450
|
return false;
|
|
443
451
|
}
|
|
444
452
|
}
|