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
|
@@ -25,11 +25,26 @@ export interface SubagentSpawnOptions {
|
|
|
25
25
|
/** Optional batch grouping id (Rule 1). Agents sharing a batchId coalesce
|
|
26
26
|
* completion notifications into one. undefined => individual (default). */
|
|
27
27
|
batchId?: string;
|
|
28
|
+
/** WP-1/R1 (H6): owning team-run task id. Set by the one-shot Agent-tool
|
|
29
|
+
* route once the run manifest resolves (taskId is only knowable after the
|
|
30
|
+
* run is dispatched). undefined for unlinked/legacy spawns. */
|
|
31
|
+
taskId?: string;
|
|
32
|
+
/** WP-1/R1 (H6): ownership depth in the task tree (0 for a root one-shot).
|
|
33
|
+
* Optional for back-compat; defaults to 0 at spawn. WP-5 owns depth
|
|
34
|
+
* semantics for nested runs. */
|
|
35
|
+
depth?: number;
|
|
28
36
|
}
|
|
29
37
|
|
|
30
38
|
export interface SubagentRecord {
|
|
31
39
|
id: string;
|
|
32
40
|
runId?: string;
|
|
41
|
+
/** WP-1/R1 (H6): owning team-run task id (task ⇄ subagentId link).
|
|
42
|
+
* Optional for back-compat — records without it render as today and steer
|
|
43
|
+
* returns the existing "not linked" message (no throw). */
|
|
44
|
+
taskId?: string;
|
|
45
|
+
/** WP-1/R1 (H6): ownership depth (0 for root one-shot). Optional for
|
|
46
|
+
* back-compat — legacy records without it render as today. */
|
|
47
|
+
depth?: number;
|
|
33
48
|
type: string;
|
|
34
49
|
description: string;
|
|
35
50
|
prompt: string;
|
|
@@ -144,6 +159,7 @@ const ALLOWED_RECORD_FIELDS = new Set([
|
|
|
144
159
|
"runId",
|
|
145
160
|
"cwd",
|
|
146
161
|
"taskId",
|
|
162
|
+
"depth",
|
|
147
163
|
"result",
|
|
148
164
|
"error",
|
|
149
165
|
"resultConsumed",
|
|
@@ -185,7 +201,13 @@ export function readPersistedSubagentRecord(cwd: string, id: string): SubagentRe
|
|
|
185
201
|
try {
|
|
186
202
|
const raw = JSON.parse(fs.readFileSync(persistedSubagentPath(cwd, id), "utf-8"));
|
|
187
203
|
return sanitizePersistedRecord(raw);
|
|
188
|
-
} catch {
|
|
204
|
+
} catch (error) {
|
|
205
|
+
// R17-B5: parse/read failures were fully silent, hiding lost records
|
|
206
|
+
// (see NEW-R3 note in savePersistedSubagentRecord). ENOENT stays quiet —
|
|
207
|
+
// a legitimately removed record (cancel) is not an error.
|
|
208
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
209
|
+
logInternalError("subagent-manager.read-persisted", error, `id=${id}`, "warn");
|
|
210
|
+
}
|
|
189
211
|
return undefined;
|
|
190
212
|
}
|
|
191
213
|
}
|
|
@@ -254,6 +276,8 @@ export class SubagentManager {
|
|
|
254
276
|
ownerSessionGeneration: options.ownerSessionGeneration,
|
|
255
277
|
ownerSessionId: options.ownerSessionId,
|
|
256
278
|
batchId: options.batchId,
|
|
279
|
+
taskId: options.taskId,
|
|
280
|
+
depth: options.depth ?? 0,
|
|
257
281
|
};
|
|
258
282
|
this.records.set(record.id, record);
|
|
259
283
|
this.cwdByRecord.set(record.id, options.cwd);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { DEFAULT_OUTPUT_CONTEXT } from "../config/defaults.ts";
|
|
4
|
+
import { getCrewEnv } from "../config/env-vars.ts";
|
|
4
5
|
import { atomicWriteFile } from "../state/atomic-write.ts";
|
|
5
6
|
import { writeArtifact } from "../state/stores/artifact-store.ts";
|
|
6
7
|
import type { ArtifactDescriptor, TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
@@ -124,6 +125,13 @@ export interface ReadIfSmallTeeResult {
|
|
|
124
125
|
content: string;
|
|
125
126
|
/** Set only when tee was actually written (file size > 2× threshold + write succeeded). */
|
|
126
127
|
fullOutputPath?: string;
|
|
128
|
+
/** R10-1 (dep-context cache tee-safety): character length of the RAW file
|
|
129
|
+
* content BEFORE the truncation pipeline ran. Populated on every
|
|
130
|
+
* successful read; lets callers tell whether a cached no-tee read
|
|
131
|
+
* (`readIfSmall`) would have teed
|
|
132
|
+
* (length > TEE_THRESHOLD_MULTIPLIER × MAX_RESULT_INLINE_BYTES) without
|
|
133
|
+
* re-reading the file — no heuristics on the truncated body. */
|
|
134
|
+
originalLength?: number;
|
|
127
135
|
}
|
|
128
136
|
|
|
129
137
|
/**
|
|
@@ -211,9 +219,11 @@ export function readIfSmallWithTee(
|
|
|
211
219
|
},
|
|
212
220
|
}),
|
|
213
221
|
]);
|
|
214
|
-
return fullOutputPath
|
|
222
|
+
return fullOutputPath
|
|
223
|
+
? { content: result.text, fullOutputPath, originalLength: content.length }
|
|
224
|
+
: { content: result.text, originalLength: content.length };
|
|
215
225
|
}
|
|
216
|
-
return { content };
|
|
226
|
+
return { content, originalLength: content.length };
|
|
217
227
|
} catch {
|
|
218
228
|
return undefined;
|
|
219
229
|
}
|
|
@@ -401,6 +411,12 @@ export function collectDependencyOutputContext(
|
|
|
401
411
|
tasks: TeamTaskState[],
|
|
402
412
|
task: TeamTaskState,
|
|
403
413
|
step: WorkflowStep,
|
|
414
|
+
/** R10-1 residual: per-run result-artifact read cache (same instance the
|
|
415
|
+
* closeout aggregation uses — created once in executeTeamRunCore, threaded
|
|
416
|
+
* SchedulerContext → baseInput → TaskRunnerInput). Optional: undefined
|
|
417
|
+
* keeps the previous uncached per-dep readIfSmallWithTee behavior
|
|
418
|
+
* byte-for-byte. NEVER pass a per-call cache instance. */
|
|
419
|
+
cache?: ResultArtifactReadCache,
|
|
404
420
|
): DependencyOutputContext {
|
|
405
421
|
const byStep = new Map(tasks.map((item) => [item.stepId, item]).filter((entry): entry is [string, TeamTaskState] => Boolean(entry[0])));
|
|
406
422
|
const byId = new Map(tasks.map((item) => [item.id, item]));
|
|
@@ -424,11 +440,19 @@ export function collectDependencyOutputContext(
|
|
|
424
440
|
.filter((item): item is TeamTaskState => Boolean(item))
|
|
425
441
|
.map((item) => {
|
|
426
442
|
const fullOutputPath = item.resultArtifact ? teePathForArtifact(manifest.artifactsRoot, task.id, item.id) : undefined;
|
|
443
|
+
// R10-1 residual: route the dep result read through the same per-run
|
|
444
|
+
// cache the closeout aggregation populated (readTaskResultArtifactWithTee).
|
|
445
|
+
// Without a cache this issues the identical readIfSmallWithTee call as
|
|
446
|
+
// before (byte-identical output); with one, a non-tee-band hit returns
|
|
447
|
+
// the memoized body while tee-band truncations still do the real read
|
|
448
|
+
// (tee file + fullOutputPath preserved per consumer).
|
|
427
449
|
const teeResult = item.resultArtifact
|
|
428
|
-
?
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
450
|
+
? readTaskResultArtifactWithTee(
|
|
451
|
+
item.resultArtifact,
|
|
452
|
+
manifest.artifactsRoot,
|
|
453
|
+
fullOutputPath ? { fullOutputPath } : undefined,
|
|
454
|
+
cache,
|
|
455
|
+
)
|
|
432
456
|
: undefined;
|
|
433
457
|
const resultText = teeResult?.content;
|
|
434
458
|
const inlineBytes = resultText?.length ?? 0;
|
|
@@ -570,12 +594,209 @@ export function writeTaskInputsArtifact(
|
|
|
570
594
|
});
|
|
571
595
|
}
|
|
572
596
|
|
|
573
|
-
|
|
597
|
+
/**
|
|
598
|
+
* R10-1: outcome of one aggregated result-artifact read (the pair of disk
|
|
599
|
+
* ops {@link aggregateTaskOutputs} performs per task: readIfSmall +
|
|
600
|
+
* containedExists). Cached as a unit so a cache hit replaces BOTH ops.
|
|
601
|
+
*/
|
|
602
|
+
export interface ResultArtifactReadOutcome {
|
|
603
|
+
/** Truncated body from readIfSmall; undefined when the read failed/missing. */
|
|
604
|
+
body: string | undefined;
|
|
605
|
+
/** containedExists() result for the same artifact path. */
|
|
606
|
+
exists: boolean;
|
|
607
|
+
/** R10-1 (dep-context tee-safety): character length of the raw file content
|
|
608
|
+
* at miss time. Present whenever the read succeeded; used by the
|
|
609
|
+
* dependency-context seam to decide whether a cache hit is byte-identical
|
|
610
|
+
* (no tee would have been written) or must fall through to a real
|
|
611
|
+
* readIfSmallWithTee. Undefined when the read failed. */
|
|
612
|
+
originalLength?: number;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* R10-1: per-run memoization for result-artifact reads inside
|
|
617
|
+
* {@link aggregateTaskOutputs} (batch summary + group-join both aggregate the
|
|
618
|
+
* SAME settled batch every closeout — the second aggregation re-reads each
|
|
619
|
+
* `results/<taskId>.txt` from disk for no benefit).
|
|
620
|
+
*
|
|
621
|
+
* Cache key = artifact path + descriptor identity (`sizeBytes|contentHash`).
|
|
622
|
+
* writeArtifact computes both on every write (STATE-9: in-memory,
|
|
623
|
+
* post-redaction), so a retry that rewrites `results/<taskId>.txt` produces a
|
|
624
|
+
* NEW descriptor → new key → automatic miss. Descriptors missing BOTH fields
|
|
625
|
+
* bypass the cache entirely (cannot distinguish a rewrite from unchanged
|
|
626
|
+
* content). "Missing" outcomes are cached too (result artifacts use the
|
|
627
|
+
* default `retention: "run"` and are not pruned mid-run, so a miss is stable
|
|
628
|
+
* within a run). Path is used as-is (producer/taskId-keyed, stable absolute
|
|
629
|
+
* path from the manifest — no per-lookup realpath needed).
|
|
630
|
+
*
|
|
631
|
+
* Lifetime: one run (closure-level in executeTeamRunCore) — no cross-run
|
|
632
|
+
* leakage, no invalidation beyond the descriptor-identity key.
|
|
633
|
+
*
|
|
634
|
+
* Env bypass: `PI_CREW_DISABLE_RESULT_READ_CACHE=1` (read ONCE at creation)
|
|
635
|
+
* returns a cache whose lookups always miss — control mode for benches/tests
|
|
636
|
+
* that need the uncached behavior while still routing through the counting
|
|
637
|
+
* wrapper below.
|
|
638
|
+
*/
|
|
639
|
+
export interface ResultArtifactReadCache {
|
|
640
|
+
/** Cached outcome for the artifact identity, or undefined on miss/bypass. */
|
|
641
|
+
lookup(descriptor: ArtifactDescriptor): ResultArtifactReadOutcome | undefined;
|
|
642
|
+
/** Store an outcome. No-op when the descriptor lacks identity fields. */
|
|
643
|
+
store(descriptor: ArtifactDescriptor, outcome: ResultArtifactReadOutcome): void;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* R10-1 test/bench counters for the result-artifact read path. Track disk ops
|
|
648
|
+
* ACTUALLY ISSUED through {@link readTaskResultArtifact} (incremented in the
|
|
649
|
+
* miss branch right before the real readIfSmall/containedExists calls) plus
|
|
650
|
+
* cache hits/misses. The disabled-control mode still routes through the
|
|
651
|
+
* counting wrapper (map simply not consulted) so cached-vs-uncached
|
|
652
|
+
* comparisons are honest.
|
|
653
|
+
*/
|
|
654
|
+
export interface ResultArtifactReadStats {
|
|
655
|
+
readFile: number;
|
|
656
|
+
exists: number;
|
|
657
|
+
hits: number;
|
|
658
|
+
misses: number;
|
|
659
|
+
reset(): void;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
export const __test__resultReadStats: ResultArtifactReadStats = {
|
|
663
|
+
readFile: 0,
|
|
664
|
+
exists: 0,
|
|
665
|
+
hits: 0,
|
|
666
|
+
misses: 0,
|
|
667
|
+
reset() {
|
|
668
|
+
this.readFile = 0;
|
|
669
|
+
this.exists = 0;
|
|
670
|
+
this.hits = 0;
|
|
671
|
+
this.misses = 0;
|
|
672
|
+
},
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
function resultArtifactCacheKey(descriptor: ArtifactDescriptor): string | undefined {
|
|
676
|
+
if (descriptor.sizeBytes === undefined && descriptor.contentHash === undefined) return undefined;
|
|
677
|
+
return `${descriptor.path}|${descriptor.sizeBytes ?? ""}|${descriptor.contentHash ?? ""}`;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
export function createResultArtifactReadCache(): ResultArtifactReadCache {
|
|
681
|
+
// Read once at creation — the bypass decision is fixed for the cache's
|
|
682
|
+
// (per-run) lifetime, matching PI_CREW_USE_BUNDLE-style one-shot env reads.
|
|
683
|
+
if (getCrewEnv("PI_CREW_DISABLE_RESULT_READ_CACHE") === "1") {
|
|
684
|
+
return {
|
|
685
|
+
lookup: () => undefined,
|
|
686
|
+
store: () => undefined,
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
const entries = new Map<string, ResultArtifactReadOutcome>();
|
|
690
|
+
return {
|
|
691
|
+
lookup(descriptor) {
|
|
692
|
+
const key = resultArtifactCacheKey(descriptor);
|
|
693
|
+
if (key === undefined) return undefined;
|
|
694
|
+
const hit = entries.get(key);
|
|
695
|
+
if (hit === undefined) {
|
|
696
|
+
__test__resultReadStats.misses += 1;
|
|
697
|
+
return undefined;
|
|
698
|
+
}
|
|
699
|
+
__test__resultReadStats.hits += 1;
|
|
700
|
+
return hit;
|
|
701
|
+
},
|
|
702
|
+
store(descriptor, outcome) {
|
|
703
|
+
const key = resultArtifactCacheKey(descriptor);
|
|
704
|
+
if (key !== undefined) entries.set(key, outcome);
|
|
705
|
+
},
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Single read seam for {@link aggregateTaskOutputs}. Without a cache this is
|
|
711
|
+
* byte-for-byte the old behavior: readIfSmall + containedExists issued in the
|
|
712
|
+
* same order. With a cache, a hit returns the memoized pair (zero disk ops);
|
|
713
|
+
* a miss issues the same disk ops as the uncached path, counts them, and
|
|
714
|
+
* memoizes the outcome.
|
|
715
|
+
*/
|
|
716
|
+
function readTaskResultArtifact(
|
|
717
|
+
descriptor: ArtifactDescriptor,
|
|
718
|
+
baseDir: string | undefined,
|
|
719
|
+
cache: ResultArtifactReadCache | undefined,
|
|
720
|
+
): ResultArtifactReadOutcome {
|
|
721
|
+
const cached = cache?.lookup(descriptor);
|
|
722
|
+
if (cached !== undefined) return cached;
|
|
723
|
+
__test__resultReadStats.readFile += 1;
|
|
724
|
+
__test__resultReadStats.exists += 1;
|
|
725
|
+
// readIfSmall is a thin wrapper over readIfSmallWithTee(...).content — the
|
|
726
|
+
// direct call is byte-identical and also captures originalLength (R10-1
|
|
727
|
+
// tee-safety metadata consumed by readTaskResultArtifactWithTee).
|
|
728
|
+
const raw = readIfSmallWithTee(descriptor.path, { baseDir });
|
|
729
|
+
const outcome: ResultArtifactReadOutcome = {
|
|
730
|
+
body: raw?.content,
|
|
731
|
+
exists: containedExists(descriptor.path, baseDir),
|
|
732
|
+
...(raw?.originalLength !== undefined ? { originalLength: raw.originalLength } : {}),
|
|
733
|
+
};
|
|
734
|
+
cache?.store(descriptor, outcome);
|
|
735
|
+
return outcome;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* R10-1 residual: cached read seam for the dependency-context path
|
|
740
|
+
* ({@link collectDependencyOutputContext}). Mirrors {@link readTaskResultArtifact}
|
|
741
|
+
* while preserving the tee-recovery semantics of the direct
|
|
742
|
+
* `readIfSmallWithTee(path, { tee })` call it replaces:
|
|
743
|
+
*
|
|
744
|
+
* - A cache hit is reusable ONLY when the memoized read proves the uncached
|
|
745
|
+
* call would NOT have teed: `originalLength` is known AND
|
|
746
|
+
* `originalLength <= TEE_THRESHOLD_MULTIPLIER × MAX_RESULT_INLINE_BYTES`.
|
|
747
|
+
* In that band readIfSmallWithTee().content is byte-identical to the
|
|
748
|
+
* memoized body (same file, same truncation pipeline) and fullOutputPath is
|
|
749
|
+
* unset, so returning the memoized body cannot change the rendered prompt.
|
|
750
|
+
* - A memoized read that FAILED (body undefined) is also returned as-is: the
|
|
751
|
+
* direct readIfSmallWithTee would have thrown and returned undefined for
|
|
752
|
+
* the same descriptor identity.
|
|
753
|
+
* - Otherwise (miss, bypass, or tee-band truncation) the real
|
|
754
|
+
* readIfSmallWithTee runs — WITH tee, so the per-consumer
|
|
755
|
+
* `tee/<taskId>-<depId>.full.txt` write and `fullOutputPath` survive — and
|
|
756
|
+
* the disk ops are counted in `__test__resultReadStats` exactly like
|
|
757
|
+
* readTaskResultArtifact's miss branch (honest cached-vs-bypass benches).
|
|
758
|
+
* When a cache is provided the miss ALSO populates it with the SAME
|
|
759
|
+
* outcome readTaskResultArtifact would have stored (body = teeResult.content
|
|
760
|
+
* — byte-identical to readIfSmall's output since tee never alters content —
|
|
761
|
+
* exists via containedExists, originalLength), so the next dep collect /
|
|
762
|
+
* closeout aggregation hits. Truncation checks in later consumers use
|
|
763
|
+
* originalLength, never heuristics on the truncated body.
|
|
764
|
+
*/
|
|
765
|
+
function readTaskResultArtifactWithTee(
|
|
766
|
+
descriptor: ArtifactDescriptor,
|
|
767
|
+
baseDir: string | undefined,
|
|
768
|
+
tee: TeeRecoveryOptions | undefined,
|
|
769
|
+
cache: ResultArtifactReadCache | undefined,
|
|
770
|
+
): ReadIfSmallTeeResult | undefined {
|
|
771
|
+
const cached = cache?.lookup(descriptor);
|
|
772
|
+
if (cached !== undefined) {
|
|
773
|
+
if (cached.body === undefined) return undefined;
|
|
774
|
+
if (cached.originalLength !== undefined && cached.originalLength <= MAX_RESULT_INLINE_BYTES * TEE_THRESHOLD_MULTIPLIER) {
|
|
775
|
+
return { content: cached.body };
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
__test__resultReadStats.readFile += 1;
|
|
779
|
+
const result = readIfSmallWithTee(descriptor.path, { baseDir, ...(tee ? { tee } : {}) });
|
|
780
|
+
if (cache) {
|
|
781
|
+
// Populate with the outcome the closeout seam would have stored. The op
|
|
782
|
+
// order (read → existsSync) matches readTaskResultArtifact's miss branch.
|
|
783
|
+
__test__resultReadStats.exists += 1;
|
|
784
|
+
cache.store(descriptor, {
|
|
785
|
+
body: result?.content,
|
|
786
|
+
exists: containedExists(descriptor.path, baseDir),
|
|
787
|
+
...(result?.originalLength !== undefined ? { originalLength: result.originalLength } : {}),
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
return result;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
export function aggregateTaskOutputs(tasks: TeamTaskState[], manifest?: TeamRunManifest, cache?: ResultArtifactReadCache): string {
|
|
574
794
|
return tasks
|
|
575
795
|
.map((task, index) => {
|
|
576
|
-
const
|
|
796
|
+
const read = task.resultArtifact ? readTaskResultArtifact(task.resultArtifact, manifest?.artifactsRoot, cache) : undefined;
|
|
797
|
+
const body = read?.body;
|
|
577
798
|
const hasBody = Boolean(body?.trim());
|
|
578
|
-
const expectedMissing =
|
|
799
|
+
const expectedMissing = read ? !read.exists : undefined;
|
|
579
800
|
const status =
|
|
580
801
|
task.status === "skipped"
|
|
581
802
|
? "SKIPPED"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
-
import
|
|
2
|
+
import { freezeSpecSnapshot, loadSpecRecord } from "../state/stores/spec-store.ts";
|
|
3
|
+
import type { SpecRecord, TaskPacket, TaskScope, TeamRunManifest, VerificationContract } from "../state/types.ts";
|
|
3
4
|
import type { WorkflowStep } from "../workflows/workflow-config.ts";
|
|
4
5
|
import { generateTaskHashId } from "./task-id.ts";
|
|
5
6
|
|
|
@@ -70,6 +71,10 @@ export function sanitizeTaskText(task: string): string {
|
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
export interface BuildTaskPacketInput {
|
|
74
|
+
/** T4/R6 (ADR-6 §2): workspace spec ids — loaded + FROZEN into the packet. */
|
|
75
|
+
specRefs?: string[];
|
|
76
|
+
/** T4/R6 (ADR-6 §7): strict mode — coverage AND machine-check (§4). */
|
|
77
|
+
specStrict?: boolean;
|
|
73
78
|
manifest: TeamRunManifest;
|
|
74
79
|
step: WorkflowStep;
|
|
75
80
|
taskId: string;
|
|
@@ -98,6 +103,20 @@ export function defaultVerificationContract(step: WorkflowStep): VerificationCon
|
|
|
98
103
|
}
|
|
99
104
|
|
|
100
105
|
export function buildTaskPacket(input: BuildTaskPacketInput): TaskPacket {
|
|
106
|
+
// T4/R6 (ADR-6 §1/§2): the ONLY packet-creation path is also the freeze
|
|
107
|
+
// point — specRefs load from the workspace store and freeze into immutable
|
|
108
|
+
// snapshots embedded in the packet (later spec edits never rewrite them).
|
|
109
|
+
// Round-1 review (fail-closed freeze): declared-but-unresolvable ids are
|
|
110
|
+
// kept on the packet as unresolvedSpecRefs instead of being dropped — the
|
|
111
|
+
// gate surfaces them (badge non-strict / failure strict), never a silent
|
|
112
|
+
// no-op. Snapshots freeze trust via the store at THIS moment.
|
|
113
|
+
const declaredSpecRefs = input.specRefs ?? [];
|
|
114
|
+
const resolvedRecords = declaredSpecRefs
|
|
115
|
+
.map((id) => loadSpecRecord(input.manifest.cwd, id))
|
|
116
|
+
.filter((r): r is SpecRecord => r !== undefined);
|
|
117
|
+
const specSnapshots = resolvedRecords.map((r) => freezeSpecSnapshot(r, input.manifest.cwd));
|
|
118
|
+
const unresolvedSpecRefs = declaredSpecRefs.filter((id) => !specSnapshots.some((s) => s.specId === id));
|
|
119
|
+
|
|
101
120
|
const scope = inferTaskScope(input.step);
|
|
102
121
|
const reads = input.step.reads === false ? [] : (input.step.reads ?? []);
|
|
103
122
|
const scopePath = reads.length === 1 ? reads[0] : reads.length > 1 ? reads.join(", ") : undefined;
|
|
@@ -112,6 +131,9 @@ export function buildTaskPacket(input: BuildTaskPacketInput): TaskPacket {
|
|
|
112
131
|
|
|
113
132
|
return {
|
|
114
133
|
objective: sanitizedTask.replaceAll("{goal}", sanitizedGoal),
|
|
134
|
+
...(declaredSpecRefs.length > 0 ? { specRefs: declaredSpecRefs, specSnapshots } : {}),
|
|
135
|
+
...(unresolvedSpecRefs.length > 0 ? { unresolvedSpecRefs } : {}),
|
|
136
|
+
...(input.specStrict === true ? { specStrict: true } : {}),
|
|
115
137
|
scope,
|
|
116
138
|
scopePath,
|
|
117
139
|
repo: path.basename(input.manifest.cwd) || input.manifest.cwd,
|
|
@@ -28,17 +28,21 @@
|
|
|
28
28
|
import * as fs from "node:fs";
|
|
29
29
|
import * as path from "node:path";
|
|
30
30
|
import { loadConfig } from "../../config/config.ts";
|
|
31
|
+
import { getCrewEnv } from "../../config/env-vars.ts";
|
|
31
32
|
import { errors } from "../../errors.ts";
|
|
32
33
|
import { appendEventAsync, appendEventBuffered } from "../../state/event-log/event-log.ts";
|
|
33
34
|
import { writeArtifact } from "../../state/stores/artifact-store.ts";
|
|
35
|
+
import { upsertOwnershipEntry } from "../../state/stores/ownership-map.ts";
|
|
34
36
|
import type { ArtifactDescriptor, OperationTerminalEvidence, TeamRunManifest } from "../../state/types.ts";
|
|
37
|
+
import { type FatalFsCause, failureCauseForAttempt } from "../../utils/fs-errno.ts";
|
|
35
38
|
import { logInternalError } from "../../utils/internal-error.ts";
|
|
36
39
|
import { resolveRealContainedPath } from "../../utils/safe-paths.ts";
|
|
37
40
|
import type { ChildPiLifecycleEvent, ChildPiRunResult } from "../child-pi/child-pi.ts";
|
|
38
41
|
import {
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
appendCrewAgentEventBuffered,
|
|
43
|
+
appendCrewAgentOutputBuffered,
|
|
41
44
|
emptyCrewAgentProgress,
|
|
45
|
+
flushCrewAgentRecordBuffer,
|
|
42
46
|
recordFromTask,
|
|
43
47
|
upsertCrewAgent,
|
|
44
48
|
} from "../crew-agent-records.ts";
|
|
@@ -380,6 +384,9 @@ export async function runChildProcessTask(ctx: TaskExecutionContext): Promise<Ta
|
|
|
380
384
|
}
|
|
381
385
|
const logs: string[] = [];
|
|
382
386
|
let finalStderr = "";
|
|
387
|
+
// bug-026 sub-issue B: fatal-fs cause (enospc/edquot/emfile/enfile)
|
|
388
|
+
// derived per attempt from the attempt error / stderr tail.
|
|
389
|
+
let failureCause: FatalFsCause | undefined;
|
|
383
390
|
// One-shot: the re-resolve is a safety net for a chain that was computed
|
|
384
391
|
// before the registry was complete, not a retry loop of its own.
|
|
385
392
|
let reResolveUsed = false;
|
|
@@ -445,6 +452,12 @@ export async function runChildProcessTask(ctx: TaskExecutionContext): Promise<Ta
|
|
|
445
452
|
lastRunProgressPersistedAt = now;
|
|
446
453
|
}
|
|
447
454
|
};
|
|
455
|
+
// F7 (B1 battery 2026-08-18): one dispatch timestamp for the WHOLE attempt
|
|
456
|
+
// loop. A steer racing anywhere between task-dispatch and any attempt's
|
|
457
|
+
// onSpawn (including across the model-fallback respawn gap — observed live:
|
|
458
|
+
// steer at 15:59:11 wiped by attempt-1's truncate at 15:59:59) must survive
|
|
459
|
+
// the per-incarnation truncate; residue older than the dispatch is wiped.
|
|
460
|
+
const taskDispatchStartedAtMs = Date.now();
|
|
448
461
|
for (let i = 0; i < attemptModels.length; i++) {
|
|
449
462
|
// M1 fix: set transcript path per attempt to avoid mixing across fallback attempts.
|
|
450
463
|
transcriptPath = `${manifest.artifactsRoot}/transcripts/${task.id}.attempt-${i}.jsonl`;
|
|
@@ -549,10 +562,65 @@ export async function runChildProcessTask(ctx: TaskExecutionContext): Promise<Ta
|
|
|
549
562
|
steeringFile: resolveRealContainedPath(`${manifest.artifactsRoot}/steering`, `${task.id}.jsonl`),
|
|
550
563
|
onSpawn: (pid) => {
|
|
551
564
|
try {
|
|
565
|
+
// WP-1/R1 (H6): dispatch-time ownership writer — record the
|
|
566
|
+
// task ⇄ pid ⇄ artifactsDir leg of the ownership map as soon as
|
|
567
|
+
// a real worker spawns. The one-shot Agent-tool path fills the
|
|
568
|
+
// subagentId leg on the same taskId; the store merges per field
|
|
569
|
+
// under the run lock. Best-effort: never throws into the spawn
|
|
570
|
+
// path. onSpawn fires per spawn attempt (model-fallback loop), so
|
|
571
|
+
// the pid is overwritten with the latest attempt's live pid —
|
|
572
|
+
// correct, that is the worker steer targets.
|
|
573
|
+
try {
|
|
574
|
+
upsertOwnershipEntry(manifest, {
|
|
575
|
+
taskId: task.id,
|
|
576
|
+
runId: manifest.runId,
|
|
577
|
+
pid,
|
|
578
|
+
artifactsDir: manifest.artifactsRoot,
|
|
579
|
+
updatedAt: new Date().toISOString(),
|
|
580
|
+
});
|
|
581
|
+
} catch (err) {
|
|
582
|
+
logInternalError("task-runner.ownership-write", err, `pid=${pid}, taskId=${task.id}`);
|
|
583
|
+
}
|
|
552
584
|
({ task, tasks } = checkpointTask(manifest, tasks, task, "child-spawned", pid));
|
|
585
|
+
// Security review (T1/WP-1 round 1, security-1 replay fix): scope the
|
|
586
|
+
// steering file to ONE worker incarnation on EVERY spawn. Steering
|
|
587
|
+
// lines are id-less; each new worker process reads the file from
|
|
588
|
+
// byte 0 (prompt-runtime poll `lastOffset = 0`), so un-truncated
|
|
589
|
+
// files re-deliver every accumulated line — including stale steers
|
|
590
|
+
// written after the run went terminal — into the next spawn
|
|
591
|
+
// attempt's first-turn context. Best-effort; never throws.
|
|
592
|
+
// ACCEPTED TRADE-OFF (security review round 2): a steer appended
|
|
593
|
+
// directly by steer_subagent to a worker that dies BEFORE its 500ms
|
|
594
|
+
// poll reads it is lost when the respawn truncates here. The reliable
|
|
595
|
+
// cross-attempt channel is task.pendingSteers (survives in the task
|
|
596
|
+
// record and is re-appended after truncation); the direct file append
|
|
597
|
+
// is best-effort live-delivery only.
|
|
598
|
+
{
|
|
599
|
+
const steeringDir = `${manifest.artifactsRoot}/steering`;
|
|
600
|
+
try {
|
|
601
|
+
const safeSteeringPath = resolveRealContainedPath(steeringDir, `${task.id}.jsonl`);
|
|
602
|
+
// F7 fix: skip the truncate when the file's last write happened at or
|
|
603
|
+
// after THIS task's dispatch began — that is a live steer racing the
|
|
604
|
+
// spawn (observed live: "Steer delivered" + 0-byte file). Residue
|
|
605
|
+
// predating the dispatch (stale terminal-era steers) is still wiped.
|
|
606
|
+
// stat→truncate gap is microseconds; a steer landing inside it is
|
|
607
|
+
// lost — best-effort, same trade-off as before.
|
|
608
|
+
let racedSteerArrived = false;
|
|
609
|
+
try {
|
|
610
|
+
racedSteerArrived = fs.statSync(safeSteeringPath).mtimeMs >= taskDispatchStartedAtMs - 1;
|
|
611
|
+
} catch {
|
|
612
|
+
/* no file yet — nothing to truncate */
|
|
613
|
+
}
|
|
614
|
+
if (!racedSteerArrived) fs.writeFileSync(safeSteeringPath, "");
|
|
615
|
+
} catch (truncateErr) {
|
|
616
|
+
logInternalError("task-runner.steering-truncate", truncateErr, `taskId=${task.id}`);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
553
619
|
if (task.pendingSteers?.length) {
|
|
554
620
|
const steeringDir = `${manifest.artifactsRoot}/steering`;
|
|
555
|
-
// Fire-and-forget async write for steering events
|
|
621
|
+
// Fire-and-forget async write for steering events (the file was
|
|
622
|
+
// truncated just above; this incarnation receives only its own
|
|
623
|
+
// pending steers).
|
|
556
624
|
void appendSteeringAsync(steeringDir, task.id, task.pendingSteers);
|
|
557
625
|
// RT-8: spread before clearing pendingSteers instead of mutating
|
|
558
626
|
// in place — preserves the immutable-snapshot invariant (the same
|
|
@@ -574,7 +642,10 @@ export async function runChildProcessTask(ctx: TaskExecutionContext): Promise<Ta
|
|
|
574
642
|
}).catch((error) => logInternalError("task-runner.lifecycle-event", error, `taskId=${task.id}, type=${event.type}`));
|
|
575
643
|
},
|
|
576
644
|
onStdoutLine: (line) => {
|
|
577
|
-
|
|
645
|
+
// R10-5 (Wave 2B item 4): buffered — one appendFileSync per flush
|
|
646
|
+
// window instead of one per stdout line. Flushes at task boundary
|
|
647
|
+
// (finally below), process exit, 32-event cap, or 250ms window.
|
|
648
|
+
appendCrewAgentOutputBuffered(manifest, task.id, line);
|
|
578
649
|
persistHeartbeat();
|
|
579
650
|
},
|
|
580
651
|
onJsonEvent: (event) => {
|
|
@@ -585,7 +656,11 @@ export async function runChildProcessTask(ctx: TaskExecutionContext): Promise<Ta
|
|
|
585
656
|
// the full payload through); was dead on the old displayLine path.
|
|
586
657
|
const contact = supervisorContactFromEvent(event);
|
|
587
658
|
if (contact) recordSupervisorContact(manifest, { runId: manifest.runId, ...contact });
|
|
588
|
-
|
|
659
|
+
// R10-5 (Wave 2B item 4): buffered agent-record sink — collapses the
|
|
660
|
+
// per-event stat+append fanout into one appendFileSync per flush
|
|
661
|
+
// window. Seqs are reserved at buffer time (no collisions); the
|
|
662
|
+
// run-level events.jsonl, steering, and result.md stay unbuffered.
|
|
663
|
+
appendCrewAgentEventBuffered(manifest, task.id, event);
|
|
589
664
|
if (collectedJsonEvents && event && typeof event === "object" && !Array.isArray(event))
|
|
590
665
|
collectedJsonEvents.push(event as Record<string, unknown>);
|
|
591
666
|
if (collectedJsonEvents && collectedJsonEvents.length > 1000) {
|
|
@@ -613,7 +688,7 @@ export async function runChildProcessTask(ctx: TaskExecutionContext): Promise<Ta
|
|
|
613
688
|
persistHeartbeat();
|
|
614
689
|
// Bug #3 fix: Write worker JSON events to background.log for debugging when running in background mode.
|
|
615
690
|
// This supplements the event log so developers can see what the child Pi worker produced.
|
|
616
|
-
if (
|
|
691
|
+
if (getCrewEnv("PI_CREW_BACKGROUND_MODE") === "1" && event) {
|
|
617
692
|
const bgLogPath = `${manifest.stateRoot}/background.log`;
|
|
618
693
|
const eventLine = typeof event === "object" && !Array.isArray(event) ? JSON.stringify(event) : String(event);
|
|
619
694
|
// Fire-and-forget async write for background log
|
|
@@ -650,6 +725,12 @@ export async function runChildProcessTask(ctx: TaskExecutionContext): Promise<Ta
|
|
|
650
725
|
},
|
|
651
726
|
});
|
|
652
727
|
} finally {
|
|
728
|
+
// R10-5 (Wave 2B item 4): task-boundary flush — land every buffered
|
|
729
|
+
// agent-record line from this attempt's callbacks before terminal
|
|
730
|
+
// state is derived/read. Runs on completion, failure, AND throw, so
|
|
731
|
+
// the persisted events/output after a task always match what the
|
|
732
|
+
// unbuffered implementation would have written.
|
|
733
|
+
flushCrewAgentRecordBuffer(manifest, task.id);
|
|
653
734
|
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
654
735
|
// W2 fix — release the listener so it doesn't leak. {once:true}
|
|
655
736
|
// only auto-removes when the listener FIRES; if the timeout
|
|
@@ -712,6 +793,13 @@ export async function runChildProcessTask(ctx: TaskExecutionContext): Promise<Ta
|
|
|
712
793
|
rawFinalText = childResult.rawFinalText;
|
|
713
794
|
intermediateFindings = childResult.intermediateFindings;
|
|
714
795
|
error = attemptErrorFor(childResult, parsedOutput, task.id);
|
|
796
|
+
// bug-026 sub-issue B: classify fatal fs errnos (ENOSPC/EDQUOT/EMFILE/
|
|
797
|
+
// ENFILE). During the 2026-08-15 disk-full window the errno only surfaced
|
|
798
|
+
// inside E007 stderr-tail strings, never as a raw errno object in the
|
|
799
|
+
// parent — so match the message text too. Recomputed every attempt;
|
|
800
|
+
// stamped on the task record immediately so a crash before loop exit
|
|
801
|
+
// still persists the cause. Only meaningful on failure.
|
|
802
|
+
failureCause = error ? failureCauseForAttempt(error, finalStderr) : undefined;
|
|
715
803
|
persistHeartbeat(true);
|
|
716
804
|
persistChildProgress({ type: "attempt_finished" }, true);
|
|
717
805
|
const attempt: ModelAttemptSummary = {
|
|
@@ -721,7 +809,11 @@ export async function runChildProcessTask(ctx: TaskExecutionContext): Promise<Ta
|
|
|
721
809
|
error,
|
|
722
810
|
};
|
|
723
811
|
modelAttempts.push(attempt);
|
|
724
|
-
task = {
|
|
812
|
+
task = {
|
|
813
|
+
...task,
|
|
814
|
+
modelAttempts: [...modelAttempts],
|
|
815
|
+
...(failureCause ? { failureCause } : {}),
|
|
816
|
+
};
|
|
725
817
|
tasks = updateTask(tasks, task);
|
|
726
818
|
logs.push(
|
|
727
819
|
`MODEL ATTEMPT ${i + 1}: ${attempt.model}`,
|
|
@@ -791,6 +883,11 @@ export async function runChildProcessTask(ctx: TaskExecutionContext): Promise<Ta
|
|
|
791
883
|
error,
|
|
792
884
|
).message;
|
|
793
885
|
}
|
|
886
|
+
// bug-026 sub-issue B: reclassify after the E008 modelExhausted rewrite —
|
|
887
|
+
// its message embeds the last attempt's error (incl. stderr tails), so the
|
|
888
|
+
// errno survives the rewrite. Cleared on success (a task that recovered on
|
|
889
|
+
// retry is not an fs failure).
|
|
890
|
+
failureCause = error ? failureCauseForAttempt(error, finalStderr) : undefined;
|
|
794
891
|
// NEW-8 fix: register all attempt transcripts as artifacts, not just the used one.
|
|
795
892
|
// Earlier failed attempts' transcripts exist on disk but were invisible to the artifact system.
|
|
796
893
|
const successfulAttemptIndex = modelAttempts.findIndex((attempt) => attempt.success);
|
|
@@ -850,6 +947,7 @@ export async function runChildProcessTask(ctx: TaskExecutionContext): Promise<Ta
|
|
|
850
947
|
const fallbackReason = usedAttempt > 0 ? modelAttempts[usedAttempt - 1]?.error : undefined;
|
|
851
948
|
task = {
|
|
852
949
|
...task,
|
|
950
|
+
...(failureCause ? { failureCause } : {}),
|
|
853
951
|
modelRouting: {
|
|
854
952
|
requested: modelRoutingPlan.requested,
|
|
855
953
|
resolved: resolvedModel,
|
|
@@ -915,6 +1013,7 @@ export async function runChildProcessTask(ctx: TaskExecutionContext): Promise<Ta
|
|
|
915
1013
|
modelAttempts,
|
|
916
1014
|
parsedOutput,
|
|
917
1015
|
finalStdout,
|
|
1016
|
+
rawFinalText,
|
|
918
1017
|
transcriptPath,
|
|
919
1018
|
terminalEvidence,
|
|
920
1019
|
startupEvidence,
|