pi-crew 0.10.2 → 0.10.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +2 -1
- package/CHANGELOG.md +249 -0
- package/README.md +5 -1
- package/dist/index.mjs +10844 -7250
- package/docs/architecture.md +4 -4
- package/docs/commands-reference.md +3 -0
- package/docs/publishing.md +15 -3
- package/install.mjs +90 -39
- package/package.json +9 -3
- package/schema.json +11 -0
- package/scripts/README.md +4 -3
- package/skills/real-test-pi-crew/REPORT-TEMPLATE.md +7 -2
- package/skills/real-test-pi-crew/SKILL.md +428 -82
- package/src/config/config-merge.ts +11 -1
- package/src/config/config-validation.ts +40 -1
- package/src/config/config.ts +28 -6
- package/src/config/defaults.ts +35 -10
- package/src/config/env-vars.ts +27 -2
- package/src/config/migration-validator.ts +113 -0
- package/src/config/types.ts +36 -0
- package/src/extension/cross-extension-rpc.ts +3 -7
- package/src/extension/register.ts +13 -0
- package/src/extension/registration/lifecycle-handlers.ts +40 -9
- package/src/extension/registration/observability.ts +3 -7
- package/src/extension/registration/subagent-tools.ts +3 -7
- package/src/extension/registration/team-tool.ts +56 -12
- package/src/extension/registration/ui.ts +3 -8
- package/src/extension/registration/viewers.ts +3 -10
- package/src/extension/team-manager-command.ts +3 -7
- package/src/extension/team-tool/api/agent-control.ts +17 -10
- package/src/extension/team-tool/api/heartbeat.ts +4 -3
- package/src/extension/team-tool/api/mailbox.ts +33 -20
- package/src/extension/team-tool/api/plan-approval.ts +5 -5
- package/src/extension/team-tool/api/task-claims.ts +8 -7
- package/src/extension/team-tool/cancel.ts +6 -0
- package/src/extension/team-tool/doctor.ts +364 -7
- package/src/extension/team-tool/handle-settings.ts +23 -1
- package/src/extension/team-tool/inspect.ts +10 -2
- package/src/extension/team-tool/run.ts +3 -7
- package/src/extension/team-tool/status.ts +12 -0
- package/src/extension/team-tool.ts +41 -16
- package/src/hooks/registry.ts +62 -56
- package/src/prompt/inbox-poll.ts +90 -0
- package/src/prompt/message-tool.ts +166 -0
- package/src/prompt/prompt-runtime.ts +201 -18
- package/src/prompt/scratchpad-lifecycle.ts +3 -3
- package/src/prompt/surface-worker.ts +720 -0
- package/src/prompt/worker-events-channel.ts +49 -3
- package/src/runtime/async-runner.ts +29 -1
- package/src/runtime/background-runner.ts +43 -42
- package/src/runtime/broker/broker-issuer.ts +27 -2
- package/src/runtime/broker/crew-broker-tokens.ts +56 -4
- package/src/runtime/broker/crew-broker.ts +334 -443
- package/src/runtime/broker/delegate/delegate-event.ts +37 -0
- package/src/runtime/broker/mailbox-observer/mailbox-fanout.ts +59 -0
- package/src/runtime/broker/protocol/connection-state.ts +103 -0
- package/src/runtime/broker/protocol/events-replay.ts +68 -0
- package/src/runtime/broker/protocol/manifest-loader.ts +20 -0
- package/src/runtime/broker/protocol/msg-inbox.ts +69 -0
- package/src/runtime/broker/protocol/request-parsers.ts +175 -0
- package/src/runtime/broker/protocol/wait-auth.ts +46 -0
- package/src/runtime/child-pi/child-pi-spawn.ts +23 -9
- package/src/runtime/child-pi/child-pi-streams.ts +9 -1
- package/src/runtime/child-pi/child-pi.ts +368 -5
- package/src/runtime/crew-agent-records.ts +13 -1
- package/src/runtime/dispatch-batch.ts +12 -1
- package/src/runtime/event-log-tail-source.ts +374 -0
- package/src/runtime/finalize-run.ts +19 -7
- package/src/runtime/foreground-control.ts +19 -6
- package/src/runtime/goal-workflow/dynamic-workflow-context.ts +6 -0
- package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +3 -0
- package/src/runtime/goal-workflow/goal-loop-runner.ts +29 -27
- package/src/runtime/goal-workflow/goal-state-store.ts +3 -0
- package/src/runtime/heartbeat/heartbeat-watcher.ts +3 -3
- package/src/runtime/live-session/live-agent-manager.ts +34 -1
- package/src/runtime/live-session/live-control-realtime.ts +10 -0
- package/src/runtime/live-session/live-session-runtime.ts +47 -27
- package/src/runtime/manifest-cache.ts +128 -17
- package/src/runtime/model/pi-args.ts +59 -65
- package/src/runtime/output/sidechain-output.ts +61 -6
- package/src/runtime/plan-replan.ts +3 -0
- package/src/runtime/process/proc-stat.ts +46 -0
- package/src/runtime/process/zombie-scanner.ts +32 -19
- package/src/runtime/spawn-policy.ts +27 -41
- package/src/runtime/stale-reconciler.ts +28 -3
- package/src/runtime/supervisor-contact.ts +3 -0
- package/src/runtime/surface/degrade.ts +776 -0
- package/src/runtime/surface/herdr-provider.ts +546 -0
- package/src/runtime/surface/launch-script.ts +172 -0
- package/src/runtime/surface/resolve-surface.ts +274 -0
- package/src/runtime/surface/surface-provider.ts +129 -0
- package/src/runtime/surface/surface-spawn.ts +475 -0
- package/src/runtime/surface/tmux-provider.ts +400 -0
- package/src/runtime/task-runner/child-executor.ts +80 -0
- package/src/runtime/task-runner/post-execution.ts +57 -2
- package/src/runtime/task-runner/prompt-builder.ts +1 -0
- package/src/runtime/task-runner/retrieval-orchestrator.ts +191 -56
- package/src/runtime/task-runner/state-helpers.ts +54 -30
- package/src/runtime/task-runner.ts +4 -2
- package/src/runtime/team-runner.ts +104 -3
- package/src/schema/config-schema.ts +24 -0
- package/src/state/atomic-write.ts +219 -40
- package/src/state/coordination/locks.ts +7 -5
- package/src/state/coordination/mailbox.ts +56 -10
- package/src/state/event-log/cursor.ts +413 -23
- package/src/state/event-log/event-log.ts +120 -113
- package/src/state/event-log/sequence-cache.ts +21 -3
- package/src/state/stores/ownership-map.ts +5 -4
- package/src/state/stores/plan-store.ts +12 -0
- package/src/state/stores/state-store.ts +103 -6
- package/src/state/types.ts +51 -0
- package/src/ui/inline-panel/agent-pane.ts +3 -0
- package/src/ui/powerbar-publisher.ts +3 -7
- package/src/ui/render-diff.ts +16 -8
- package/src/ui/run-action-dispatcher.ts +7 -10
- package/src/ui/run-dashboard.ts +87 -42
- package/src/ui/run-event-bus.ts +10 -1
- package/src/ui/run-snapshot-cache.ts +83 -35
- package/src/ui/settings-overlay.ts +4 -1
- package/src/ui/transcript-cache.ts +101 -13
- package/src/ui/transcript-viewer.ts +92 -24
- package/src/ui/widget/index.ts +32 -8
- package/src/utils/visual.ts +43 -0
- package/src/worktree/worktree-manager.ts +65 -4
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import * as fs from "node:fs";
|
|
26
|
+
import { fieldsAfterComm, PROC_STAT_PPID_INDEX, PROC_STAT_STARTTIME_INDEX } from "./proc-stat.ts";
|
|
26
27
|
|
|
27
28
|
export interface ZombieSubagent {
|
|
28
29
|
pid: number;
|
|
@@ -32,6 +33,15 @@ export interface ZombieSubagent {
|
|
|
32
33
|
/** Whether the recorded crew parent PID is still alive. */
|
|
33
34
|
parentAlive: boolean;
|
|
34
35
|
role: string | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* Mux kind from PI_CREW_SURFACE ("tmux"|"herdr") when the worker booted in
|
|
38
|
+
* a surface pane (T12). Surface workers have NO heartbeat (T9 handoff) —
|
|
39
|
+
* env markers are the only identity signal, so the scanner never gates a
|
|
40
|
+
* surface entry on liveness telemetry.
|
|
41
|
+
*/
|
|
42
|
+
surface?: "tmux" | "herdr";
|
|
43
|
+
/** Pane id from PI_CREW_SURFACE_PANE — doctor closes orphan panes by it. */
|
|
44
|
+
surfacePaneId?: string;
|
|
35
45
|
rssKb: number;
|
|
36
46
|
elapsedSec: number | undefined;
|
|
37
47
|
cmd: string;
|
|
@@ -66,17 +76,12 @@ function readProcStat(pid: number): { ppid: number; elapsedSec: number | undefin
|
|
|
66
76
|
try {
|
|
67
77
|
const stat = fs.readFileSync(`/proc/${pid}/stat`, "utf-8");
|
|
68
78
|
// stat format: pid (comm) state ppid ... starttime ...
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
.split(/\s+/);
|
|
76
|
-
// rest[0] = state, rest[1] = ppid
|
|
77
|
-
const ppid = Number.parseInt(rest[1] ?? "", 10);
|
|
78
|
-
// starttime (clock ticks since boot) is field 22 in the full stat → index 19 in `rest`
|
|
79
|
-
const starttimeTicksRaw = Number.parseInt(rest[19] ?? "", 10);
|
|
79
|
+
// Shared paren-aware cut (proc-stat.ts) — MUST index identically with
|
|
80
|
+
// surface-spawn's PI_CREW_PARENT_START_TIME capture.
|
|
81
|
+
const rest = fieldsAfterComm(stat);
|
|
82
|
+
if (!rest) return undefined;
|
|
83
|
+
const ppid = Number.parseInt(rest[PROC_STAT_PPID_INDEX] ?? "", 10);
|
|
84
|
+
const starttimeTicksRaw = Number.parseInt(rest[PROC_STAT_STARTTIME_INDEX] ?? "", 10);
|
|
80
85
|
const starttimeTicks = Number.isFinite(starttimeTicksRaw) ? starttimeTicksRaw : undefined;
|
|
81
86
|
const elapsedSec = computeElapsedSec(starttimeTicks);
|
|
82
87
|
return { ppid: Number.isFinite(ppid) ? ppid : 0, elapsedSec };
|
|
@@ -184,12 +189,17 @@ export function scanZombieSubagents(): ZombieScanResult {
|
|
|
184
189
|
|
|
185
190
|
const crewParentPid = Number.parseInt(environ.PI_CREW_PARENT_PID ?? "", 10);
|
|
186
191
|
const stat = readProcStat(pid);
|
|
192
|
+
// Surface identity (T12): strict allowlist — an unrelated PI_CREW_SURFACE
|
|
193
|
+
// value must not half-populate the fields doctor relies on for pane close.
|
|
194
|
+
const surface = environ.PI_CREW_SURFACE === "tmux" || environ.PI_CREW_SURFACE === "herdr" ? environ.PI_CREW_SURFACE : undefined;
|
|
187
195
|
const entry: ZombieSubagent = {
|
|
188
196
|
pid,
|
|
189
197
|
ppid: stat?.ppid ?? 0,
|
|
190
198
|
crewParentPid: Number.isFinite(crewParentPid) ? crewParentPid : 0,
|
|
191
199
|
parentAlive: Number.isFinite(crewParentPid) && isPidAlive(crewParentPid),
|
|
192
200
|
role: environ.PI_CREW_ROLE,
|
|
201
|
+
surface,
|
|
202
|
+
surfacePaneId: environ.PI_CREW_SURFACE_PANE || undefined,
|
|
193
203
|
rssKb: readProcRssKb(pid),
|
|
194
204
|
elapsedSec: stat?.elapsedSec,
|
|
195
205
|
cmd: readProcCmdline(pid),
|
|
@@ -256,11 +266,9 @@ export function formatZombieReport(scan: ZombieScanResult): string {
|
|
|
256
266
|
lines.push(`### Zombies — parent dead (${scan.zombies.length})`);
|
|
257
267
|
lines.push("These sub-agents are orphaned. Safe to kill after review:");
|
|
258
268
|
lines.push("");
|
|
259
|
-
lines.push(" PID PARENT RSS ROLE CMD");
|
|
269
|
+
lines.push(" PID PARENT RSS ROLE SURFACE CMD");
|
|
260
270
|
for (const z of scan.zombies) {
|
|
261
|
-
lines.push(
|
|
262
|
-
` ${String(z.pid).padEnd(9)}${String(z.crewParentPid).padEnd(8)}${formatRss(z.rssKb).padEnd(10)}${(z.role ?? "?").padEnd(14)}${z.cmd.slice(0, 60)}`,
|
|
263
|
-
);
|
|
271
|
+
lines.push(formatZombieRow(z));
|
|
264
272
|
}
|
|
265
273
|
lines.push("");
|
|
266
274
|
}
|
|
@@ -269,11 +277,9 @@ export function formatZombieReport(scan: ZombieScanResult): string {
|
|
|
269
277
|
lines.push(`### Live — parent still running (${scan.live.length})`);
|
|
270
278
|
lines.push("NOT zombies. Do not kill (parent PID is alive and may still reap them).");
|
|
271
279
|
lines.push("");
|
|
272
|
-
lines.push(" PID PARENT RSS ROLE CMD");
|
|
280
|
+
lines.push(" PID PARENT RSS ROLE SURFACE CMD");
|
|
273
281
|
for (const l of scan.live) {
|
|
274
|
-
lines.push(
|
|
275
|
-
` ${String(l.pid).padEnd(9)}${String(l.crewParentPid).padEnd(8)}${formatRss(l.rssKb).padEnd(10)}${(l.role ?? "?").padEnd(14)}${l.cmd.slice(0, 60)}`,
|
|
276
|
-
);
|
|
282
|
+
lines.push(formatZombieRow(l));
|
|
277
283
|
}
|
|
278
284
|
lines.push("");
|
|
279
285
|
}
|
|
@@ -288,6 +294,13 @@ export function formatZombieReport(scan: ZombieScanResult): string {
|
|
|
288
294
|
return lines.join("\n");
|
|
289
295
|
}
|
|
290
296
|
|
|
297
|
+
/** One table row — shared by the zombie + live sections so columns stay aligned. */
|
|
298
|
+
function formatZombieRow(z: ZombieSubagent): string {
|
|
299
|
+
// SURFACE = "kind:paneId" (T12) — the pane doctor closes when orphaned.
|
|
300
|
+
const surface = z.surface ? `${z.surface}:${z.surfacePaneId ?? "?"}` : "-";
|
|
301
|
+
return ` ${String(z.pid).padEnd(9)}${String(z.crewParentPid).padEnd(8)}${formatRss(z.rssKb).padEnd(10)}${(z.role ?? "?").padEnd(14)}${surface.padEnd(16)}${z.cmd.slice(0, 44)}`;
|
|
302
|
+
}
|
|
303
|
+
|
|
291
304
|
function formatRss(kb: number): string {
|
|
292
305
|
if (kb >= 1024 * 1024) return `${(kb / 1024 / 1024).toFixed(1)}G`;
|
|
293
306
|
if (kb >= 1024) return `${(kb / 1024).toFixed(0)}M`;
|
|
@@ -8,26 +8,26 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Gate dimensions (each with a distinct machine-readable reason + fail-fast
|
|
10
10
|
* message; evaluated in this order — cheapest/most-global first):
|
|
11
|
-
* 1.
|
|
12
|
-
* 2. role-denied — the PARENT task's role is not executor-class
|
|
13
|
-
* (read-only roles cannot raise spawn privileges),
|
|
14
|
-
* or the requested grandchild role is not one of
|
|
15
|
-
* the delegate-tool's allowed roles
|
|
16
|
-
* 3. trust-denied — untrusted escalation context (manual-only trust
|
|
11
|
+
* 1. trust-denied — untrusted escalation context (manual-only trust
|
|
17
12
|
* gate; the handler resolves trust, policy enforces)
|
|
18
|
-
*
|
|
19
|
-
*
|
|
13
|
+
* 2. role-denied — the REQUESTED grandchild role is not one of the
|
|
14
|
+
* delegate-tool's allowed roles (the parent role is
|
|
15
|
+
* NO LONGER gated — D8: every role may delegate;
|
|
16
|
+
* the spawn-side checkCrewDepth cap is the boundary)
|
|
17
|
+
* 3. depth-exceeded — child depth (parent.depth + 1) exceeds the
|
|
18
|
+
* resolved maxDepth (default 4). Depth comes from
|
|
20
19
|
* the parent task RECORD, never worker env
|
|
21
20
|
* (design §7 rev-2 P0-2)
|
|
22
|
-
*
|
|
21
|
+
* 4. slots-exhausted — nested-slot budget exhausted ("N/M in flight");
|
|
23
22
|
* fail-fast, never queue
|
|
24
|
-
*
|
|
23
|
+
* 5. budget-insufficient— requested budgetTokens exceed the parent task's
|
|
25
24
|
* remaining allocation (tokensGranted - tokensSpent)
|
|
26
|
-
*
|
|
25
|
+
* 6. model-invalid — requested model is not in the resolved model
|
|
27
26
|
* catalog (the unvalidated provider/model
|
|
28
27
|
* pass-through — model-fallback.ts:282, the
|
|
29
28
|
* 429-cascade root — must not be reachable here)
|
|
30
|
-
*
|
|
29
|
+
* 7. timeout-invalid — timeoutSec outside 1..MAX (default 900)
|
|
30
|
+
* 8. workspace-conflict — write-capable grandchild narrowing cwd overlap
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
33
|
import { modelRefToString } from "./model/model-fallback.ts";
|
|
@@ -48,7 +48,6 @@ export const DEFAULT_DELEGATE_TIMEOUT_SEC = 900;
|
|
|
48
48
|
export const MAX_DELEGATE_TIMEOUT_SEC = 86_400;
|
|
49
49
|
|
|
50
50
|
export type SpawnPolicyDenyReason =
|
|
51
|
-
| "nesting-disabled"
|
|
52
51
|
| "role-denied"
|
|
53
52
|
| "trust-denied"
|
|
54
53
|
| "depth-exceeded"
|
|
@@ -59,14 +58,14 @@ export type SpawnPolicyDenyReason =
|
|
|
59
58
|
| "workspace-conflict";
|
|
60
59
|
|
|
61
60
|
export interface DelegateAdmissionInput {
|
|
62
|
-
/** Resolved
|
|
63
|
-
nestingEnabled: boolean;
|
|
64
|
-
/** Resolved max depth (nesting.maxDepth default 2; env clamp mirrors PI_CREW_MAX_DEPTH 1..10). */
|
|
61
|
+
/** Resolved max depth (nesting.maxDepth default 4; env clamp mirrors PI_CREW_MAX_DEPTH 1..10). */
|
|
65
62
|
maxDepth: number;
|
|
66
63
|
/** Parent task record fields the policy is allowed to trust. */
|
|
67
64
|
parentTask: {
|
|
68
65
|
taskId: string;
|
|
69
|
-
|
|
66
|
+
/** Role for workspace-capability evaluation (policy no longer gates the
|
|
67
|
+
* parent role — D8 opens delegation to every role). */
|
|
68
|
+
role?: string;
|
|
70
69
|
/** Absent on pre-v2 records — treated as depth 1 (a worker). */
|
|
71
70
|
depth?: number;
|
|
72
71
|
allocation?: { tokensGranted?: number; tokensSpent?: number };
|
|
@@ -122,26 +121,13 @@ function deny(reason: SpawnPolicyDenyReason, message: string): SpawnPolicyDecisi
|
|
|
122
121
|
}
|
|
123
122
|
|
|
124
123
|
export function evaluateDelegateAdmission(input: DelegateAdmissionInput): SpawnPolicyDecision {
|
|
125
|
-
const {
|
|
124
|
+
const { maxDepth, parentTask, requested } = input;
|
|
126
125
|
const parentDepth = parentTask.depth ?? 1; // pre-v2 records: workers are depth 1
|
|
127
126
|
const childDepth = parentDepth + 1;
|
|
128
127
|
|
|
129
|
-
// 1.
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
"nesting-disabled",
|
|
133
|
-
"delegate rejected: governed nesting is disabled (nesting.enabled=false, the fail-closed default; enable it in USER config)",
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// 2. Role gate — parent must be executor-class; requested role must be in
|
|
138
|
-
// the tool's allowed set.
|
|
139
|
-
if (!isExecutorClassRole(parentTask.role)) {
|
|
140
|
-
return deny(
|
|
141
|
-
"role-denied",
|
|
142
|
-
`delegate rejected: role '${parentTask.role}' is not executor-class (${EXECUTOR_CLASS_ROLES.join("/")}) — read-only roles cannot delegate`,
|
|
143
|
-
);
|
|
144
|
-
}
|
|
128
|
+
// 1. Role gate — the REQUESTED grandchild role must be in the tool's allowed
|
|
129
|
+
// set (the PARENT role is no longer gated — D8: every role may delegate;
|
|
130
|
+
// the spawn-side checkCrewDepth cap + slot budget below are the boundary).
|
|
145
131
|
const requestedRole = requested?.role;
|
|
146
132
|
if (requestedRole !== undefined && !(DELEGATE_ALLOWED_ROLES as readonly string[]).includes(requestedRole)) {
|
|
147
133
|
return deny(
|
|
@@ -150,12 +136,12 @@ export function evaluateDelegateAdmission(input: DelegateAdmissionInput): SpawnP
|
|
|
150
136
|
);
|
|
151
137
|
}
|
|
152
138
|
|
|
153
|
-
//
|
|
139
|
+
// 2. Trust gate — untrusted escalation context never spawns.
|
|
154
140
|
if (input.untrusted) {
|
|
155
141
|
return deny("trust-denied", "delegate rejected: untrusted escalation context (trust gate is manual-only)");
|
|
156
142
|
}
|
|
157
143
|
|
|
158
|
-
//
|
|
144
|
+
// 3. Depth gate — computed from the parent RECORD (never worker env).
|
|
159
145
|
if (childDepth > maxDepth) {
|
|
160
146
|
return deny(
|
|
161
147
|
"depth-exceeded",
|
|
@@ -163,7 +149,7 @@ export function evaluateDelegateAdmission(input: DelegateAdmissionInput): SpawnP
|
|
|
163
149
|
);
|
|
164
150
|
}
|
|
165
151
|
|
|
166
|
-
//
|
|
152
|
+
// 4. Nested-slot budget — fail-fast, never queue.
|
|
167
153
|
if (input.slots.used >= input.slots.max) {
|
|
168
154
|
return deny(
|
|
169
155
|
"slots-exhausted",
|
|
@@ -171,7 +157,7 @@ export function evaluateDelegateAdmission(input: DelegateAdmissionInput): SpawnP
|
|
|
171
157
|
);
|
|
172
158
|
}
|
|
173
159
|
|
|
174
|
-
//
|
|
160
|
+
// 5. Parent allocation sufficiency — reserve up-front or reject.
|
|
175
161
|
if (requested?.budgetTokens !== undefined) {
|
|
176
162
|
const granted = parentTask.allocation?.tokensGranted ?? 0;
|
|
177
163
|
const spent = parentTask.allocation?.tokensSpent ?? 0;
|
|
@@ -184,7 +170,7 @@ export function evaluateDelegateAdmission(input: DelegateAdmissionInput): SpawnP
|
|
|
184
170
|
}
|
|
185
171
|
}
|
|
186
172
|
|
|
187
|
-
//
|
|
173
|
+
// 6. Model validation against the resolved catalog.
|
|
188
174
|
const normalizedModel = requested?.model !== undefined ? modelRefToString(requested.model) : undefined;
|
|
189
175
|
if (normalizedModel !== undefined && input.modelCatalog !== undefined) {
|
|
190
176
|
if (!input.modelCatalog.includes(normalizedModel)) {
|
|
@@ -192,7 +178,7 @@ export function evaluateDelegateAdmission(input: DelegateAdmissionInput): SpawnP
|
|
|
192
178
|
}
|
|
193
179
|
}
|
|
194
180
|
|
|
195
|
-
//
|
|
181
|
+
// 7. Timeout validation (mandatory default 900).
|
|
196
182
|
const timeoutSec = requested?.timeoutSec ?? DEFAULT_DELEGATE_TIMEOUT_SEC;
|
|
197
183
|
if (!Number.isFinite(timeoutSec) || timeoutSec < 1 || timeoutSec > MAX_DELEGATE_TIMEOUT_SEC) {
|
|
198
184
|
return deny(
|
|
@@ -201,7 +187,7 @@ export function evaluateDelegateAdmission(input: DelegateAdmissionInput): SpawnP
|
|
|
201
187
|
);
|
|
202
188
|
}
|
|
203
189
|
|
|
204
|
-
//
|
|
190
|
+
// 8. Workspace interaction (ADR-5 §9): a write-capable grandchild sharing
|
|
205
191
|
// the parent's cwd with ANOTHER in-flight executor is only admitted when
|
|
206
192
|
// serialization is established (limits.serializeOnPathOverlap) — otherwise
|
|
207
193
|
// reject; read-only grandchild roles (explorer/analyst) never conflict.
|
|
@@ -270,10 +270,35 @@ function isTaskHeartbeatStale(task: TeamTaskState, now: number): boolean {
|
|
|
270
270
|
// Compute elapsed from both sources and use the fresher (minimum) one,
|
|
271
271
|
// mirroring heartbeat-watcher logic: if either source has recent activity,
|
|
272
272
|
// the task is not stale even if the other is stale.
|
|
273
|
-
const heartbeatAge = Number.isFinite(heartbeatAt) ? now - heartbeatAt :
|
|
274
|
-
const activityAge = Number.isFinite(activityAt) ? now - activityAt :
|
|
273
|
+
const heartbeatAge = Number.isFinite(heartbeatAt) ? now - heartbeatAt : Number.MAX_SAFE_INTEGER;
|
|
274
|
+
const activityAge = Number.isFinite(activityAt) ? now - activityAt : Number.MAX_SAFE_INTEGER;
|
|
275
275
|
const elapsed = Math.min(heartbeatAge, activityAge);
|
|
276
|
-
|
|
276
|
+
if (elapsed <= NO_PID_HEARTBEAT_STALE_MS) return false;
|
|
277
|
+
// F1 part 2 (battery 2026-09-10, live evidence run team_20260910171307): a
|
|
278
|
+
// surface worker's recorder only flushes at turn boundaries — one slow LLM
|
|
279
|
+
// turn (observed: 6.7min) freezes lastSeenAt/lastActivityAt while the worker
|
|
280
|
+
// process is alive and mid-turn. The heartbeat-watcher already has a
|
|
281
|
+
// PID-liveness gate for exactly this ("alive → downgrade dead→stale"); the
|
|
282
|
+
// reconciler MUST apply the same gate before repairing, or it kills healthy
|
|
283
|
+
// runs at the 5-minute mark mid-turn. A truly dead pid still repairs as before.
|
|
284
|
+
const taskPid = task.heartbeat?.pid ?? task.checkpoint?.childPid;
|
|
285
|
+
const pidAlive = taskPid ? checkProcessLiveness(taskPid).alive : false;
|
|
286
|
+
if (taskPid && pidAlive) return false;
|
|
287
|
+
if (process.env.PI_CREW_DEBUG_STALE === "1") {
|
|
288
|
+
// F1 forensic (battery 2026-09-10): sidecar log of every STALE verdict so
|
|
289
|
+
// live repros can show exactly what the reconciler saw (pid present? alive?
|
|
290
|
+
// elapsed?) — the reconciler may run in ANY host process, hence a fixed
|
|
291
|
+
// sidecar path instead of console.
|
|
292
|
+
try {
|
|
293
|
+
fs.appendFileSync(
|
|
294
|
+
"/tmp/pi-crew-f1-debug.log",
|
|
295
|
+
`${JSON.stringify({ ts: new Date().toISOString(), taskId: task.id, status: task.status, taskPid, pidAlive, heartbeatAge, activityAge, hbLastSeen: task.heartbeat?.lastSeenAt })}\n`,
|
|
296
|
+
);
|
|
297
|
+
} catch {
|
|
298
|
+
/* best-effort forensic log */
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return true;
|
|
277
302
|
}
|
|
278
303
|
|
|
279
304
|
/**
|
|
@@ -22,6 +22,9 @@ export function recordSupervisorContact(manifest: TeamRunManifest, payload: Omit
|
|
|
22
22
|
timestamp: new Date().toISOString(),
|
|
23
23
|
};
|
|
24
24
|
try {
|
|
25
|
+
// REVIEW FIX (2026-09-10): reverted M2b buffered conversion —
|
|
26
|
+
// supervisor.contact events are read back synchronously (tests +
|
|
27
|
+
// contact-history display); low-frequency by design.
|
|
25
28
|
appendEvent(manifest.eventsPath, {
|
|
26
29
|
type: "supervisor.contact",
|
|
27
30
|
runId: manifest.runId,
|