pi-crew 0.11.24 → 0.11.26
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 +72 -0
- package/README.md +2 -2
- package/agents/designer.md +23 -0
- package/agents/orchestrator.md +17 -0
- package/agents/verifier.md +1 -1
- package/dist/index.mjs +568 -559
- package/{workflows/chain.workflow.md → docs/chain-workflow.md} +8 -0
- package/docs/dynamic-workflows.md +23 -0
- package/docs/publishing.md +34 -5
- package/docs/troubleshooting.md +50 -2
- package/docs/usage.md +7 -2
- package/install.mjs +2 -1
- package/package.json +4 -2
- package/skills/read-only-explorer/SKILL.md +15 -5
- package/skills/real-test-pi-crew/SKILL.md +7 -7
- package/src/config/config-validation.ts +58 -0
- package/src/config/config.ts +90 -5
- package/src/config/env-vars.ts +14 -7
- package/src/config/role-tools.ts +7 -5
- package/src/config/types.ts +4 -1
- package/src/extension/crew-cleanup.ts +6 -0
- package/src/extension/project-init.ts +19 -1
- package/src/extension/register.ts +68 -30
- package/src/extension/registration/artifact-cleanup.ts +52 -11
- package/src/extension/registration/commands/manage.ts +9 -2
- package/src/extension/registration/hook-registration.ts +3 -0
- package/src/extension/registration/lazy-configurers.ts +2 -0
- package/src/extension/registration/lifecycle-handlers.ts +79 -18
- package/src/extension/registration/render-loop.ts +27 -2
- package/src/extension/registration/tool-loop-guard.ts +5 -3
- package/src/extension/registration/verify-gate-tool.ts +7 -3
- package/src/extension/team-tool/api/read.ts +12 -3
- package/src/extension/team-tool/cancel.ts +11 -0
- package/src/extension/team-tool/config-patch.ts +56 -11
- package/src/extension/team-tool/destructive-gate.ts +72 -12
- package/src/extension/team-tool/dispatch/manage.ts +9 -1
- package/src/extension/team-tool/handle-schedule.ts +38 -4
- package/src/extension/team-tool/handle-settings.ts +76 -6
- package/src/extension/team-tool/health-monitor.ts +14 -2
- package/src/extension/team-tool/lifecycle-actions.ts +28 -10
- package/src/extension/team-tool/respond.ts +12 -5
- package/src/extension/team-tool/run-deadline.ts +29 -2
- package/src/extension/team-tool/run.ts +11 -0
- package/src/extension/team-tool/status.ts +64 -8
- package/src/extension/team-tool/workflow-manage.ts +5 -1
- package/src/extension/team-tool.ts +3 -0
- package/src/i18n.ts +18 -14
- package/src/prompt/inbox-poll.ts +14 -2
- package/src/prompt/message-tool.ts +1 -1
- package/src/prompt/prompt-runtime.ts +25 -3
- package/src/prompt/scratchpad-lifecycle.ts +14 -13
- package/src/prompt/surface-worker.ts +35 -9
- package/src/runtime/broker/crew-broker.ts +113 -111
- package/src/runtime/broker/delegate/delegate-abort.ts +118 -0
- package/src/runtime/broker/delegate/delegate-event.ts +3 -1
- package/src/runtime/broker/delegate/delegate-finalize.ts +179 -0
- package/src/runtime/broker/protocol/wait-resolve.ts +71 -5
- package/src/runtime/budget-enforcement.ts +26 -6
- package/src/runtime/chain-parser.ts +5 -3
- package/src/runtime/chain-runner.ts +41 -22
- package/src/runtime/child-pi/mock-fixtures.ts +12 -5
- package/src/runtime/compaction/tool-output-pruner.ts +8 -3
- package/src/runtime/delegate-spawn.ts +61 -8
- package/src/runtime/dispatch-batch.ts +10 -4
- package/src/runtime/effectiveness.ts +31 -3
- package/src/runtime/goal-workflow/plan-templates.ts +2 -2
- package/src/runtime/live-session/live-agent-control.ts +89 -6
- package/src/runtime/live-session/live-agent-manager.ts +94 -10
- package/src/runtime/live-session/live-session-runtime.ts +9 -2
- package/src/runtime/model/model-fallback.ts +6 -9
- package/src/runtime/model/pi-args.ts +3 -3
- package/src/runtime/model/runtime-policy.ts +4 -2
- package/src/runtime/model/runtime-resolver.ts +6 -2
- package/src/runtime/process/orphan-reaper.ts +143 -0
- package/src/runtime/process-status.ts +38 -16
- package/src/runtime/resilient-edit.ts +170 -93
- package/src/runtime/role-permission.ts +3 -1
- package/src/runtime/run-tracker.ts +9 -1
- package/src/runtime/sanitize/worker-text.ts +98 -0
- package/src/runtime/scheduling/run-coalesced-task-group.ts +82 -28
- package/src/runtime/spawn-policy.ts +190 -8
- package/src/runtime/stale-reconciler.ts +36 -1
- package/src/runtime/subagent-manager.ts +29 -0
- package/src/runtime/surface/herdr-provider.ts +31 -1
- package/src/runtime/task-output-context.ts +58 -11
- package/src/runtime/task-runner/post-execution.ts +150 -0
- package/src/runtime/task-runner/prompt-builder.ts +41 -8
- package/src/runtime/task-runner.ts +16 -3
- package/src/runtime/verification/classifier-pre-gate.ts +27 -6
- package/src/runtime/verification/verify-gate.ts +153 -30
- package/src/state/atomic-write.ts +73 -4
- package/src/state/contracts.ts +7 -0
- package/src/state/coordination/mailbox.ts +40 -11
- package/src/state/event-log/event-log-rotation.ts +28 -6
- package/src/state/event-log/poison-marker.ts +16 -1
- package/src/state/stores/artifact-store.ts +181 -6
- package/src/state/stores/manifest-io.ts +126 -8
- package/src/state/stores/state-store.ts +30 -31
- package/src/state/types.ts +13 -0
- package/src/state/usage.ts +41 -0
- package/src/ui/run-dashboard.ts +3 -3
- package/src/ui/settings-overlay.ts +19 -1
- package/src/utils/internal-error.ts +5 -1
- package/src/utils/timer-limits.ts +30 -0
- package/src/utils/token-counter.ts +47 -0
- package/src/workflows/discover-workflows.ts +9 -1
- package/src/worktree/worktree-manager.ts +128 -18
- package/workflows/default.workflow.md +1 -1
- package/workflows/fast-fix.workflow.md +1 -1
- package/workflows/plan-execute.workflow.md +1 -1
- package/workflows/review.workflow.md +1 -1
- package/workflows/strict-fast-fix.workflow.md +1 -1
|
@@ -14,6 +14,13 @@
|
|
|
14
14
|
* apply (`--tools`/`--exclude-tools` from role-tools config) — a
|
|
15
15
|
* "read-only" explorer/analyst grandchild must actually be read-only
|
|
16
16
|
* (ADR-5 §9's serialization exemption depends on it).
|
|
17
|
+
* R43-S1 (2026-10-19 fix-wave 6): the restrictions are now TRANSITIVE —
|
|
18
|
+
* the effective grandchild tool policy is the intersection (min) of the
|
|
19
|
+
* PARENT task role's policy and the requested role's policy
|
|
20
|
+
* (spawn-policy.ts effectiveGrandchildToolPolicy), applied to the
|
|
21
|
+
* in-memory agent config below. The delegate grandchildren are spawned
|
|
22
|
+
* from a MINIMAL AgentConfig (no agents/<role>.md frontmatter is read),
|
|
23
|
+
* so this is the ONLY argv-level tool cut they receive.
|
|
17
24
|
* - S1#1: the grandchild authenticates under its OWN subId identity
|
|
18
25
|
* (grandchild-scoped broker token pre-minted by the handler) — never the
|
|
19
26
|
* parent's task token. Identity fields (`agentId: subId`) match.
|
|
@@ -27,11 +34,15 @@ import * as fs from "node:fs";
|
|
|
27
34
|
import * as path from "node:path";
|
|
28
35
|
import type { AgentConfig } from "../agents/agent-config.ts";
|
|
29
36
|
import { type ChildPiRunResult, runChildPi } from "./child-pi/child-pi.ts";
|
|
37
|
+
import { effectiveGrandchildToolPolicy } from "./spawn-policy.ts";
|
|
30
38
|
|
|
31
39
|
export interface GrandchildSpawnInput {
|
|
32
40
|
cwd: string;
|
|
33
41
|
runId: string;
|
|
34
42
|
parentTaskId: string;
|
|
43
|
+
/** Parent task record role (run-locked read) — R43-S1 transitive tier
|
|
44
|
+
* containment: the grandchild's tool policy is min(parent, requested). */
|
|
45
|
+
parentRole?: string;
|
|
35
46
|
/** Nested-slot holder id (minted by the broker handler). */
|
|
36
47
|
subId: string;
|
|
37
48
|
prompt: string;
|
|
@@ -48,6 +59,13 @@ export interface GrandchildSpawnInput {
|
|
|
48
59
|
/** Grandchild-scoped broker credentials (S1#1 — NEVER the parent token). */
|
|
49
60
|
brokerSpawn?: { socketPath: string; token: string };
|
|
50
61
|
onSpawn?: (pid: number | null) => void;
|
|
62
|
+
/** M3-5 / R43-N3: owner-side cancellation. The broker's delegate abort
|
|
63
|
+
* registry aborts this on run terminalise / budget abort / broker stop, so
|
|
64
|
+
* a grandchild can no longer outlive its run by up to its own 24h deadline
|
|
65
|
+
* (pre-fix: fire-and-forget spawner with no abort path). Aborting produces
|
|
66
|
+
* `{ ok: false, cancelled: true }` — distinct from the deadline
|
|
67
|
+
* `timedOut` outcome so the settle path can audit which one happened. */
|
|
68
|
+
signal?: AbortSignal;
|
|
51
69
|
}
|
|
52
70
|
|
|
53
71
|
export interface GrandchildSpawnResult {
|
|
@@ -55,19 +73,36 @@ export interface GrandchildSpawnResult {
|
|
|
55
73
|
/** Fenced-ish result text (caller fences before mailbox delivery). */
|
|
56
74
|
resultText: string;
|
|
57
75
|
/** Usage in tokens for roll-up (input+output+cache summed over
|
|
58
|
-
* message_end events); undefined = unattributed.
|
|
76
|
+
* message_end events); undefined = unattributed.
|
|
77
|
+
* M3-4/R44-N4: AUDIT ONLY — the budget charge is the host-side
|
|
78
|
+
* reservation, never this self-attested number. */
|
|
59
79
|
usageTokens?: number;
|
|
60
80
|
timedOut?: boolean;
|
|
81
|
+
/** M3-5/R43-N3: the OWNER aborted the spawn (run terminalise / budget abort
|
|
82
|
+
* / broker stop) — distinct from the grandchild's own deadline. */
|
|
83
|
+
cancelled?: boolean;
|
|
61
84
|
}
|
|
62
85
|
|
|
63
|
-
/**
|
|
64
|
-
|
|
86
|
+
/**
|
|
87
|
+
* Minimal in-memory agent config for a delegate grandchild role.
|
|
88
|
+
*
|
|
89
|
+
* R43-S1: applies the EFFECTIVE tool policy — intersection (min) of the
|
|
90
|
+
* parent task role's policy and the requested grandchild role's policy —
|
|
91
|
+
* so the pi argv `--tools`/`--exclude-tools` cut reflects the transitive
|
|
92
|
+
* floor. A read-only parent's grandchild is bash/edit/write-denied even if
|
|
93
|
+
* the requested role itself is executor-class (defense-in-depth behind the
|
|
94
|
+
* spawn-policy admission gate).
|
|
95
|
+
*/
|
|
96
|
+
export function grandchildAgentForRole(role: string, parentRole?: string): AgentConfig {
|
|
97
|
+
const policy = effectiveGrandchildToolPolicy(parentRole, role);
|
|
65
98
|
return {
|
|
66
99
|
name: role,
|
|
67
100
|
description: `delegate grandchild (${role})`,
|
|
68
101
|
source: "builtin",
|
|
69
102
|
filePath: "",
|
|
70
103
|
systemPrompt: "",
|
|
104
|
+
...(policy.tools !== undefined ? { tools: policy.tools } : {}),
|
|
105
|
+
...(policy.excludeTools !== undefined ? { disallowedTools: policy.excludeTools } : {}),
|
|
71
106
|
};
|
|
72
107
|
}
|
|
73
108
|
|
|
@@ -118,7 +153,18 @@ export async function spawnDelegateGrandchild(input: GrandchildSpawnInput): Prom
|
|
|
118
153
|
const artifactsRoot = grandchildArtifactsRoot(input.cwd, input.runId, input.parentTaskId, input.subId);
|
|
119
154
|
fs.mkdirSync(artifactsRoot, { recursive: true });
|
|
120
155
|
|
|
156
|
+
// M3-5/R43-N3: two abort sources — this spawn's own deadline and the
|
|
157
|
+
// OWNER (broker abort registry). `ownerAborted` distinguishes them for the
|
|
158
|
+
// settle audit; the AbortController it drives is the single signal handed to
|
|
159
|
+
// runChildPi, so both paths kill the child the same way.
|
|
121
160
|
const abort = new AbortController();
|
|
161
|
+
let ownerAborted = false;
|
|
162
|
+
const onOwnerAbort = (): void => {
|
|
163
|
+
ownerAborted = true;
|
|
164
|
+
abort.abort(input.signal?.reason);
|
|
165
|
+
};
|
|
166
|
+
if (input.signal?.aborted) onOwnerAbort();
|
|
167
|
+
else input.signal?.addEventListener("abort", onOwnerAbort, { once: true });
|
|
122
168
|
const timer = setTimeout(() => abort.abort(), input.timeoutSec * 1000);
|
|
123
169
|
timer.unref?.();
|
|
124
170
|
|
|
@@ -133,8 +179,9 @@ export async function spawnDelegateGrandchild(input: GrandchildSpawnInput): Prom
|
|
|
133
179
|
const result: ChildPiRunResult = await runChildPi({
|
|
134
180
|
cwd: input.cwd,
|
|
135
181
|
task: input.prompt,
|
|
136
|
-
|
|
137
|
-
//
|
|
182
|
+
// S1#2 + R43-S1: thread the role AND the parent role so the effective
|
|
183
|
+
// (min-intersected) tool policy applies at the argv level.
|
|
184
|
+
agent: grandchildAgentForRole(input.role, input.parentRole),
|
|
138
185
|
role: input.role,
|
|
139
186
|
...(input.model !== undefined ? { model: input.model } : {}),
|
|
140
187
|
...(input.maxTurns !== undefined ? { maxTurns: input.maxTurns } : {}),
|
|
@@ -154,13 +201,19 @@ export async function spawnDelegateGrandchild(input: GrandchildSpawnInput): Prom
|
|
|
154
201
|
const ok = result.exitCode === 0;
|
|
155
202
|
return { ok, resultText: grandchildResultText(result), ...(usageTokens !== undefined ? { usageTokens } : {}) };
|
|
156
203
|
} catch (error) {
|
|
157
|
-
const timedOut = abort.signal.aborted;
|
|
204
|
+
const timedOut = abort.signal.aborted && !ownerAborted;
|
|
205
|
+
const cancelled = ownerAborted;
|
|
158
206
|
return {
|
|
159
207
|
ok: false,
|
|
160
|
-
timedOut,
|
|
161
|
-
resultText:
|
|
208
|
+
...(cancelled ? { cancelled } : { timedOut }),
|
|
209
|
+
resultText: cancelled
|
|
210
|
+
? "[delegate cancelled: run terminated]"
|
|
211
|
+
: timedOut
|
|
212
|
+
? "[delegate timed out]"
|
|
213
|
+
: `delegate spawn failed: ${(error as Error).message}`,
|
|
162
214
|
};
|
|
163
215
|
} finally {
|
|
164
216
|
clearTimeout(timer);
|
|
217
|
+
input.signal?.removeEventListener("abort", onOwnerAbort);
|
|
165
218
|
}
|
|
166
219
|
}
|
|
@@ -21,6 +21,7 @@ import type { TaskAttemptState, TeamRunManifest, TeamTaskState } from "../state/
|
|
|
21
21
|
import { logInternalError } from "../utils/internal-error.ts";
|
|
22
22
|
import type { WorkflowConfig, WorkflowStep } from "../workflows/workflow-config.ts";
|
|
23
23
|
import { isDelegateShadowTask } from "./broker/delegate/shadow-lifecycle.ts";
|
|
24
|
+
import { repointWaitState } from "./broker/protocol/wait-resolve.ts";
|
|
24
25
|
import { classifyAmbientNoiseGate } from "./classifier/ambient-noise-gate.ts";
|
|
25
26
|
import { resolveClassifierEnabled, resolveClassifierModel } from "./classifier/classifier-service.ts";
|
|
26
27
|
import { readCrewAgents, recordFromTask, saveCrewAgents } from "./crew-agent-records.ts";
|
|
@@ -192,11 +193,16 @@ export async function sweepExpiredWaitingTasks(
|
|
|
192
193
|
}
|
|
193
194
|
saveRunTasks(fresh.manifest, tasks);
|
|
194
195
|
let manifest = fresh.manifest;
|
|
195
|
-
// WP-2 review round 1 (P3)
|
|
196
|
-
//
|
|
197
|
-
//
|
|
196
|
+
// WP-2 review round 1 (P3) + M4-R1(b) (fix-wave 6 residual): the ask-timeout
|
|
197
|
+
// requeue ended ONE park — scoped-forget the slot by RE-POINTING it at a
|
|
198
|
+
// surviving parked task (latest deadline wins, the wait.resolve M3-6
|
|
199
|
+
// invariant) instead of clearing it: a third task may still be parked and
|
|
200
|
+
// this single slot is its only stale-repair shield (isWaitAnswerPending
|
|
201
|
+
// reads only this field). Cleared only when no park is left — which also
|
|
202
|
+
// retires the stale-pointer-shields-the-run issue this guard was added for.
|
|
198
203
|
if (manifest.waitState && requeuedTaskIds.includes(manifest.waitState.taskId)) {
|
|
199
|
-
|
|
204
|
+
const waitState = repointWaitState(tasks, manifest.waitState, (candidateId) => requeuedTaskIds.includes(candidateId));
|
|
205
|
+
manifest = { ...manifest, waitState, updatedAt: new Date().toISOString() };
|
|
200
206
|
saveRunManifest(manifest);
|
|
201
207
|
}
|
|
202
208
|
if (
|
|
@@ -34,6 +34,17 @@ export function delegateCompletedTaskIdsFromEvents(events: ReadonlyArray<{ type?
|
|
|
34
34
|
return ids;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/** R47-V7 (2026-10-19): a task completed by the deterministic gate-skip path
|
|
38
|
+
* (task-runner.ts pre-gate `skipVerifier` branch) carries a SYNTHESIZED
|
|
39
|
+
* terminal result — a fabricated `exitCode: 0`, a green gate summary as the
|
|
40
|
+
* result artifact/stdout, and no model attempt. task-runner tags that
|
|
41
|
+
* provenance as `diagnostics.synthetic = true`. Unlike the genuine gate
|
|
42
|
+
* verdict (also in diagnostics), this synthesized "completed with exit 0"
|
|
43
|
+
* shape must never be mistaken for real worker output. */
|
|
44
|
+
export function taskIsSyntheticGateSkip(task: TeamTaskState): boolean {
|
|
45
|
+
return task.diagnostics?.synthetic === true;
|
|
46
|
+
}
|
|
47
|
+
|
|
37
48
|
export function taskHasObservableWorkerActivity(task: TeamTaskState): boolean {
|
|
38
49
|
return Boolean(
|
|
39
50
|
(task.agentProgress?.toolCount ?? 0) > 0 ||
|
|
@@ -49,7 +60,13 @@ export function taskHasObservableWorkerActivity(task: TeamTaskState): boolean {
|
|
|
49
60
|
// team_20261010162310): green-gated verifier tasks were false-blocked
|
|
50
61
|
// here ("no observable worker activity") → run stuck non-terminal
|
|
51
62
|
// "blocked" → sync team calls hung forever.
|
|
52
|
-
|
|
63
|
+
//
|
|
64
|
+
// R47-V7 (2026-10-19): the skip-path result is SYNTHETIC provenance —
|
|
65
|
+
// its exit code is fabricated, not a real worker outcome — so a
|
|
66
|
+
// provenance-tagged synthetic task does NOT count as observable work.
|
|
67
|
+
// (The gate verdict stays on diagnostics as genuine evidence; only the
|
|
68
|
+
// synthesized result is excluded.) See {@link taskIsSyntheticGateSkip}.
|
|
69
|
+
(task.diagnostics?.verifyGate !== undefined && !taskIsSyntheticGateSkip(task)),
|
|
53
70
|
);
|
|
54
71
|
}
|
|
55
72
|
|
|
@@ -106,8 +123,19 @@ export function evaluateRunEffectiveness(input: {
|
|
|
106
123
|
let severity: RunEffectivenessSeverity = "ok";
|
|
107
124
|
if (input.executeWorkers && guardMode !== "off" && noObservedWorkTasks.length > 0) {
|
|
108
125
|
severity = guardMode === "fail" ? "failed" : guardMode === "block" ? "blocked" : "warning";
|
|
109
|
-
// P0.1: default warn escalates to blocked for mutating-role tasks without observed work
|
|
110
|
-
|
|
126
|
+
// P0.1: default warn escalates to blocked for mutating-role tasks without observed work.
|
|
127
|
+
// R47-V7 / U13 reconciliation (2026-10-19): a synthetic gate-skip task has
|
|
128
|
+
// no observable worker activity (so it is excluded from the score above, per
|
|
129
|
+
// R47-V7) but it is NOT evidence of an idle/broken run — the host gate really
|
|
130
|
+
// verified the change. Escalating it to "blocked" would re-open the U13
|
|
131
|
+
// false-block → non-terminal run → sync team call hang (live-caught
|
|
132
|
+
// 2026-10-10). Exclude synthetic tasks from the auto-escalation so a
|
|
133
|
+
// gate-skipped verifier (a WRITE role) can never block the run by itself.
|
|
134
|
+
// (An explicit guardMode block/fail still honors the user's setting.)
|
|
135
|
+
if (
|
|
136
|
+
severity === "warning" &&
|
|
137
|
+
noObservedWorkTasks.some((task) => !taskIsSyntheticGateSkip(task) && permissionForRole(task.role) !== "read_only")
|
|
138
|
+
) {
|
|
111
139
|
severity = "blocked";
|
|
112
140
|
}
|
|
113
141
|
}
|
|
@@ -141,7 +141,7 @@ registerPlanTemplate({
|
|
|
141
141
|
name: "verify",
|
|
142
142
|
role: "verifier",
|
|
143
143
|
taskTemplate:
|
|
144
|
-
"Verify that all review findings are addressed. Run FAST checks (completes in <2 min): `npm run test:critical && npx tsc --noEmit`. Do NOT run `npm run test:unit` or `npm test` — too slow (642 files, >4 min). Confirm: {{goal}} is achieved.",
|
|
144
|
+
"Verify that all review findings are addressed. Run FAST checks (completes in <2 min): `npm run test:critical && npx tsc --noEmit`. Do NOT run `npm run test:unit` or `npm test` — too slow (642 files, >4 min). Confirm: {{goal}} is achieved. Start the report with the verdict line `PASS: <what verified> — <evidence>` or `FAIL: <what failed> — <reason>` (the green-contract consumer reads it; no verdict line = UNVERIFIED).",
|
|
145
145
|
maxTasks: 1,
|
|
146
146
|
dependsOn: ["review"],
|
|
147
147
|
verificationCommand: "npm run test:critical && npx tsc --noEmit",
|
|
@@ -189,7 +189,7 @@ registerPlanTemplate({
|
|
|
189
189
|
name: "verify",
|
|
190
190
|
role: "verifier",
|
|
191
191
|
taskTemplate:
|
|
192
|
-
"Verify the complete implementation of: {{goal}}. Run FAST checks (`npm run test:critical && npx tsc --noEmit`, completes in <2 min). Do NOT run `npm run test:unit` or `npm test` — too slow for in-loop verification (642 files, >4 min). Validate all acceptance criteria.",
|
|
192
|
+
"Verify the complete implementation of: {{goal}}. Run FAST checks (`npm run test:critical && npx tsc --noEmit`, completes in <2 min). Do NOT run `npm run test:unit` or `npm test` — too slow for in-loop verification (642 files, >4 min). Validate all acceptance criteria. Start the report with the verdict line `PASS: <what verified> — <evidence>` or `FAIL: <what failed> — <reason>` (the green-contract consumer reads it; no verdict line = UNVERIFIED).",
|
|
193
193
|
maxTasks: 1,
|
|
194
194
|
dependsOn: ["review"],
|
|
195
195
|
verificationCommand: "npm run test:critical && npx tsc --noEmit",
|
|
@@ -22,6 +22,35 @@ export interface LiveAgentControlCursor {
|
|
|
22
22
|
offset: number;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* R41-1 (M3-11) — durable processed marker.
|
|
27
|
+
*
|
|
28
|
+
* `processedAt` used to be set on the IN-MEMORY request object only, and the
|
|
29
|
+
* dedupe that made it meaningful lived in a per-invocation
|
|
30
|
+
* `BoundedSeenControlRequestIdSet` (live-session-runtime.ts). After a crash or
|
|
31
|
+
* a resume, the cursor starts at offset 0 and the seen-set is empty, so EVERY
|
|
32
|
+
* control request already applied was read back and re-applied (stop / steer /
|
|
33
|
+
* follow-up replayed into a fresh session).
|
|
34
|
+
*
|
|
35
|
+
* The fix persists the consumed state as an append-only marker line in the same
|
|
36
|
+
* `live-control.jsonl` file: crash-safe, no rewrite of request lines, no extra
|
|
37
|
+
* file to lock, and it survives a resume with a cold cursor. Marker lines are
|
|
38
|
+
* distinguishable from request lines by `type === "processed"` and are filtered
|
|
39
|
+
* out of every read.
|
|
40
|
+
*/
|
|
41
|
+
export interface LiveAgentControlMarker {
|
|
42
|
+
type: "processed";
|
|
43
|
+
requestId: string;
|
|
44
|
+
runId: string;
|
|
45
|
+
taskId: string;
|
|
46
|
+
processedAt: string;
|
|
47
|
+
error?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isControlMarker(value: unknown): value is LiveAgentControlMarker {
|
|
51
|
+
return Boolean(value && typeof value === "object" && (value as { type?: unknown }).type === "processed");
|
|
52
|
+
}
|
|
53
|
+
|
|
25
54
|
export function liveAgentControlPath(manifest: TeamRunManifest, taskId: string): string {
|
|
26
55
|
return path.join(ensureAgentStateDir(manifest, taskId), "live-control.jsonl");
|
|
27
56
|
}
|
|
@@ -71,14 +100,31 @@ export function readLiveAgentControlRequests(
|
|
|
71
100
|
if (!fs.existsSync(filePath)) return { requests: [], cursor };
|
|
72
101
|
const text = fs.readFileSync(filePath, "utf-8");
|
|
73
102
|
const lines = text.split(/\r?\n/).filter(Boolean);
|
|
74
|
-
const
|
|
103
|
+
const records = lines.map((line) => {
|
|
75
104
|
try {
|
|
76
|
-
|
|
77
|
-
return parsed && parsed.runId === manifest.runId && parsed.taskId === taskId ? [parsed] : [];
|
|
105
|
+
return JSON.parse(line) as unknown;
|
|
78
106
|
} catch {
|
|
79
|
-
return
|
|
107
|
+
return undefined; // unparseable line — skip (same tolerance as before)
|
|
80
108
|
}
|
|
81
109
|
});
|
|
110
|
+
// R41-1: markers may appear anywhere in the file (appended by any process),
|
|
111
|
+
// so the processed set is scanned over the WHOLE file — not just the unread
|
|
112
|
+
// window — before the cursor slice is filtered. This is what makes a cold
|
|
113
|
+
// cursor after crash/resume replay-free.
|
|
114
|
+
const processedRequestIds = new Set<string>();
|
|
115
|
+
for (const record of records) {
|
|
116
|
+
if (!isControlMarker(record)) continue;
|
|
117
|
+
if (record.runId !== manifest.runId || record.taskId !== taskId) continue;
|
|
118
|
+
processedRequestIds.add(record.requestId);
|
|
119
|
+
}
|
|
120
|
+
const requests = records.slice(cursor.offset).flatMap((record) => {
|
|
121
|
+
if (!record || isControlMarker(record)) return [];
|
|
122
|
+
const parsed = record as LiveAgentControlRequest;
|
|
123
|
+
if (parsed.runId !== manifest.runId || parsed.taskId !== taskId) return [];
|
|
124
|
+
// Already consumed in an earlier process (persisted marker) — never replay.
|
|
125
|
+
if (processedRequestIds.has(parsed.id)) return [];
|
|
126
|
+
return [parsed];
|
|
127
|
+
});
|
|
82
128
|
return { requests, cursor: { offset: lines.length } };
|
|
83
129
|
}
|
|
84
130
|
|
|
@@ -92,6 +138,8 @@ export async function applyLiveAgentControlRequest(input: {
|
|
|
92
138
|
abort?: () => Promise<void> | void;
|
|
93
139
|
};
|
|
94
140
|
seenRequestIds?: Set<string>;
|
|
141
|
+
/** R41-1: when supplied, a processed marker is durably appended after the request is consumed. */
|
|
142
|
+
manifest?: TeamRunManifest;
|
|
95
143
|
}): Promise<boolean> {
|
|
96
144
|
const { request, taskId, agentId, session, seenRequestIds } = input;
|
|
97
145
|
if (seenRequestIds?.has(request.id)) return false;
|
|
@@ -130,9 +178,34 @@ export async function applyLiveAgentControlRequest(input: {
|
|
|
130
178
|
`runId=${request.runId} taskId=${request.taskId} operation=${request.operation} requestId=${request.id}`,
|
|
131
179
|
);
|
|
132
180
|
}
|
|
181
|
+
// R41-1: persist the consumed state BEFORE anyone can observe "applied" — a
|
|
182
|
+
// crash right after this return must not replay the request.
|
|
183
|
+
if (input.manifest) appendProcessedControlMarker(input.manifest, taskId, request);
|
|
133
184
|
return true;
|
|
134
185
|
}
|
|
135
186
|
|
|
187
|
+
/**
|
|
188
|
+
* R41-1: appends a processed marker for a request this process consumed.
|
|
189
|
+
* Best-effort — a failed marker write must never fail the control loop (the
|
|
190
|
+
* in-memory seen-set still dedupes within this process); it only means the
|
|
191
|
+
* request may replay once after a crash.
|
|
192
|
+
*/
|
|
193
|
+
function appendProcessedControlMarker(manifest: TeamRunManifest, taskId: string, request: LiveAgentControlRequest): void {
|
|
194
|
+
const marker: LiveAgentControlMarker = {
|
|
195
|
+
type: "processed",
|
|
196
|
+
requestId: request.id,
|
|
197
|
+
runId: manifest.runId,
|
|
198
|
+
taskId,
|
|
199
|
+
processedAt: request.processedAt ?? new Date().toISOString(),
|
|
200
|
+
...(request.error ? { error: request.error } : {}),
|
|
201
|
+
};
|
|
202
|
+
try {
|
|
203
|
+
fs.appendFileSync(liveAgentControlFile(manifest, taskId), `${JSON.stringify(marker)}\n`, "utf-8");
|
|
204
|
+
} catch (error) {
|
|
205
|
+
logInternalError("live-agent-control.mark-processed", error, `taskId=${taskId} requestId=${request.id}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
136
209
|
export async function applyLiveAgentControlRequests(input: {
|
|
137
210
|
manifest: TeamRunManifest;
|
|
138
211
|
taskId: string;
|
|
@@ -146,13 +219,23 @@ export async function applyLiveAgentControlRequests(input: {
|
|
|
146
219
|
seenRequestIds?: Set<string>;
|
|
147
220
|
}): Promise<LiveAgentControlCursor> {
|
|
148
221
|
const batch = readLiveAgentControlRequests(input.manifest, input.taskId, input.cursor);
|
|
149
|
-
for (const request of batch.requests)
|
|
150
|
-
await applyLiveAgentControlRequest({
|
|
222
|
+
for (const request of batch.requests) {
|
|
223
|
+
const applied = await applyLiveAgentControlRequest({
|
|
151
224
|
request,
|
|
152
225
|
taskId: input.taskId,
|
|
153
226
|
agentId: input.agentId,
|
|
154
227
|
session: input.session,
|
|
155
228
|
seenRequestIds: input.seenRequestIds,
|
|
229
|
+
manifest: input.manifest,
|
|
156
230
|
});
|
|
231
|
+
// R41-1: the realtime subscription (live-session-runtime) consumes an
|
|
232
|
+
// already-persisted request WITHOUT a manifest, so it cannot write the
|
|
233
|
+
// marker itself. The poll sees the id in the shared seen-set on its next
|
|
234
|
+
// tick — mark it consumed here so a crash before then cannot replay it.
|
|
235
|
+
// A request addressed to a DIFFERENT agent is never marked (it is not ours
|
|
236
|
+
// to consume): applyLiveAgentControlRequest returns false for it without
|
|
237
|
+
// touching the seen-set.
|
|
238
|
+
if (!applied && input.seenRequestIds?.has(request.id)) appendProcessedControlMarker(input.manifest, input.taskId, request);
|
|
239
|
+
}
|
|
157
240
|
return batch.cursor;
|
|
158
241
|
}
|
|
@@ -84,6 +84,50 @@ const liveAgents = new Map<string, LiveAgentHandle>();
|
|
|
84
84
|
// warning) to keep memory bounded.
|
|
85
85
|
const MAX_LIVE_AGENTS = 5_000;
|
|
86
86
|
|
|
87
|
+
/**
|
|
88
|
+
* R41-4 (M3-12): hard deadline for `session.abort()`.
|
|
89
|
+
*
|
|
90
|
+
* terminateLiveAgent awaited abort() with NO deadline: a session whose abort
|
|
91
|
+
* never settles (hung tool / provider holding the turn) deferred the whole
|
|
92
|
+
* teardown (dispose + registry removal + worker kill) forever, so the record
|
|
93
|
+
* stayed "running" and the process leaked. Mirrors the in-file
|
|
94
|
+
* Promise.race-with-timeout pattern in live-session-runtime.ts
|
|
95
|
+
* (bindExtensions 30s / control 30s).
|
|
96
|
+
*/
|
|
97
|
+
const LIVE_AGENT_ABORT_DEADLINE_MS = 10_000;
|
|
98
|
+
|
|
99
|
+
async function abortLiveSessionWithDeadline(handle: LiveAgentHandle, deadlineMs: number = LIVE_AGENT_ABORT_DEADLINE_MS): Promise<void> {
|
|
100
|
+
const abort = handle.session.abort;
|
|
101
|
+
if (typeof abort !== "function") return;
|
|
102
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
103
|
+
const deadline = new Promise<void>((resolve) => {
|
|
104
|
+
timer = setTimeout(() => {
|
|
105
|
+
logInternalError(
|
|
106
|
+
"live-agent-manager.abort-timeout",
|
|
107
|
+
new Error(`session.abort() did not settle within ${deadlineMs}ms`),
|
|
108
|
+
`agentId=${handle.agentId}`,
|
|
109
|
+
);
|
|
110
|
+
resolve();
|
|
111
|
+
}, deadlineMs);
|
|
112
|
+
// NOTE: deliberately NOT unref'd. A ref'd timer is what lets the deadline
|
|
113
|
+
// actually fire when nothing else keeps the event loop alive (an unref'd
|
|
114
|
+
// deadline on an awaited race can never settle — the RR-021 unref
|
|
115
|
+
// incident). The wait is bounded by deadlineMs, so it cannot leak.
|
|
116
|
+
});
|
|
117
|
+
try {
|
|
118
|
+
// The abort promise is left to settle on its own after the deadline;
|
|
119
|
+
// only the RACE is bounded (teardown must not be deferred).
|
|
120
|
+
await Promise.race([Promise.resolve(abort.call(handle.session)), deadline]);
|
|
121
|
+
} finally {
|
|
122
|
+
if (timer) clearTimeout(timer);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** @internal — test seam for the R41-4 abort deadline (see live-agent-manager-eviction tests). */
|
|
127
|
+
export const __test__abortLiveSessionWithDeadline = abortLiveSessionWithDeadline;
|
|
128
|
+
/** @internal — the production abort deadline, exposed so tests can assert its magnitude. */
|
|
129
|
+
export const __test__LIVE_AGENT_ABORT_DEADLINE_MS = LIVE_AGENT_ABORT_DEADLINE_MS;
|
|
130
|
+
|
|
87
131
|
/**
|
|
88
132
|
* List all live agents for a specific workspace.
|
|
89
133
|
* Only agents belonging to the given workspaceId are returned.
|
|
@@ -128,17 +172,23 @@ export function registerLiveAgent(
|
|
|
128
172
|
// evict the oldest completed agent (already finished, so caller no
|
|
129
173
|
// longer needs it). If none exist, evict the oldest running one with
|
|
130
174
|
// a warning so memory stays bounded.
|
|
175
|
+
// R41-4 (M3-12): eviction used to be a bare map.delete — the evicted
|
|
176
|
+
// session was never aborted/disposed, and the cap could therefore drop a
|
|
177
|
+
// RUNNING record while its session kept executing (an untracked live
|
|
178
|
+
// worker). Both branches now go through evictLiveAgentForCap, which
|
|
179
|
+
// initiates abort() (bounded by a deadline) and disposes before removing
|
|
180
|
+
// the record. Also: "oldest completed" is now picked by completedAtMs, not
|
|
181
|
+
// by insertion order (Map order says nothing about completion time).
|
|
131
182
|
if (liveAgents.size >= MAX_LIVE_AGENTS) {
|
|
132
|
-
const completed = [...liveAgents.entries()]
|
|
183
|
+
const completed = [...liveAgents.entries()]
|
|
184
|
+
.filter(([, h]) => h.activity.completedAtMs > 0)
|
|
185
|
+
.sort((a, b) => a[1].activity.completedAtMs - b[1].activity.completedAtMs)[0];
|
|
133
186
|
if (completed) {
|
|
134
|
-
|
|
135
|
-
invalidateSortedLiveAgents();
|
|
187
|
+
evictLiveAgentForCap(completed[0], completed[1], `liveAgents at cap ${MAX_LIVE_AGENTS}`);
|
|
136
188
|
} else {
|
|
137
|
-
const
|
|
138
|
-
if (
|
|
139
|
-
|
|
140
|
-
liveAgents.delete(oldestKey);
|
|
141
|
-
invalidateSortedLiveAgents();
|
|
189
|
+
const oldest = liveAgents.entries().next().value;
|
|
190
|
+
if (oldest) {
|
|
191
|
+
evictLiveAgentForCap(oldest[0], oldest[1], `liveAgents at cap ${MAX_LIVE_AGENTS} with no completed agent`);
|
|
142
192
|
}
|
|
143
193
|
}
|
|
144
194
|
}
|
|
@@ -259,6 +309,33 @@ function markLiveAgentHandleTerminated(handle: LiveAgentHandle): void {
|
|
|
259
309
|
invalidateSortedLiveAgents();
|
|
260
310
|
}
|
|
261
311
|
|
|
312
|
+
/**
|
|
313
|
+
* R41-4 (M3-12): cap eviction = abort (deadline-bounded) + dispose + delete.
|
|
314
|
+
*
|
|
315
|
+
* Order matters: abort() is INITIATED first (it is the only channel that
|
|
316
|
+
* cancels an in-flight turn), then the handle is marked terminal, the session
|
|
317
|
+
* disposed and the record removed. registerLiveAgent is synchronous, so the
|
|
318
|
+
* abort promise itself cannot be awaited — it runs fire-and-forget through
|
|
319
|
+
* abortLiveSessionWithDeadline, so a never-settling abort cannot keep the
|
|
320
|
+
* session alive past the deadline (and never blocks the registration).
|
|
321
|
+
*
|
|
322
|
+
* The delete is identity-checked: only the exact handle being evicted is
|
|
323
|
+
* removed, so a handle re-registered for the same agentId can never be
|
|
324
|
+
* dropped by this path.
|
|
325
|
+
*/
|
|
326
|
+
function evictLiveAgentForCap(agentId: string, handle: LiveAgentHandle, reason: string): void {
|
|
327
|
+
logInternalError("live-agent-manager.cap", new Error(`${reason}; evicting ${agentId}`));
|
|
328
|
+
const aborting = abortLiveSessionWithDeadline(handle).catch((error) => {
|
|
329
|
+
logInternalError("live-agent-manager.cap-abort", error, `agentId=${agentId}`);
|
|
330
|
+
});
|
|
331
|
+
markLiveAgentHandleTerminated(handle);
|
|
332
|
+
safeDisposeLiveSession(handle);
|
|
333
|
+
if (liveAgents.get(agentId) === handle) liveAgents.delete(agentId);
|
|
334
|
+
invalidateSortedLiveAgents();
|
|
335
|
+
// Fire-and-forget by design — see docblock. Keeps the timer promise observed.
|
|
336
|
+
void aborting;
|
|
337
|
+
}
|
|
338
|
+
|
|
262
339
|
/**
|
|
263
340
|
* U4 hung-tool escape hatch — drop-reference.
|
|
264
341
|
*
|
|
@@ -308,11 +385,18 @@ export async function terminateLiveAgent(
|
|
|
308
385
|
/* non-critical */
|
|
309
386
|
}
|
|
310
387
|
try {
|
|
311
|
-
|
|
388
|
+
// R41-4 (M3-12): abort must not defer teardown — a hung abort() used to
|
|
389
|
+
// leave the handle registered as "running" forever. Bounded by
|
|
390
|
+
// LIVE_AGENT_ABORT_DEADLINE_MS; teardown runs either way (finally).
|
|
391
|
+
await abortLiveSessionWithDeadline(handle);
|
|
312
392
|
} finally {
|
|
313
393
|
markLiveAgentHandleTerminated(handle);
|
|
314
394
|
safeDisposeLiveSession(handle);
|
|
315
|
-
|
|
395
|
+
// Move AFTER abort completes to prevent race — and identity-check the
|
|
396
|
+
// entry so a handle re-registered for the same agentId during the
|
|
397
|
+
// bounded abort is not removed (R41-4 sibling invariant to the cap
|
|
398
|
+
// eviction path).
|
|
399
|
+
if (liveAgents.get(handle.agentId) === handle) liveAgents.delete(handle.agentId);
|
|
316
400
|
invalidateSortedLiveAgents();
|
|
317
401
|
// WI-5 (SDD-4): if the session still backs a live worker process that
|
|
318
402
|
// abort()/dispose() failed to stop, signal it last so terminate cannot
|
|
@@ -892,6 +892,13 @@ export async function runLiveSessionTask(input: LiveSessionSpawnInput): Promise<
|
|
|
892
892
|
const agentId = `${input.manifest.runId}:${input.task.id}`;
|
|
893
893
|
const inherited =
|
|
894
894
|
input.runtimeConfig?.inheritContext === true && input.parentContext ? ` with inherited context: ${input.parentContext}` : "";
|
|
895
|
+
// M3-2 (R47-3/V4): the verifier output contract requires a PASS:/FAIL:
|
|
896
|
+
// verdict line; the mock must emit contract-shaped output for verifier-role
|
|
897
|
+
// tasks or the (now real) verdict consumer marks the evidence unverified
|
|
898
|
+
// and the green contract blocks the run (fail-closed — correct for real
|
|
899
|
+
// verifiers, wrong for the success mock).
|
|
900
|
+
const mockVerdictSuffix = input.task.role === "verifier" ? "\nPASS: mock verifier — mock live-session success" : "";
|
|
901
|
+
const mockText = `Mock live-session success for ${input.agent.name}${inherited}${mockVerdictSuffix}`;
|
|
895
902
|
const event = {
|
|
896
903
|
type: "message_end",
|
|
897
904
|
message: {
|
|
@@ -899,7 +906,7 @@ export async function runLiveSessionTask(input: LiveSessionSpawnInput): Promise<
|
|
|
899
906
|
content: [
|
|
900
907
|
{
|
|
901
908
|
type: "text",
|
|
902
|
-
text:
|
|
909
|
+
text: mockText,
|
|
903
910
|
},
|
|
904
911
|
],
|
|
905
912
|
},
|
|
@@ -945,7 +952,7 @@ export async function runLiveSessionTask(input: LiveSessionSpawnInput): Promise<
|
|
|
945
952
|
// the batched writers here — tests read these files right after return.
|
|
946
953
|
flushPendingSidechainWrites();
|
|
947
954
|
if (isCurrent()) input.onEvent?.(event);
|
|
948
|
-
const stdout =
|
|
955
|
+
const stdout = mockText;
|
|
949
956
|
if (isCurrent()) input.onOutput?.(stdout);
|
|
950
957
|
updateLiveAgentStatus(agentId, "completed");
|
|
951
958
|
markLiveAgentCompleted(agentId);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
+
import { getCrewEnv } from "../../config/env-vars.ts";
|
|
4
5
|
import { errors } from "../../errors.ts";
|
|
5
6
|
import { logInternalError } from "../../utils/internal-error.ts";
|
|
6
7
|
import { fuzzyResolveModelId } from "./model-resolver.ts";
|
|
@@ -646,7 +647,7 @@ export function resolveModelFallbackPolicy(
|
|
|
646
647
|
| undefined,
|
|
647
648
|
env: NodeJS.ProcessEnv = process.env,
|
|
648
649
|
): ModelFallbackPolicy | undefined {
|
|
649
|
-
const envMaxAuto = env
|
|
650
|
+
const envMaxAuto = getCrewEnv("PI_CREW_MAX_AUTO_FALLBACKS", env);
|
|
650
651
|
let maxAutoFallbacks: number | undefined;
|
|
651
652
|
if (envMaxAuto) {
|
|
652
653
|
const parsed = Number.parseInt(envMaxAuto, 10);
|
|
@@ -680,7 +681,7 @@ export function resolveModelFallbackPolicy(
|
|
|
680
681
|
// "parent-first") silently behaved as parentFirst. Invalid → warn + fall
|
|
681
682
|
// back to config/default, mirroring the PI_CREW_MAX_AUTO_FALLBACKS
|
|
682
683
|
// clamp-warn sibling pattern above.
|
|
683
|
-
const envOrder = env
|
|
684
|
+
const envOrder = getCrewEnv("PI_CREW_MODEL_FALLBACK_ORDER", env);
|
|
684
685
|
let order: "parentFirst" | "asIs" | undefined;
|
|
685
686
|
if (envOrder) {
|
|
686
687
|
if (envOrder === "parentFirst" || envOrder === "asIs") {
|
|
@@ -697,12 +698,8 @@ export function resolveModelFallbackPolicy(
|
|
|
697
698
|
} else {
|
|
698
699
|
order = config?.order;
|
|
699
700
|
}
|
|
700
|
-
const
|
|
701
|
-
|
|
702
|
-
? true
|
|
703
|
-
: env.PI_CREW_MODEL_REQUIRE_CREDENTIALS === "0"
|
|
704
|
-
? false
|
|
705
|
-
: config?.requireCredentials;
|
|
701
|
+
const envRequireCredentials = getCrewEnv("PI_CREW_MODEL_REQUIRE_CREDENTIALS", env);
|
|
702
|
+
const requireCredentials = envRequireCredentials === "1" ? true : envRequireCredentials === "0" ? false : config?.requireCredentials;
|
|
706
703
|
// quotaAwareOrdering defaults to true (user preference: default-on with cache).
|
|
707
704
|
// When explicitly disabled, no deprioritization/rank data is attached.
|
|
708
705
|
const quotaAware = config?.quotaAwareOrdering !== false;
|
|
@@ -725,7 +722,7 @@ export function resolveDefaultSubagentModel(
|
|
|
725
722
|
config: { defaultSubagentModel?: string } | undefined,
|
|
726
723
|
env: NodeJS.ProcessEnv = process.env,
|
|
727
724
|
): string | undefined {
|
|
728
|
-
return env
|
|
725
|
+
return getCrewEnv("PI_CREW_MODEL", env)?.trim() || config?.defaultSubagentModel?.trim() || undefined;
|
|
729
726
|
}
|
|
730
727
|
|
|
731
728
|
export interface ConfiguredModelRouting {
|
|
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import type { AgentConfig } from "../../agents/agent-config.ts";
|
|
5
|
-
import { getCrewEnvBool } from "../../config/env-vars.ts";
|
|
5
|
+
import { getCrewEnv, getCrewEnvBool } from "../../config/env-vars.ts";
|
|
6
6
|
import { atomicWriteFile } from "../../state/atomic-write.ts";
|
|
7
7
|
import { hasRunStateLayout, packageRoot, userPiRoot } from "../../utils/paths.ts";
|
|
8
8
|
import { PI_NO_MCP_FLOOR, piVersionAtLeast } from "../child-pi/pi-version.ts";
|
|
@@ -131,13 +131,13 @@ export function applyThinkingSuffix(model: string | undefined, thinking: string
|
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
export function currentCrewDepth(env: NodeJS.ProcessEnv = process.env): number {
|
|
134
|
-
const raw =
|
|
134
|
+
const raw = getCrewEnv("PI_CREW_DEPTH", env) ?? "0";
|
|
135
135
|
const parsed = Number(raw);
|
|
136
136
|
return Number.isInteger(parsed) && parsed >= 0 ? parsed : 0;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
export function resolveCrewMaxDepth(inputMaxDepth?: number, env: NodeJS.ProcessEnv = process.env): number {
|
|
140
|
-
const raw =
|
|
140
|
+
const raw = getCrewEnv("PI_CREW_MAX_DEPTH", env);
|
|
141
141
|
const envDepth = raw !== undefined ? Number(raw) : NaN;
|
|
142
142
|
if (Number.isInteger(envDepth) && envDepth >= 1 && envDepth <= 10) return envDepth;
|
|
143
143
|
if (Number.isInteger(envDepth) && envDepth > 10) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CrewRuntimeConfig } from "../../config/config.ts";
|
|
2
|
+
import { getCrewEnv } from "../../config/env-vars.ts";
|
|
2
3
|
import type { CrewRuntimeKind } from "../crew-agent-runtime.ts";
|
|
3
4
|
import { currentCrewDepth } from "./pi-args.ts";
|
|
4
5
|
|
|
@@ -51,8 +52,9 @@ export function resolveEmbeddedFaultThreshold(policy: CrewRuntimeConfig["embedde
|
|
|
51
52
|
* set, and PI_CREW_ALLOW_MOCK is filtered from child envs.
|
|
52
53
|
*/
|
|
53
54
|
export function mockLiveSessionEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
54
|
-
if (
|
|
55
|
-
|
|
55
|
+
if (getCrewEnv("PI_CREW_MOCK_LIVE_SESSION", env) !== "success") return false;
|
|
56
|
+
const allowMock = getCrewEnv("PI_CREW_ALLOW_MOCK", env);
|
|
57
|
+
return allowMock === "1" || allowMock === "true";
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
export function resolveTaskRuntimeKind(
|