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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import * as fs from "node:fs";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import { atomicWriteJson, atomicWriteJsonCoalesced, flushPendingAtomicWrites, readJsonFile } from "../state/atomic-write.ts";
|
|
@@ -143,25 +144,79 @@ function removeStaleAgentsLock(lockPath: string, staleMs: number): boolean {
|
|
|
143
144
|
}
|
|
144
145
|
fs.rmSync(lockPath, { force: true });
|
|
145
146
|
return true;
|
|
146
|
-
} catch {
|
|
147
|
+
} catch (error) {
|
|
148
|
+
// R17-B1 (HIGH): a bare catch here swallowed parse/stat/rm failures, so the
|
|
149
|
+
// root cause became invisible after withAgentsLock's 60s "locked" error.
|
|
150
|
+
logInternalError("crew-agents.remove-stale-lock", error, `lockPath=${lockPath}`, "warn");
|
|
147
151
|
return false;
|
|
148
152
|
}
|
|
149
153
|
}
|
|
150
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Phase 3.1 (decision α): token-guarded release for the agents-record lock,
|
|
157
|
+
* mirroring `locks.ts` `releaseLock` semantics — only `rmSync` if the stored
|
|
158
|
+
* token still matches the token we wrote at acquisition. If the lock was
|
|
159
|
+
* stolen while our critical section ran (stale-steal replaced the file with a
|
|
160
|
+
* NEW owner's token), releasing must be a no-op — otherwise we would delete
|
|
161
|
+
* the new holder's lock and break mutual exclusion (the exact race the
|
|
162
|
+
* token guard in locks.ts fixes). Missing/corrupt files are handled safely:
|
|
163
|
+
* ENOENT is a no-op (lock already gone); a symlink is never removed (defense
|
|
164
|
+
* against attacker-planted symlinks, matching locks.ts); an unreadable/
|
|
165
|
+
* unparseable payload is treated like locks.ts `releaseLock` (stored token
|
|
166
|
+
* unknown → remove) so a torn write cannot strand the lock forever — the
|
|
167
|
+
* 30s stale-steal path remains the backstop for genuinely foreign locks.
|
|
168
|
+
*/
|
|
169
|
+
function releaseAgentsLock(filePath: string, token: string): void {
|
|
170
|
+
try {
|
|
171
|
+
const stat = fs.lstatSync(filePath);
|
|
172
|
+
if (stat.isSymbolicLink()) return;
|
|
173
|
+
} catch {
|
|
174
|
+
/* ENOENT — lock already gone, nothing to release */
|
|
175
|
+
}
|
|
176
|
+
let stored: string | undefined;
|
|
177
|
+
try {
|
|
178
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
179
|
+
const parsed = JSON.parse(raw) as { token?: unknown };
|
|
180
|
+
stored = typeof parsed.token === "string" ? parsed.token : undefined;
|
|
181
|
+
} catch {
|
|
182
|
+
/* unreadable/corrupt — see doc comment: remove, mirroring locks.ts */
|
|
183
|
+
}
|
|
184
|
+
if (stored === undefined || stored === token) {
|
|
185
|
+
try {
|
|
186
|
+
fs.rmSync(filePath, { force: true });
|
|
187
|
+
} catch (error) {
|
|
188
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
189
|
+
if (code !== "ENOENT") {
|
|
190
|
+
logInternalError("crew-agents.release-lock", error, `lockPath=${filePath}`, "warn");
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// stored !== token → lock stolen by another process; do NOT touch.
|
|
195
|
+
}
|
|
196
|
+
|
|
151
197
|
function withAgentsLock<T>(manifest: TeamRunManifest, fn: () => T): T {
|
|
152
198
|
const filePath = agentsLockPath(manifest);
|
|
153
199
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
200
|
+
// Phase 3.1 (decision α): the agents lock file now carries a randomUUID
|
|
201
|
+
// `token` (format change explicitly accepted by the plan) so the release
|
|
202
|
+
// below is token-guarded — a stale-steal by another process can no longer
|
|
203
|
+
// be released by the wrong owner (previously PID-only release). Mirrors
|
|
204
|
+
// the `writeLockFile` payload shape in locks.ts ({kind,pid,createdAt,token}
|
|
205
|
+
// minus `kind`). Generated ONCE per call; only the successful O_EXCL write
|
|
206
|
+
// persists it, so a single token per acquisition is correct.
|
|
207
|
+
const token = randomUUID();
|
|
154
208
|
let attempt = 0;
|
|
155
209
|
const deadline = Date.now() + AGENTS_LOCK_STALE_MS * 2;
|
|
156
210
|
while (true) {
|
|
157
211
|
try {
|
|
158
|
-
const fd = fs.openSync(filePath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL,
|
|
212
|
+
const fd = fs.openSync(filePath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, 0o600);
|
|
159
213
|
try {
|
|
160
214
|
fs.writeSync(
|
|
161
215
|
fd,
|
|
162
216
|
JSON.stringify({
|
|
163
217
|
pid: process.pid,
|
|
164
218
|
createdAt: new Date().toISOString(),
|
|
219
|
+
token,
|
|
165
220
|
}),
|
|
166
221
|
);
|
|
167
222
|
} finally {
|
|
@@ -188,11 +243,8 @@ function withAgentsLock<T>(manifest: TeamRunManifest, fn: () => T): T {
|
|
|
188
243
|
try {
|
|
189
244
|
return fn();
|
|
190
245
|
} finally {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
} catch {
|
|
194
|
-
/* best-effort */
|
|
195
|
-
}
|
|
246
|
+
// Phase 3.1 (decision α): unconditional rmSync → token-guarded release.
|
|
247
|
+
releaseAgentsLock(filePath, token);
|
|
196
248
|
}
|
|
197
249
|
}
|
|
198
250
|
|
|
@@ -221,7 +273,10 @@ export function readCrewAgents(manifest: TeamRunManifest): CrewAgentRecord[] {
|
|
|
221
273
|
// 2.5: ensure intra-process coalesced writes are visible to subsequent
|
|
222
274
|
// readers in the same process. Cross-process readers still see the file
|
|
223
275
|
// after at most one coalesce window (250 ms).
|
|
224
|
-
|
|
276
|
+
// R10-2: scoped flush — readCrewAgents only reads agents.json, so it must
|
|
277
|
+
// not drain unrelated coalesced writes (tasks.json of live runs, other
|
|
278
|
+
// runs' agents.json) process-wide on every read.
|
|
279
|
+
flushPendingAtomicWrites(agentsPath(manifest));
|
|
225
280
|
try {
|
|
226
281
|
const records = readJsonFileCoalesced(
|
|
227
282
|
agentsPath(manifest),
|
|
@@ -236,7 +291,13 @@ export function readCrewAgents(manifest: TeamRunManifest): CrewAgentRecord[] {
|
|
|
236
291
|
seen.add(r.id);
|
|
237
292
|
return true;
|
|
238
293
|
});
|
|
239
|
-
|
|
294
|
+
// R10-7: write back only when the corrected list actually differs from
|
|
295
|
+
// the current content. A filter() only removes entries, so element
|
|
296
|
+
// identity + length is an exact (and cheap) change check — deep-equal is
|
|
297
|
+
// unnecessary, and this eliminates the redundant durable write + lock
|
|
298
|
+
// churn on unchanged reads.
|
|
299
|
+
const changed = deduped.length !== records.length || records.some((record, index) => record !== deduped[index]);
|
|
300
|
+
if (changed) {
|
|
240
301
|
// Schema mismatch or duplicates detected — save corrected state
|
|
241
302
|
saveCrewAgents(manifest, deduped);
|
|
242
303
|
}
|
|
@@ -264,7 +325,10 @@ export async function readCrewAgentsAsync(manifest: TeamRunManifest): Promise<Cr
|
|
|
264
325
|
seen.add(r.id);
|
|
265
326
|
return true;
|
|
266
327
|
});
|
|
267
|
-
|
|
328
|
+
// R10-7: write back only on actual change (element identity + length —
|
|
329
|
+
// see the sync readCrewAgents path for the reasoning).
|
|
330
|
+
const changed = deduped.length !== raw.length || raw.some((record, index) => record !== deduped[index]);
|
|
331
|
+
if (changed) {
|
|
268
332
|
try {
|
|
269
333
|
saveCrewAgents(manifest, deduped);
|
|
270
334
|
} catch {
|
|
@@ -295,7 +359,7 @@ export async function readCrewAgentsAsync(manifest: TeamRunManifest): Promise<Cr
|
|
|
295
359
|
export function saveCrewAgents(manifest: TeamRunManifest, records: CrewAgentRecord[]): void {
|
|
296
360
|
// P0-3: flush any pending coalesced (best-effort) write first so a stale
|
|
297
361
|
// debounced progress snapshot can't clobber this durable write.
|
|
298
|
-
flushPendingAgentWrites();
|
|
362
|
+
flushPendingAgentWrites(manifest, records);
|
|
299
363
|
withAgentsLock(manifest, () => {
|
|
300
364
|
fs.mkdirSync(manifest.stateRoot, { recursive: true });
|
|
301
365
|
const filePath = agentsPath(manifest);
|
|
@@ -430,9 +494,14 @@ export function writeCrewAgentStatusCoalesced(manifest: TeamRunManifest, record:
|
|
|
430
494
|
});
|
|
431
495
|
}
|
|
432
496
|
|
|
433
|
-
/** @internal Flush
|
|
434
|
-
function flushPendingAgentWrites(): void {
|
|
435
|
-
|
|
497
|
+
/** @internal Flush coalesced agent-record writes synchronously. Hook into cleanup paths. */
|
|
498
|
+
function flushPendingAgentWrites(manifest: TeamRunManifest, records: CrewAgentRecord[]): void {
|
|
499
|
+
// R10-2: scope the pre-durable-save flush to the agent-record files this
|
|
500
|
+
// save can be clobbered by (agents.json + the per-task status.json files).
|
|
501
|
+
// Unrelated coalesced writes (tasks.json, other runs) stay pending on their
|
|
502
|
+
// own timers — draining them here only added latency to this path.
|
|
503
|
+
flushPendingAtomicWrites(agentsPath(manifest));
|
|
504
|
+
for (const record of records) flushPendingAtomicWrites(agentStatusPath(manifest, record.taskId));
|
|
436
505
|
}
|
|
437
506
|
|
|
438
507
|
export function readCrewAgentStatus(manifest: TeamRunManifest, taskOrAgentId: string): CrewAgentRecord | undefined {
|
|
@@ -528,6 +597,10 @@ function nextAgentEventSeq(filePath: string): number {
|
|
|
528
597
|
}
|
|
529
598
|
|
|
530
599
|
export function appendCrewAgentEvent(manifest: TeamRunManifest, taskId: string, event: unknown): void {
|
|
600
|
+
// Mixed-usage guard: if a buffered batch is pending for this task, land it
|
|
601
|
+
// first so seq allocation stays monotonic and file order matches seq order
|
|
602
|
+
// (reader cursors advance by seq in file order).
|
|
603
|
+
flushCrewAgentRecordBuffer(manifest, taskId);
|
|
531
604
|
ensureAgentStateDir(manifest, taskId);
|
|
532
605
|
const filePath = agentStateFile(manifest, taskId, "events.jsonl");
|
|
533
606
|
const seq = nextAgentEventSeq(filePath);
|
|
@@ -617,10 +690,182 @@ export function readCrewAgentEventsCursor(
|
|
|
617
690
|
|
|
618
691
|
export function appendCrewAgentOutput(manifest: TeamRunManifest, taskId: string, text: string): void {
|
|
619
692
|
if (!text.trim()) return;
|
|
693
|
+
// Mixed-usage guard (see appendCrewAgentEvent): land any pending buffered
|
|
694
|
+
// lines first so buffered/direct lines never interleave out of order.
|
|
695
|
+
flushCrewAgentRecordBuffer(manifest, taskId);
|
|
620
696
|
ensureAgentStateDir(manifest, taskId);
|
|
621
697
|
fs.appendFileSync(agentStateFile(manifest, taskId, "output.log"), `${redactSecretString(text)}\n`, "utf-8");
|
|
622
698
|
}
|
|
623
699
|
|
|
700
|
+
// ---------------------------------------------------------------------------
|
|
701
|
+
// R10-5 (Wave 2B item 4): per-task batching for the agent-record append sinks.
|
|
702
|
+
// ---------------------------------------------------------------------------
|
|
703
|
+
// child-executor's hot path previously did, PER CHILD EVENT:
|
|
704
|
+
// appendCrewAgentEvent -> nextAgentEventSeq (stat + sidecar read) + appendFileSync
|
|
705
|
+
// appendCrewAgentOutput -> appendFileSync
|
|
706
|
+
// (~4 syscalls/event after the P1-5 path memo, ×N events/task). The buffered
|
|
707
|
+
// variants below collapse adjacent appends into ONE appendFileSync per flush.
|
|
708
|
+
// Flush triggers: task boundary (child-executor finally), process exit / signal,
|
|
709
|
+
// 32-event cap, or the 250ms window timer — whichever comes first.
|
|
710
|
+
//
|
|
711
|
+
// CRASH-WINDOW TRADEOFF (deliberate, documented): buffering widens the SIGKILL
|
|
712
|
+
// loss window for AGENT PROGRESS events to <=250ms (AGENT_RECORD_BUFFER_WINDOW_MS
|
|
713
|
+
// below) instead of 0. Precedent: the event-log coalescer already accepts a
|
|
714
|
+
// documented 50ms loss window for non-terminal task.progress events; agent
|
|
715
|
+
// events.jsonl/output.log lines are the same class of re-derivable progress
|
|
716
|
+
// telemetry — authoritative terminal state lives in tasks.json/agents.json,
|
|
717
|
+
// which stay on their existing durable/coalesced-atomic paths. Durable
|
|
718
|
+
// artifacts (result.md, steering, run-level events.jsonl) are NOT routed
|
|
719
|
+
// through this buffer.
|
|
720
|
+
//
|
|
721
|
+
// SEQUENCE-NUMBER CORRECTNESS: seqs are RESERVED from nextAgentEventSeq() at
|
|
722
|
+
// the first buffered event and incremented in memory, so buffered events can
|
|
723
|
+
// never collide with each other, and the post-flush seq cache + sidecar are
|
|
724
|
+
// updated with the last reserved seq exactly like a direct append. The direct
|
|
725
|
+
// appendCrewAgentEvent()/appendCrewAgentOutput() flush any pending buffer for
|
|
726
|
+
// the task BEFORE appending, so a mixed buffered/direct caller can neither
|
|
727
|
+
// double-allocate a seq nor land a higher-seq line before lower-seq buffered
|
|
728
|
+
// lines. Within pi-crew a task's sinks have exactly one writer at a time
|
|
729
|
+
// (child-executor OR live-executor, never both), so that guard is
|
|
730
|
+
// defense-in-depth, not a load-bearing lock.
|
|
731
|
+
//
|
|
732
|
+
// v0.9.26 lesson (event-log.ts:237,416-425): this buffer is for the AGENT
|
|
733
|
+
// record sinks ONLY — core run events.jsonl appends must NOT be routed
|
|
734
|
+
// through buffered machinery.
|
|
735
|
+
const AGENT_RECORD_BUFFER_MAX_EVENTS = 32;
|
|
736
|
+
const AGENT_RECORD_BUFFER_WINDOW_MS = 250;
|
|
737
|
+
|
|
738
|
+
interface AgentRecordBuffer {
|
|
739
|
+
manifest: TeamRunManifest;
|
|
740
|
+
taskId: string;
|
|
741
|
+
events: string[];
|
|
742
|
+
output: string[];
|
|
743
|
+
lastReservedSeq: number; // 0 = not yet reserved from disk state
|
|
744
|
+
timer?: NodeJS.Timeout;
|
|
745
|
+
}
|
|
746
|
+
const agentRecordBuffers = new Map<string, AgentRecordBuffer>();
|
|
747
|
+
|
|
748
|
+
function agentRecordBufferKey(manifest: TeamRunManifest, taskId: string): string {
|
|
749
|
+
return `${manifest.stateRoot}\0${safeAgentTaskId(taskId)}`;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function flushAgentRecordBuffer(buffer: AgentRecordBuffer): void {
|
|
753
|
+
const { manifest, taskId } = buffer;
|
|
754
|
+
if (buffer.timer) {
|
|
755
|
+
clearTimeout(buffer.timer);
|
|
756
|
+
buffer.timer = undefined;
|
|
757
|
+
}
|
|
758
|
+
if (buffer.events.length > 0) {
|
|
759
|
+
const filePath = agentStateFile(manifest, taskId, "events.jsonl");
|
|
760
|
+
try {
|
|
761
|
+
fs.appendFileSync(filePath, buffer.events.join(""), "utf-8");
|
|
762
|
+
// Mirror appendCrewAgentEvent's post-append bookkeeping so subsequent
|
|
763
|
+
// nextAgentEventSeq calls (direct or buffered) see the reserved seqs.
|
|
764
|
+
const stat = fs.statSync(filePath);
|
|
765
|
+
setAgentEventSeqCache(filePath, {
|
|
766
|
+
size: stat.size,
|
|
767
|
+
mtimeMs: stat.mtimeMs,
|
|
768
|
+
seq: buffer.lastReservedSeq,
|
|
769
|
+
});
|
|
770
|
+
writeSeqToSidecar(filePath, buffer.lastReservedSeq);
|
|
771
|
+
} catch (error) {
|
|
772
|
+
// Best-effort progress telemetry: a fs failure here (e.g. run dir
|
|
773
|
+
// deleted by prune/forget) must not fail the task; drop the batch.
|
|
774
|
+
logInternalError("crew-agent-records.buffered-events-flush", error, `filePath=${filePath}`);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
if (buffer.output.length > 0) {
|
|
778
|
+
try {
|
|
779
|
+
fs.appendFileSync(agentStateFile(manifest, taskId, "output.log"), buffer.output.join(""), "utf-8");
|
|
780
|
+
} catch (error) {
|
|
781
|
+
logInternalError("crew-agent-records.buffered-output-flush", error, `taskId=${taskId}`);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
buffer.events.length = 0;
|
|
785
|
+
buffer.output.length = 0;
|
|
786
|
+
buffer.lastReservedSeq = 0;
|
|
787
|
+
agentRecordBuffers.delete(agentRecordBufferKey(manifest, taskId));
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* Flush any pending buffered agent-record appends for `taskId` synchronously.
|
|
792
|
+
* Task-boundary hook — child-executor calls this at attempt completion/failure
|
|
793
|
+
* and process exit so no buffered line outlives the task.
|
|
794
|
+
*/
|
|
795
|
+
export function flushCrewAgentRecordBuffer(manifest: TeamRunManifest, taskId: string): void {
|
|
796
|
+
const buffer = agentRecordBuffers.get(agentRecordBufferKey(manifest, taskId));
|
|
797
|
+
if (buffer) flushAgentRecordBuffer(buffer);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/** @internal Flush ALL buffered agent-record writes (process-exit / signal hook). */
|
|
801
|
+
export function flushAllCrewAgentRecordBuffers(): void {
|
|
802
|
+
for (const buffer of [...agentRecordBuffers.values()]) flushAgentRecordBuffer(buffer);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
export function appendCrewAgentEventBuffered(manifest: TeamRunManifest, taskId: string, event: unknown): void {
|
|
806
|
+
const filePath = agentStateFile(manifest, taskId, "events.jsonl");
|
|
807
|
+
const key = agentRecordBufferKey(manifest, taskId);
|
|
808
|
+
let buffer = agentRecordBuffers.get(key);
|
|
809
|
+
if (!buffer) {
|
|
810
|
+
buffer = { manifest, taskId, events: [], output: [], lastReservedSeq: 0 };
|
|
811
|
+
agentRecordBuffers.set(key, buffer);
|
|
812
|
+
}
|
|
813
|
+
if (buffer.lastReservedSeq === 0) {
|
|
814
|
+
// Reserve the next seq ONCE from the on-disk state; further buffered
|
|
815
|
+
// events increment in memory (no per-event stat/sidecar read).
|
|
816
|
+
buffer.lastReservedSeq = nextAgentEventSeq(filePath) - 1;
|
|
817
|
+
}
|
|
818
|
+
const seq = ++buffer.lastReservedSeq;
|
|
819
|
+
buffer.events.push(`${JSON.stringify(redactSecrets({ seq, time: new Date().toISOString(), event }))}\n`);
|
|
820
|
+
scheduleAgentRecordBufferFlush(buffer);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
export function appendCrewAgentOutputBuffered(manifest: TeamRunManifest, taskId: string, text: string): void {
|
|
824
|
+
if (!text.trim()) return;
|
|
825
|
+
// Ensure the state dir at BUFFER time (the event variant gets this for free
|
|
826
|
+
// via agentStateFile): fs.appendFileSync at flush time does NOT create parent
|
|
827
|
+
// dirs, so an output-only task with no events buffered yet would drop its
|
|
828
|
+
// whole batch on ENOENT. Memoized (ensuredAgentDirs) — one mkdir per task,
|
|
829
|
+
// not per line — preserving the R10-5 batching win.
|
|
830
|
+
ensureAgentStateDir(manifest, taskId);
|
|
831
|
+
const key = agentRecordBufferKey(manifest, taskId);
|
|
832
|
+
let buffer = agentRecordBuffers.get(key);
|
|
833
|
+
if (!buffer) {
|
|
834
|
+
buffer = { manifest, taskId, events: [], output: [], lastReservedSeq: 0 };
|
|
835
|
+
agentRecordBuffers.set(key, buffer);
|
|
836
|
+
}
|
|
837
|
+
buffer.output.push(`${redactSecretString(text)}\n`);
|
|
838
|
+
scheduleAgentRecordBufferFlush(buffer);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function scheduleAgentRecordBufferFlush(buffer: AgentRecordBuffer): void {
|
|
842
|
+
// Cap: flush now so buffer memory and the crash window stay bounded.
|
|
843
|
+
if (buffer.events.length >= AGENT_RECORD_BUFFER_MAX_EVENTS || buffer.output.length >= AGENT_RECORD_BUFFER_MAX_EVENTS) {
|
|
844
|
+
flushAgentRecordBuffer(buffer);
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
// Window: a REAL (unref'd) timer enforces the <=250ms loss-window bound even
|
|
848
|
+
// when no further events arrive (lazy check-on-next-append would not).
|
|
849
|
+
if (!buffer.timer) {
|
|
850
|
+
buffer.timer = setTimeout(() => {
|
|
851
|
+
buffer.timer = undefined;
|
|
852
|
+
flushAgentRecordBuffer(buffer);
|
|
853
|
+
}, AGENT_RECORD_BUFFER_WINDOW_MS);
|
|
854
|
+
buffer.timer.unref();
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Defense-in-depth (mirrors atomic-write.ts): land buffered agent-record lines
|
|
859
|
+
// on normal process exit and best-effort on termination signals.
|
|
860
|
+
/** @internal Test-only: number of pending buffered agent-record batches. */
|
|
861
|
+
export function __test__agentRecordBufferCount(): number {
|
|
862
|
+
return agentRecordBuffers.size;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
process.on("exit", () => flushAllCrewAgentRecordBuffers());
|
|
866
|
+
process.on("SIGTERM", () => setImmediate(() => flushAllCrewAgentRecordBuffers()));
|
|
867
|
+
process.on("SIGINT", () => setImmediate(() => flushAllCrewAgentRecordBuffers()));
|
|
868
|
+
|
|
624
869
|
export function emptyCrewAgentProgress(): CrewAgentProgress {
|
|
625
870
|
return { recentTools: [], recentOutput: [], toolCount: 0 };
|
|
626
871
|
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* delegate-spawn.ts — root-side grandchild spawner for governed nesting
|
|
3
|
+
* (ADR-5 §1/§2/§6, WP-5 step 5).
|
|
4
|
+
*
|
|
5
|
+
* The broker's `delegate.request` handler calls this on an admitted request:
|
|
6
|
+
* a DIRECT `runChildPi` call-site that never touches the global worker
|
|
7
|
+
* semaphore (the MAJ#3 anti-deadlock precedent generalized — the nested-slot
|
|
8
|
+
* budget is managed by the broker handler, not here), with `depthOverride`
|
|
9
|
+
* from the parent task record, namespaced artifacts, and a timeout-driven
|
|
10
|
+
* soft cancel (`delegate-timeout`).
|
|
11
|
+
*
|
|
12
|
+
* Security review round 1 (T3):
|
|
13
|
+
* - S1#2: the grandchild's ROLE is threaded so role-based tool restrictions
|
|
14
|
+
* apply (`--tools`/`--exclude-tools` from role-tools config) — a
|
|
15
|
+
* "read-only" explorer/analyst grandchild must actually be read-only
|
|
16
|
+
* (ADR-5 §9's serialization exemption depends on it).
|
|
17
|
+
* - S1#1: the grandchild authenticates under its OWN subId identity
|
|
18
|
+
* (grandchild-scoped broker token pre-minted by the handler) — never the
|
|
19
|
+
* parent's task token. Identity fields (`agentId: subId`) match.
|
|
20
|
+
* - S2#1: namespaced artifacts via the `artifactsRoot` input field (the env
|
|
21
|
+
* spread never survived the spawn allowlist).
|
|
22
|
+
* - S2#3: usage roll-up — message_end usage events are accumulated from the
|
|
23
|
+
* child's JSON event stream and reported for the parent-task reconciliation.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import * as fs from "node:fs";
|
|
27
|
+
import * as path from "node:path";
|
|
28
|
+
import type { AgentConfig } from "../agents/agent-config.ts";
|
|
29
|
+
import { type ChildPiRunResult, runChildPi } from "./child-pi/child-pi.ts";
|
|
30
|
+
|
|
31
|
+
export interface GrandchildSpawnInput {
|
|
32
|
+
cwd: string;
|
|
33
|
+
runId: string;
|
|
34
|
+
parentTaskId: string;
|
|
35
|
+
/** Nested-slot holder id (minted by the broker handler). */
|
|
36
|
+
subId: string;
|
|
37
|
+
prompt: string;
|
|
38
|
+
role: string;
|
|
39
|
+
model?: string;
|
|
40
|
+
maxTurns?: number;
|
|
41
|
+
/** Mandatory default 900 (spawn-policy normalized). */
|
|
42
|
+
timeoutSec: number;
|
|
43
|
+
/** Parent-task-record depth + 1 (ADR-5 §3). */
|
|
44
|
+
depthOverride: number;
|
|
45
|
+
/** Manifest eventsPath — threads PI_CREW_STATE_ROOT so the grandchild's
|
|
46
|
+
* ask tool + scratchpad metrics work (stateRoot = dirname(eventsPath)). */
|
|
47
|
+
eventsPath?: string;
|
|
48
|
+
/** Grandchild-scoped broker credentials (S1#1 — NEVER the parent token). */
|
|
49
|
+
brokerSpawn?: { socketPath: string; token: string };
|
|
50
|
+
onSpawn?: (pid: number | null) => void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface GrandchildSpawnResult {
|
|
54
|
+
ok: boolean;
|
|
55
|
+
/** Fenced-ish result text (caller fences before mailbox delivery). */
|
|
56
|
+
resultText: string;
|
|
57
|
+
/** Usage in tokens for roll-up (input+output+cache summed over
|
|
58
|
+
* message_end events); undefined = unattributed. */
|
|
59
|
+
usageTokens?: number;
|
|
60
|
+
timedOut?: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Minimal in-memory agent config for a delegate grandchild role. */
|
|
64
|
+
function agentForRole(role: string): AgentConfig {
|
|
65
|
+
return {
|
|
66
|
+
name: role,
|
|
67
|
+
description: `delegate grandchild (${role})`,
|
|
68
|
+
source: "builtin",
|
|
69
|
+
filePath: "",
|
|
70
|
+
systemPrompt: "",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Sum usage tokens from a message_end JSON event record (S2#3 roll-up). */
|
|
75
|
+
export function usageTokensFromEvent(event: unknown): number | undefined {
|
|
76
|
+
if (!event || typeof event !== "object" || Array.isArray(event)) return undefined;
|
|
77
|
+
const record = event as { type?: unknown; usage?: unknown; message?: { usage?: unknown } };
|
|
78
|
+
if (record.type !== "message_end") return undefined;
|
|
79
|
+
const usage = (record.usage ?? record.message?.usage) as Record<string, unknown> | undefined;
|
|
80
|
+
if (!usage || typeof usage !== "object") return undefined;
|
|
81
|
+
let total = 0;
|
|
82
|
+
let seen = false;
|
|
83
|
+
for (const key of ["input", "output", "cacheRead", "cacheWrite"]) {
|
|
84
|
+
const value = usage[key];
|
|
85
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
86
|
+
total += value;
|
|
87
|
+
seen = true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return seen ? total : undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Namespaced artifacts root: artifacts/<runId>/<parentTaskId>/nested/<subId>/ */
|
|
94
|
+
export function grandchildArtifactsRoot(cwd: string, runId: string, parentTaskId: string, subId: string): string {
|
|
95
|
+
return path.join(cwd, ".crew", "artifacts", runId, parentTaskId, "nested", subId);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function spawnDelegateGrandchild(input: GrandchildSpawnInput): Promise<GrandchildSpawnResult> {
|
|
99
|
+
const artifactsRoot = path.join(input.cwd, ".crew", "artifacts", input.runId, input.parentTaskId, "nested", input.subId);
|
|
100
|
+
fs.mkdirSync(artifactsRoot, { recursive: true });
|
|
101
|
+
|
|
102
|
+
const abort = new AbortController();
|
|
103
|
+
const timer = setTimeout(() => abort.abort(), input.timeoutSec * 1000);
|
|
104
|
+
timer.unref?.();
|
|
105
|
+
|
|
106
|
+
// S2#3: accumulate grandchild usage from message_end events.
|
|
107
|
+
let usageTokens: number | undefined;
|
|
108
|
+
const onJsonEvent = (event: unknown): void => {
|
|
109
|
+
const tokens = usageTokensFromEvent(event);
|
|
110
|
+
if (tokens !== undefined) usageTokens = (usageTokens ?? 0) + tokens;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const result: ChildPiRunResult = await runChildPi({
|
|
115
|
+
cwd: input.cwd,
|
|
116
|
+
task: input.prompt,
|
|
117
|
+
agent: agentForRole(input.role),
|
|
118
|
+
// S1#2: thread the role so role-based tool restrictions apply.
|
|
119
|
+
role: input.role,
|
|
120
|
+
...(input.model !== undefined ? { model: input.model } : {}),
|
|
121
|
+
...(input.maxTurns !== undefined ? { maxTurns: input.maxTurns } : {}),
|
|
122
|
+
depthOverride: input.depthOverride,
|
|
123
|
+
runId: input.runId,
|
|
124
|
+
// S1#1: the grandchild carries its OWN identity + credentials —
|
|
125
|
+
// never the parent's task token.
|
|
126
|
+
agentId: input.subId,
|
|
127
|
+
...(input.brokerSpawn ? { brokerSpawn: input.brokerSpawn } : {}),
|
|
128
|
+
...(input.eventsPath ? { eventsPath: input.eventsPath } : {}),
|
|
129
|
+
// S2#1: namespaced artifacts via the typed field.
|
|
130
|
+
artifactsRoot,
|
|
131
|
+
signal: abort.signal,
|
|
132
|
+
onJsonEvent,
|
|
133
|
+
onSpawn: input.onSpawn,
|
|
134
|
+
});
|
|
135
|
+
const ok = result.exitCode === 0;
|
|
136
|
+
const text = (result.rawFinalText ?? result.stdout ?? "").trim() || (ok ? "" : result.stderr.trim());
|
|
137
|
+
return { ok, resultText: text, ...(usageTokens !== undefined ? { usageTokens } : {}) };
|
|
138
|
+
} catch (error) {
|
|
139
|
+
const timedOut = abort.signal.aborted;
|
|
140
|
+
return {
|
|
141
|
+
ok: false,
|
|
142
|
+
timedOut,
|
|
143
|
+
resultText: timedOut ? "[delegate timed out]" : `delegate spawn failed: ${(error as Error).message}`,
|
|
144
|
+
};
|
|
145
|
+
} finally {
|
|
146
|
+
clearTimeout(timer);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Result delivery for runs detached by an agent-view switch.
|
|
3
|
+
*
|
|
4
|
+
* Opening an agent view mid-run releases the foreground waiter (see
|
|
5
|
+
* `detachRunPromise`), so the parent turn settles with a "detached" tool
|
|
6
|
+
* result and the run keeps executing. Without this module the run's FINAL
|
|
7
|
+
* result would never reach the conversation: the async notifier only toasts,
|
|
8
|
+
* and its toast is suppressed while a view session is active (different
|
|
9
|
+
* session id) or when the run finished before the operator returned to main.
|
|
10
|
+
*
|
|
11
|
+
* The registry is in-process; a pending result is parked while an agent view is
|
|
12
|
+
* open so the worker's own view session never receives the parent's report.
|
|
13
|
+
*/
|
|
14
|
+
import { loadRunManifestById } from "../state/stores/state-store.ts";
|
|
15
|
+
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
16
|
+
import { isFinishedRunStatus } from "./process-status.ts";
|
|
17
|
+
|
|
18
|
+
interface DetachedRun {
|
|
19
|
+
runId: string;
|
|
20
|
+
cwd: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const detachedRuns = new Map<string, DetachedRun>();
|
|
24
|
+
|
|
25
|
+
/** Record a run whose foreground waiter was released by a view switch. */
|
|
26
|
+
export function markRunDetached(runId: string, cwd: string): void {
|
|
27
|
+
if (!runId || !cwd) return;
|
|
28
|
+
detachedRuns.set(runId, { runId, cwd });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Cheap guard for hot paths (render tick): nothing to do when empty. */
|
|
32
|
+
export function hasDetachedRuns(): boolean {
|
|
33
|
+
return detachedRuns.size > 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function forgetDetachedRun(runId: string): void {
|
|
37
|
+
detachedRuns.delete(runId);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function clearDetachedRunsForTest(): void {
|
|
41
|
+
detachedRuns.clear();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Human-readable completion summary for a detached run. */
|
|
45
|
+
export function formatDetachedRunResult(manifest: TeamRunManifest, tasks: TeamTaskState[]): string {
|
|
46
|
+
const lines = [
|
|
47
|
+
`pi-crew run ${manifest.status}: ${manifest.runId} (${manifest.team}/${manifest.workflow ?? "none"})`,
|
|
48
|
+
`Goal: ${manifest.goal}`,
|
|
49
|
+
"",
|
|
50
|
+
"This run was detached when you opened an agent view; it finished in the background.",
|
|
51
|
+
];
|
|
52
|
+
if (tasks.length > 0) {
|
|
53
|
+
lines.push("", "Tasks:");
|
|
54
|
+
for (const task of tasks) {
|
|
55
|
+
const detail = task.error ? ` — ${task.error}` : "";
|
|
56
|
+
lines.push(` • ${task.id} (${task.role ?? "?"}): ${task.status}${detail}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (manifest.artifactsRoot) lines.push("", `Artifacts: ${manifest.artifactsRoot}`);
|
|
60
|
+
lines.push(`State: ${manifest.stateRoot}`);
|
|
61
|
+
return lines.join("\n");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Results of finished detached runs that may be shown in the CURRENT session.
|
|
66
|
+
* Entries are NOT removed: the caller drops each one with `forgetDetachedRun`
|
|
67
|
+
* only after the delivery actually succeeded, so a failed send retries on the
|
|
68
|
+
* next tick instead of losing the run's outcome.
|
|
69
|
+
*
|
|
70
|
+
* Gating is by "am I inside an agent view", not by session id: the report must
|
|
71
|
+
* never land inside a worker's view session, but matching the owner's id is too
|
|
72
|
+
* brittle (the main session can be replaced/renamed while the view is open, and
|
|
73
|
+
* the registry is per pi process anyway, so any non-view session here IS the
|
|
74
|
+
* operator's own).
|
|
75
|
+
*/
|
|
76
|
+
export function peekFinishedDetachedRunResults(options: { inViewSession?: boolean } = {}): { runId: string; text: string }[] {
|
|
77
|
+
if (detachedRuns.size === 0 || options.inViewSession) return [];
|
|
78
|
+
const ready: { runId: string; text: string }[] = [];
|
|
79
|
+
for (const entry of [...detachedRuns.values()]) {
|
|
80
|
+
const loaded = loadRunManifestById(entry.cwd, entry.runId);
|
|
81
|
+
if (!loaded) {
|
|
82
|
+
// Run state vanished (pruned/deleted) — nothing left to report.
|
|
83
|
+
detachedRuns.delete(entry.runId);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (!isFinishedRunStatus(loaded.manifest.status)) continue;
|
|
87
|
+
ready.push({ runId: entry.runId, text: formatDetachedRunResult(loaded.manifest, loaded.tasks) });
|
|
88
|
+
}
|
|
89
|
+
return ready;
|
|
90
|
+
}
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { parse } from "acorn";
|
|
21
|
+
import { getCrewEnv } from "../config/env-vars.ts";
|
|
21
22
|
|
|
22
23
|
const NONDETERMINISM_ERROR =
|
|
23
24
|
"Workflow scripts must be deterministic: Date.now()/Math.random()/new Date() are unavailable. These introduce non-reproducible behavior across runs. Use ctx.vars for cached state, or pass a fixed seed via ctx.setArgs(). To bypass this check (escape hatch), set PI_CREW_DWF_SKIP_DETERMINISM_CHECK=1.";
|
|
@@ -57,7 +58,7 @@ export function assertDeterministicScript(script: string): void {
|
|
|
57
58
|
* (e.g. randomized benchmark scripts).
|
|
58
59
|
*/
|
|
59
60
|
export function isDeterminismCheckEnabled(): boolean {
|
|
60
|
-
return
|
|
61
|
+
return getCrewEnv("PI_CREW_DWF_SKIP_DETERMINISM_CHECK") !== "1";
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
// ---------------------------------------------------------------------------
|