pi-crew 0.11.5 → 0.11.6
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 +70 -0
- package/README.md +1 -1
- package/dist/index.mjs +493 -488
- package/docs/dynamic-workflows.md +37 -0
- package/package.json +12 -8
- package/scripts/analyze-run.mjs +95 -11
- package/skills/model-routing-context/SKILL.md +4 -3
- package/skills/real-test-pi-crew/SKILL.md +9 -6
- package/src/config/env-vars.ts +6 -0
- package/src/extension/crew-vibes/index.ts +29 -10
- package/src/extension/pi-api.ts +1 -0
- package/src/extension/registration/lifecycle-handlers.ts +65 -3
- package/src/extension/team-tool/respond.ts +54 -0
- package/src/extension/team-tool/status.ts +10 -0
- package/src/extension/team-tool.ts +393 -3
- package/src/extension/webhook-notify.ts +23 -1
- package/src/observability/exporters/otlp-exporter.ts +93 -11
- package/src/prompt/prompt-runtime.ts +13 -4
- package/src/runtime/agent-control.ts +1 -15
- package/src/runtime/background-runner.ts +3 -0
- package/src/runtime/broker/crew-broker.ts +12 -103
- package/src/runtime/broker/protocol/request-parsers.ts +9 -1
- package/src/runtime/broker/protocol/wait-resolve.ts +168 -0
- package/src/runtime/child-pi/child-pi-spawn.ts +5 -4
- package/src/runtime/dispatch-batch.ts +3 -1
- package/src/runtime/goal-workflow/dynamic-workflow-context.ts +111 -12
- package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +19 -0
- package/src/runtime/live-session/live-agent-manager.ts +35 -0
- package/src/runtime/live-session/live-session-runtime.ts +34 -5
- package/src/runtime/mcp-proxy.ts +110 -7
- package/src/runtime/model/pi-args.ts +14 -11
- package/src/runtime/model/provider-extensions.ts +67 -1
- package/src/runtime/scheduling/run-coalesced-task-group.ts +89 -6
- package/src/runtime/stale-reconciler.ts +47 -0
- package/src/runtime/task-runner/child-executor.ts +130 -4
- package/src/runtime/task-runner/output-splitter.ts +48 -5
- package/src/runtime/task-runner/post-execution.ts +0 -2
- package/src/runtime/task-runner/pre-execution.ts +8 -3
- package/src/runtime/task-runner/retrieval-orchestrator.ts +28 -2
- package/src/schema/team-tool-schema.ts +25 -0
- package/src/state/contracts.ts +16 -0
- package/src/state/crew-init.ts +114 -0
- package/src/state/stores/active-run-registry.ts +13 -6
- package/src/state/types.ts +0 -8
- package/src/ui/goal-flag.ts +44 -0
- package/src/ui/live-run-sidebar.ts +5 -1
- package/src/ui/pi-ui-compat.ts +20 -3
- package/src/ui/run-dashboard.ts +2 -1
- package/src/ui/widget/widget-model.ts +6 -2
- package/src/utils/ip-literal.ts +145 -0
- package/src/workflows/workflow-config.ts +6 -0
|
@@ -42,6 +42,7 @@ import { CrewScheduler, type ScheduledJob } from "../../runtime/scheduling/sched
|
|
|
42
42
|
import { tryRegisterSessionCleanup } from "../../runtime/session-resources.ts";
|
|
43
43
|
import { createSessionSnapshot } from "../../runtime/session-snapshot.ts";
|
|
44
44
|
import { applyCrewSettingsTiersToConfig, loadCrewSettingsTiers, scheduledJobsHiddenCountOf } from "../../runtime/settings-store.ts";
|
|
45
|
+
import type { ReconcileResult } from "../../runtime/stale-reconciler.ts";
|
|
45
46
|
import { loadTasksWithRecovery } from "../../state/stores/manifest-io.ts";
|
|
46
47
|
import { loadRunManifestById } from "../../state/stores/state-store.ts";
|
|
47
48
|
import type { TeamRunManifest } from "../../state/types.ts";
|
|
@@ -88,6 +89,62 @@ export function installSessionLifecycleHandlers(pi: ExtensionAPI, ctx: Registrat
|
|
|
88
89
|
installModelTrackingHandlers(pi);
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
// ─── NEW-2 (SDD-4 follow-up, P3 review MAJOR 1): honest stale-reconcile notify ──
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Repaired runIds already surfaced by the session-start stale-reconcile
|
|
96
|
+
* notify in THIS process, bounded so a long-lived session cannot grow the set
|
|
97
|
+
* unboundedly (overflow clears — a re-notify after 256+ distinct repairs is
|
|
98
|
+
* acceptable noise, an unbounded leak is not).
|
|
99
|
+
*
|
|
100
|
+
* Cross-restart dedupe is structural, not tracked here: a real repair persists
|
|
101
|
+
* a terminal run status, so the run drops out of the reconcile input and can
|
|
102
|
+
* never re-notify. The set guards the two in-process repeat windows — session
|
|
103
|
+
* reload/fork re-firing session_start, and a repair whose persistence failed
|
|
104
|
+
* (disk error / lock steal leaves the run reconcileable while the notify still
|
|
105
|
+
* claimed it). Observed pre-fix (2026-09-29): 77 dishonest notifies/day,
|
|
106
|
+
* ~3x per runId, because non-repaired verdicts (blocked_awaiting_approval,
|
|
107
|
+
* waiting_answer, result_exists) never persist anything and re-fired the
|
|
108
|
+
* "Found and repaired ghost runs" text on every session start.
|
|
109
|
+
*/
|
|
110
|
+
const notifiedRepairedRunIds = new Set<string>();
|
|
111
|
+
const NOTIFIED_REPAIRED_RUN_IDS_CAP = 256;
|
|
112
|
+
|
|
113
|
+
/** What the session-start stale-reconcile notify should say (or null: say nothing). */
|
|
114
|
+
export interface StaleReconcileNotifyPlan {
|
|
115
|
+
title: string;
|
|
116
|
+
body: string;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* NEW-2: decide the operator notification for a session-start reconcile
|
|
121
|
+
* batch. Honest by construction — a notify is emitted ONLY when reconcile
|
|
122
|
+
* actually repaired something (`repaired === true`): non-repaired verdicts
|
|
123
|
+
* are intentional states (plan approval pending, ask parked, result already
|
|
124
|
+
* exists) that the run dashboard already surfaces, so claiming "repaired"
|
|
125
|
+
* for them was a false system statement. The title says exactly what
|
|
126
|
+
* happened ("Repaired N stale run(s)"), the body names each repaired runId
|
|
127
|
+
* with its verdict, and each repaired runId notifies at most once per
|
|
128
|
+
* process (bounded set above).
|
|
129
|
+
*/
|
|
130
|
+
export function decideStaleReconcileNotification(staleResults: ReconcileResult[]): StaleReconcileNotifyPlan | null {
|
|
131
|
+
const repaired = staleResults.filter((r) => r.repaired === true && !notifiedRepairedRunIds.has(r.runId));
|
|
132
|
+
if (repaired.length === 0) return null;
|
|
133
|
+
for (const r of repaired) {
|
|
134
|
+
if (notifiedRepairedRunIds.size >= NOTIFIED_REPAIRED_RUN_IDS_CAP) notifiedRepairedRunIds.clear();
|
|
135
|
+
notifiedRepairedRunIds.add(r.runId);
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
title: `Repaired ${repaired.length} stale run(s)`,
|
|
139
|
+
body: `Repaired stale runs from previous sessions: ${repaired.map((r) => `${r.runId} (${r.verdict})`).join(", ")}`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Test seam: reset the per-process dedupe set between unit tests. */
|
|
144
|
+
export function __test__resetNotifiedRepairedRunIds(): void {
|
|
145
|
+
notifiedRepairedRunIds.clear();
|
|
146
|
+
}
|
|
147
|
+
|
|
91
148
|
/**
|
|
92
149
|
* Hand over the outcome of runs detached by an agent-view switch.
|
|
93
150
|
*
|
|
@@ -460,13 +517,18 @@ async function runDeferredSessionCleanup(
|
|
|
460
517
|
// session_start callback synchronous; notify when the result lands.
|
|
461
518
|
void reconcileAllStaleRuns(extensionCtx.cwd, ctx.getManifestCache(extensionCtx.cwd), Date.now(), currentSessionId)
|
|
462
519
|
.then((staleResults) => {
|
|
463
|
-
|
|
520
|
+
// NEW-2 (SDD-4 follow-up): honest notify — only actual repairs are
|
|
521
|
+
// reported, each repaired runId at most once per process. Non-repaired
|
|
522
|
+
// verdicts (blocked_awaiting_approval / waiting_answer /
|
|
523
|
+
// result_exists) must NEVER be claimed as "repaired".
|
|
524
|
+
const plan = decideStaleReconcileNotification(staleResults ?? []);
|
|
525
|
+
if (plan) {
|
|
464
526
|
ctx.notifyOperator({
|
|
465
527
|
id: "stale_reconcile",
|
|
466
528
|
severity: "info",
|
|
467
529
|
source: "crash-recovery",
|
|
468
|
-
title:
|
|
469
|
-
body:
|
|
530
|
+
title: plan.title,
|
|
531
|
+
body: plan.body,
|
|
470
532
|
});
|
|
471
533
|
}
|
|
472
534
|
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { renderAskAnswer } from "../../prompt/prompt-runtime.ts";
|
|
2
2
|
import { readCrewAgents, recordFromTask, saveCrewAgents } from "../../runtime/crew-agent-records.ts";
|
|
3
3
|
import { isWaitingWorkerAlive } from "../../runtime/dispatch-batch.ts";
|
|
4
|
+
import { checkProcessLiveness } from "../../runtime/process-status.ts";
|
|
4
5
|
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
5
6
|
import { withRunLockSync } from "../../state/coordination/locks.ts";
|
|
6
7
|
import { appendMailboxMessage, readMailbox, updateMailboxMessageReply } from "../../state/coordination/mailbox.ts";
|
|
@@ -142,6 +143,17 @@ export function handleRespond(params: TeamToolParamsValue, ctx: TeamContext): Pi
|
|
|
142
143
|
/** Exactly-once no-ops: a mailbox response already exists for the questionId. */
|
|
143
144
|
const noopIds: string[] = [];
|
|
144
145
|
const answeredEvents: Array<{ taskId: string; questionId: string; delivery: "mailbox" | "requeue" }> = [];
|
|
146
|
+
/** G5 (deep-review 2026-10-01): DEAD parks — the questionId-tagged response
|
|
147
|
+
* can never be picked up (the parked poller is gone); recorded with the
|
|
148
|
+
* liveness pid evidence so the leader sees the respond fell into the void.
|
|
149
|
+
* Twin of the broker-side task.respond_delivered ack. */
|
|
150
|
+
const missedEvents: Array<{
|
|
151
|
+
taskId: string;
|
|
152
|
+
questionId: string;
|
|
153
|
+
workerPid: number | undefined;
|
|
154
|
+
pidAlive: boolean;
|
|
155
|
+
pidDetail: string;
|
|
156
|
+
}> = [];
|
|
145
157
|
let updatedTasks = fresh.tasks;
|
|
146
158
|
|
|
147
159
|
for (const task of targetTasks) {
|
|
@@ -206,6 +218,21 @@ export function handleRespond(params: TeamToolParamsValue, ctx: TeamContext): Pi
|
|
|
206
218
|
updatedTasks = requeueWaitingTask(updatedTasks, task.id, message, renderAskAnswer(questionId, message));
|
|
207
219
|
requeuedIds.push(task.id);
|
|
208
220
|
answeredEvents.push({ taskId: task.id, questionId, delivery: "requeue" });
|
|
221
|
+
// G5: liveness evidence for respond_missed — the same pid source + probe
|
|
222
|
+
// the stale-reconciler / heartbeat-watcher use (heartbeat pid, fallback
|
|
223
|
+
// checkpoint childPid, then process.kill(pid,0) via checkProcessLiveness).
|
|
224
|
+
// A live pid here (stale heartbeat but process alive) is still recorded
|
|
225
|
+
// verbatim — the discriminator already ruled DEAD for delivery; the event
|
|
226
|
+
// reports what the pid probe saw, it does not re-decide the branch.
|
|
227
|
+
const missedPid = task.heartbeat?.pid ?? task.checkpoint?.childPid;
|
|
228
|
+
const liveness = checkProcessLiveness(missedPid);
|
|
229
|
+
missedEvents.push({
|
|
230
|
+
taskId: task.id,
|
|
231
|
+
questionId,
|
|
232
|
+
workerPid: missedPid,
|
|
233
|
+
pidAlive: liveness.alive,
|
|
234
|
+
pidDetail: liveness.detail,
|
|
235
|
+
});
|
|
209
236
|
}
|
|
210
237
|
|
|
211
238
|
// If this respond includes a replyTo, update the original message with reply metadata.
|
|
@@ -255,6 +282,33 @@ export function handleRespond(params: TeamToolParamsValue, ctx: TeamContext): Pi
|
|
|
255
282
|
),
|
|
256
283
|
);
|
|
257
284
|
}
|
|
285
|
+
// G5 (deep-review 2026-10-01): respond_missed — the DEAD-branch twin of
|
|
286
|
+
// the broker's respond_delivered ack. Same H1 fire-and-forget discipline
|
|
287
|
+
// (sync run-lock callback, never await/block); carries the questionId and
|
|
288
|
+
// the pid/liveness evidence the discrimination was based on.
|
|
289
|
+
for (const missed of missedEvents) {
|
|
290
|
+
void appendEventAsync(manifest.eventsPath, {
|
|
291
|
+
type: "task.respond_missed",
|
|
292
|
+
runId: manifest.runId,
|
|
293
|
+
taskId: missed.taskId,
|
|
294
|
+
message: `Leader respond missed: worker for question ${missed.questionId} is not alive to receive the mailbox response (pid ${
|
|
295
|
+
missed.workerPid ?? "none recorded"
|
|
296
|
+
}: ${missed.pidDetail}); delivery rides the requeue+inject path.`,
|
|
297
|
+
data: {
|
|
298
|
+
questionId: missed.questionId,
|
|
299
|
+
...(missed.workerPid !== undefined ? { workerPid: missed.workerPid } : {}),
|
|
300
|
+
pidAlive: missed.pidAlive,
|
|
301
|
+
pidDetail: missed.pidDetail,
|
|
302
|
+
delivery: "requeue",
|
|
303
|
+
},
|
|
304
|
+
}).catch((error) =>
|
|
305
|
+
logInternalError(
|
|
306
|
+
"respond.respond-missed-event",
|
|
307
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
308
|
+
`runId=${manifest.runId}`,
|
|
309
|
+
),
|
|
310
|
+
);
|
|
311
|
+
}
|
|
258
312
|
for (const requeuedId of requeuedIds) {
|
|
259
313
|
void appendEventAsync(manifest.eventsPath, {
|
|
260
314
|
type: "task.resumed",
|
|
@@ -15,6 +15,7 @@ import { appendEvent, readEventsCursor } from "../../state/event-log/event-log.t
|
|
|
15
15
|
import { loadRunManifestById, saveRunTasks, updateRunStatus } from "../../state/stores/state-store.ts";
|
|
16
16
|
import { aggregateUsage, formatCost, formatUsage } from "../../state/usage.ts";
|
|
17
17
|
import { formatDuration } from "../../ui/format-helpers.ts";
|
|
18
|
+
import { goalAchievedStatusLabel } from "../../ui/goal-flag.ts";
|
|
18
19
|
import { locateRunCwd } from "../team-tool.ts";
|
|
19
20
|
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
20
21
|
import { result, type TeamContext } from "./context.ts";
|
|
@@ -236,11 +237,16 @@ export function handleStatus(params: TeamToolParamsValue, ctx: TeamContext): PiT
|
|
|
236
237
|
const waitingTasks = tasks.filter((task) => task.status === "queued" || task.status === "waiting");
|
|
237
238
|
const agentLine = (agent: (typeof crewAgents)[number]): string =>
|
|
238
239
|
`- ${agent.id} [${agent.status}] ${agent.role} -> ${agent.agent} runtime=${agent.runtime}${agent.model ? ` model=${agent.model}` : ""}${agent.usage ? ` usage=${formatUsage(agent.usage)}` : ""}${agent.usage?.cost ? ` cost=${formatCost(agent.usage.cost)}` : ""}${agent.progress?.activityState ? ` activityState=${agent.progress.activityState}` : ""}${formatActivityAge(agent) ? ` activity=${formatActivityAge(agent)}` : ""}${agent.progress?.currentTool ? ` tool=${agent.progress.currentTool}` : ""}${agent.toolUses ? ` tools=${agent.toolUses}` : ""}${!agent.usage && agent.progress?.tokens ? ` tokens=${agent.progress.tokens}` : ""}${agent.progress?.turns ? ` turns=${agent.progress.turns}` : ""}${agent.jsonEvents !== undefined ? ` jsonEvents=${agent.jsonEvents}` : ""}${agent.outputPath ? ` output=${agent.outputPath}` : ""}${agent.transcriptPath ? ` transcript=${agent.transcriptPath}` : ""}${agent.statusPath ? ` status=${agent.statusPath}` : ""}${agent.error ? ` error=${agent.error}` : ""}`;
|
|
240
|
+
// G19 (W-E Phase 1): surface the goal-achievement verdict on terminal runs —
|
|
241
|
+
// ⚠ false-green warning line; silent while running, when achieved, or when
|
|
242
|
+
// the run predates the assessment (goalAchieved undefined).
|
|
243
|
+
const goalLine = goalAchievedStatusLabel(manifest);
|
|
239
244
|
const lines = [
|
|
240
245
|
`Run: ${manifest.runId}`,
|
|
241
246
|
`Team: ${manifest.team}`,
|
|
242
247
|
`Workflow: ${manifest.workflow ?? "(none)"}`,
|
|
243
248
|
`Status: ${manifest.status}`,
|
|
249
|
+
...(goalLine ? [goalLine] : []),
|
|
244
250
|
`Progress: ${phaseProgress.overallPercentage}% (~${formatDuration(phaseProgress.estimatedRemainingMs)} remaining)`,
|
|
245
251
|
`Workspace mode: ${manifest.workspaceMode}`,
|
|
246
252
|
...(manifest.runtimeResolution
|
|
@@ -347,6 +353,8 @@ export function buildCompactStatus(
|
|
|
347
353
|
status: string;
|
|
348
354
|
goal: string;
|
|
349
355
|
workspaceMode?: string;
|
|
356
|
+
goalAchieved?: boolean | "unknown";
|
|
357
|
+
goalAchievementNote?: string;
|
|
350
358
|
},
|
|
351
359
|
tasks: Array<{
|
|
352
360
|
id: string;
|
|
@@ -360,10 +368,12 @@ export function buildCompactStatus(
|
|
|
360
368
|
progress?: { overallPercentage: number; estimatedRemainingMs: number },
|
|
361
369
|
): string[] {
|
|
362
370
|
const failedOrAttention = tasks.filter((t) => t.status === "failed" || t.status === "needs_attention" || t.status === "cancelled");
|
|
371
|
+
const goalLine = goalAchievedStatusLabel(manifest);
|
|
363
372
|
const lines = [
|
|
364
373
|
`Run: ${manifest.runId}`,
|
|
365
374
|
`Team: ${manifest.team}${manifest.workflow ? ` (${manifest.workflow})` : ""}`,
|
|
366
375
|
`Status: ${manifest.status}`,
|
|
376
|
+
...(goalLine ? [goalLine] : []),
|
|
367
377
|
...(progress ? [`Progress: ${progress.overallPercentage}% (~${formatDuration(progress.estimatedRemainingMs)} remaining)`] : []),
|
|
368
378
|
`Goal: ${manifest.goal}`,
|
|
369
379
|
...(asyncLivenessLine ? [asyncLivenessLine] : []),
|