pi-crew 0.9.41 → 0.9.44
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 +253 -0
- package/README.md +14 -0
- package/dist/build-meta.json +6450 -12419
- package/dist/index.mjs +57998 -68417
- package/dist/index.mjs.map +4 -4
- package/docs/migration/atomic-write-v2-migration.md +2 -0
- package/docs/superpowers/plans/2026-06-09-fallow-patterns-adoption.md +2 -0
- package/package.json +4 -2
- package/scripts/README.md +6 -0
- package/scripts/build-bundle.mjs +2 -1
- package/scripts/check-all-skills.ts +3 -3
- package/scripts/dev-runner.mjs +106 -0
- package/scripts/postinstall.mjs +6 -2
- package/scripts/verify-skill.ts +2 -2
- package/src/agents/agent-config.ts +1 -1
- package/src/config/config.ts +20 -1
- package/src/config/drift-detector.ts +97 -15
- package/src/config/resilient-parser.ts +4 -4
- package/src/config/types.ts +0 -2
- package/src/extension/crew-cleanup.ts +17 -3
- package/src/extension/crew-vibes/config.ts +1 -1
- package/src/extension/crew-vibes/font-detect.ts +16 -3
- package/src/extension/crew-vibes/index.ts +1 -1
- package/src/extension/cross-extension-rpc.ts +26 -9
- package/src/extension/management.ts +6 -5
- package/src/extension/register.ts +58 -1573
- package/src/extension/registration/command-registration.ts +57 -0
- package/src/extension/registration/commands.ts +7 -2
- package/src/extension/registration/context-builder.ts +131 -0
- package/src/extension/registration/crash-recovery-cache.ts +54 -0
- package/src/extension/registration/foreground-run-controller.ts +238 -0
- package/src/extension/registration/hook-registration.ts +119 -0
- package/src/extension/registration/lazy-configurers.ts +124 -0
- package/src/extension/registration/lifecycle-handlers.ts +731 -0
- package/src/extension/registration/observability.ts +33 -4
- package/src/extension/registration/registration-types.ts +149 -0
- package/src/extension/registration/runtime-cleanup.ts +230 -0
- package/src/extension/registration/subagent-manager-setup.ts +211 -0
- package/src/extension/registration/tool-registration.ts +50 -0
- package/src/extension/registration/wire-cross-extension.ts +37 -0
- package/src/extension/rpc-hmac.ts +21 -1
- package/src/extension/run-import.ts +3 -3
- package/src/extension/team-tool/auto-summarize.ts +4 -4
- package/src/extension/team-tool/cancel.ts +5 -5
- package/src/extension/team-tool/chain-executor.ts +5 -4
- package/src/extension/team-tool/context.ts +11 -1
- package/src/extension/team-tool/doctor.ts +2 -1
- package/src/extension/team-tool/goal-wrap.ts +2 -2
- package/src/extension/team-tool/goal.ts +1 -1
- package/src/extension/team-tool/handle-schedule.ts +25 -36
- package/src/extension/team-tool/orchestrate.ts +0 -1
- package/src/extension/team-tool/parallel-dispatch.ts +5 -0
- package/src/extension/team-tool/run.ts +220 -214
- package/src/extension/team-tool/workflow-manage.ts +1 -1
- package/src/extension/team-tool.ts +69 -16
- package/src/hooks/registry.ts +7 -2
- package/src/observability/correlation.ts +4 -3
- package/src/observability/event-bus.ts +10 -1
- package/src/observability/event-to-metric.ts +8 -7
- package/src/observability/exporters/adapter.ts +6 -9
- package/src/observability/exporters/otlp-exporter.ts +81 -0
- package/src/observability/metric-retention.ts +4 -0
- package/src/observability/metric-sink.ts +14 -4
- package/src/prompt/prompt-runtime.ts +15 -0
- package/src/runtime/adaptive-plan.ts +1 -1
- package/src/runtime/async-runner.ts +13 -4
- package/src/runtime/background-runner.ts +32 -23
- package/src/runtime/capability-inventory.ts +1 -1
- package/src/runtime/chain-runner.ts +5 -5
- package/src/runtime/checkpoint.ts +6 -16
- package/src/runtime/child-pi-constants.ts +42 -0
- package/src/runtime/child-pi-kill.ts +180 -0
- package/src/runtime/child-pi-spawn.ts +234 -0
- package/src/runtime/child-pi-steering.ts +128 -0
- package/src/runtime/child-pi-streams.ts +296 -0
- package/src/runtime/child-pi-transcript.ts +169 -0
- package/src/runtime/child-pi.ts +80 -822
- package/src/runtime/coalesce-tasks.ts +1 -1
- package/src/runtime/compact-stages/tail-capture-stage.ts +1 -1
- package/src/runtime/crash-recovery.ts +63 -47
- package/src/runtime/delivery-coordinator.ts +1 -1
- package/src/runtime/deterministic-ast.ts +2 -2
- package/src/runtime/diagnostic-export.ts +2 -1
- package/src/runtime/dwf-state-store.ts +5 -0
- package/src/runtime/dynamic-workflow-context.ts +78 -18
- package/src/runtime/dynamic-workflow-runner.ts +18 -2
- package/src/runtime/foreground-control.ts +3 -2
- package/src/runtime/goal-evaluator.ts +59 -27
- package/src/runtime/goal-loop-runner.ts +46 -13
- package/src/runtime/goal-state-store.ts +1 -1
- package/src/runtime/handoff-manager.ts +0 -2
- package/src/runtime/intercom-bridge.ts +0 -2
- package/src/runtime/live-agent-manager.ts +30 -5
- package/src/runtime/live-extension-bridge.ts +0 -2
- package/src/runtime/live-session-runtime.ts +65 -57
- package/src/runtime/manifest-cache.ts +59 -0
- package/src/runtime/mcp-proxy.ts +1 -1
- package/src/runtime/orphan-worker-registry.ts +3 -3
- package/src/runtime/output-validator.ts +1 -1
- package/src/runtime/path-overlap.ts +1 -1
- package/src/runtime/pi-args.ts +3 -3
- package/src/runtime/pi-json-output.ts +4 -3
- package/src/runtime/pi-spawn.ts +7 -1
- package/src/runtime/pipeline-runner.ts +1 -4
- package/src/runtime/post-checks.ts +0 -1
- package/src/runtime/retry-runner.ts +1 -1
- package/src/runtime/run-coalesced-task-group.ts +18 -23
- package/src/runtime/scheduler.ts +2 -0
- package/src/runtime/session-resources.ts +1 -1
- package/src/runtime/settings-store.ts +31 -1
- package/src/runtime/skill-effectiveness.ts +2 -2
- package/src/runtime/stale-reconciler.ts +23 -53
- package/src/runtime/subagent-manager.ts +19 -3
- package/src/runtime/task-output-context.ts +2 -1
- package/src/runtime/task-runner/prompt-builder.ts +3 -3
- package/src/runtime/task-runner/run-projection.ts +3 -1
- package/src/runtime/task-runner/state-helpers.ts +13 -1
- package/src/runtime/task-runner.ts +31 -6
- package/src/runtime/team-runner.ts +8 -2
- package/src/runtime/verification-gates.ts +54 -5
- package/src/schema/config-schema.ts +19 -1
- package/src/schema/team-tool-schema.ts +2 -0
- package/src/skills/discover-skills.ts +25 -15
- package/src/state/active-run-registry.ts +0 -1
- package/src/state/atomic-write.ts +37 -11
- package/src/state/blob-store.ts +3 -9
- package/src/state/contracts.ts +5 -1
- package/src/state/crew-init.ts +3 -2
- package/src/state/decision-ledger.ts +2 -2
- package/src/state/event-log-rotation.ts +1 -1
- package/src/state/event-log.ts +4 -3
- package/src/state/gitignore-manager.ts +2 -1
- package/src/state/health-store.ts +5 -2
- package/src/state/instinct-store.ts +11 -2
- package/src/state/locks.ts +132 -49
- package/src/state/mailbox.ts +3 -4
- package/src/state/observation-store.ts +1 -13
- package/src/state/run-cache.ts +12 -12
- package/src/state/run-graph.ts +2 -1
- package/src/state/run-metrics.ts +1 -1
- package/src/state/schedule.ts +7 -1
- package/src/state/state-store.ts +9 -3
- package/src/state/tiered-eval.ts +1 -1
- package/src/state/worker-atomic-writer.ts +1 -0
- package/src/ui/dynamic-border.ts +13 -3
- package/src/ui/live-conversation-overlay.ts +7 -27
- package/src/ui/live-duration.ts +2 -2
- package/src/ui/live-run-sidebar.ts +56 -32
- package/src/ui/mascot.ts +19 -13
- package/src/ui/powerbar-publisher.ts +244 -178
- package/src/ui/render-coalescer.ts +44 -3
- package/src/ui/render-diff.ts +2 -2
- package/src/ui/run-dashboard.ts +122 -21
- package/src/ui/run-event-bus.ts +66 -0
- package/src/ui/run-snapshot-cache.ts +84 -81
- package/src/ui/settings-overlay.ts +3 -3
- package/src/ui/status-colors.ts +24 -0
- package/src/ui/tool-progress-formatter.ts +1 -1
- package/src/ui/tool-render.ts +3 -3
- package/src/ui/transcript-cache.ts +5 -10
- package/src/ui/transcript-entries.ts +68 -72
- package/src/ui/transcript-viewer.ts +54 -14
- package/src/ui/widget/index.ts +26 -15
- package/src/ui/widget/widget-model.ts +18 -9
- package/src/ui/widget/widget-renderer.ts +2 -5
- package/src/utils/fingerprint.ts +1 -1
- package/src/utils/paths.ts +1 -1
- package/src/utils/project-detector.ts +1 -1
- package/src/worktree/worktree-manager.ts +47 -19
- package/src/state/atomic-write-v2.ts +0 -108
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import { randomBytes } from "node:crypto";
|
|
18
18
|
import { existsSync, readdirSync } from "node:fs";
|
|
19
|
+
import { createRequire } from "node:module";
|
|
19
20
|
import type { AgentConfig } from "../agents/agent-config.ts";
|
|
20
21
|
import { loadConfig } from "../config/config.ts";
|
|
21
22
|
import { effectiveRunConfig } from "../extension/team-tool/config-patch.ts";
|
|
@@ -65,21 +66,29 @@ export const stubGoalEvaluator = async (
|
|
|
65
66
|
_m?: import("../state/types.ts").TeamRunManifest,
|
|
66
67
|
_t?: import("../state/types.ts").TeamTaskState[],
|
|
67
68
|
_s?: AbortSignal,
|
|
68
|
-
): Promise<
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
): Promise<GoalEvaluatorResult> => ({
|
|
70
|
+
verdict: {
|
|
71
|
+
turn: goal.turnsUsed,
|
|
72
|
+
achieved: false,
|
|
73
|
+
reason: `not-achieved: stub evaluator (P0). Turn ${goal.turnsUsed}/${goal.maxTurns} completed; P1 will judge against objective + verification.`,
|
|
74
|
+
evaluatorModel: "stub",
|
|
75
|
+
evaluatedAt: new Date().toISOString(),
|
|
76
|
+
},
|
|
77
|
+
judgeUsage: undefined,
|
|
74
78
|
});
|
|
75
79
|
|
|
80
|
+
export type GoalEvaluatorResult = {
|
|
81
|
+
verdict: GoalVerdict;
|
|
82
|
+
judgeUsage?: { totalTokens: number; inputTokens?: number; outputTokens?: number };
|
|
83
|
+
};
|
|
84
|
+
|
|
76
85
|
export type GoalEvaluatorFn = (
|
|
77
86
|
goal: GoalLoopState,
|
|
78
87
|
turnRunId: string,
|
|
79
88
|
turnManifest: import("../state/types.ts").TeamRunManifest,
|
|
80
89
|
turnTasks: import("../state/types.ts").TeamTaskState[],
|
|
81
90
|
signal: AbortSignal,
|
|
82
|
-
) => Promise<
|
|
91
|
+
) => Promise<GoalEvaluatorResult>;
|
|
83
92
|
|
|
84
93
|
/**
|
|
85
94
|
* Production evaluator (P1): bundles turn evidence + calls the LLM judge.
|
|
@@ -93,7 +102,7 @@ export const realGoalEvaluator = async (
|
|
|
93
102
|
turnManifest: import("../state/types.ts").TeamRunManifest,
|
|
94
103
|
turnTasks: import("../state/types.ts").TeamTaskState[],
|
|
95
104
|
signal: AbortSignal,
|
|
96
|
-
): Promise<
|
|
105
|
+
): Promise<GoalEvaluatorResult> => {
|
|
97
106
|
const transcriptPath = deriveTranscriptPath(turnManifest.artifactsRoot, turnTasks);
|
|
98
107
|
// Fix round-7 F1: execute verification commands (if configured) so the judge has real evidence.
|
|
99
108
|
// Previously bundleEvidence received `undefined` — the judge was told commands "MUST pass"
|
|
@@ -260,8 +269,11 @@ function resolveGoalTurnWorkflow(goal: GoalLoopState): WorkflowConfig {
|
|
|
260
269
|
const wrapName = (goal as GoalLoopState & { goalWrapWorkflow?: string }).goalWrapWorkflow;
|
|
261
270
|
if (!wrapName) return buildTurnWorkflow();
|
|
262
271
|
try {
|
|
263
|
-
|
|
264
|
-
|
|
272
|
+
// LAZY: createRequire for synchronous module resolution in ESM context
|
|
273
|
+
const requireFromHere = createRequire(import.meta.url);
|
|
274
|
+
const { discoverWorkflows, allWorkflows } = requireFromHere(
|
|
275
|
+
"../workflows/discover-workflows.ts",
|
|
276
|
+
) as typeof import("../workflows/discover-workflows.ts");
|
|
265
277
|
const found = allWorkflows(discoverWorkflows(goal.cwd)).find((w) => w.name === wrapName && w.source === "builtin");
|
|
266
278
|
if (found) return found;
|
|
267
279
|
logInternalError(
|
|
@@ -672,17 +684,38 @@ export async function runGoalLoop(input: RunGoalLoopInput): Promise<RunGoalLoopR
|
|
|
672
684
|
logInternalError("goal-loop.saveTurnTasks", error, `turnRunId=${created.manifest.runId}`);
|
|
673
685
|
}
|
|
674
686
|
|
|
687
|
+
// GL-1: if the turn ended in blocked/failed status, stop the loop without spawning the judge.
|
|
688
|
+
// Previously the loop continued to evaluateGoal on blocked/failed turns, burning maxTurns.
|
|
689
|
+
const turnStatus = turnResult.manifest.status;
|
|
690
|
+
if (turnStatus === "blocked" || turnStatus === "failed") {
|
|
691
|
+
goal = safeSetStatus(store, goal.goalId, "blocked", goal, eventsPath);
|
|
692
|
+
appendEvent(eventsPath, {
|
|
693
|
+
type: "goal.turn_terminal_status",
|
|
694
|
+
runId: manifest.runId,
|
|
695
|
+
data: {
|
|
696
|
+
goalId: goal.goalId,
|
|
697
|
+
turn: turnIndex,
|
|
698
|
+
turnRunId: created.manifest.runId,
|
|
699
|
+
turnStatus,
|
|
700
|
+
},
|
|
701
|
+
});
|
|
702
|
+
break;
|
|
703
|
+
}
|
|
704
|
+
|
|
675
705
|
// ── BUDGET accumulation (§0c C2: collectRunMetrics) ──────────────────────
|
|
676
|
-
const
|
|
706
|
+
const turnBudget = accumulateBudget(goal, created.manifest.runId);
|
|
677
707
|
|
|
678
708
|
// ── EVALUATE (P1: real LLM judge; pass turn manifest + tasks for transcript lookup) ──
|
|
679
|
-
const verdict = await evaluator(
|
|
680
|
-
{ ...goal, budgetUsed:
|
|
709
|
+
const { verdict, judgeUsage } = await evaluator(
|
|
710
|
+
{ ...goal, budgetUsed: turnBudget },
|
|
681
711
|
created.manifest.runId,
|
|
682
712
|
turnResult.manifest,
|
|
683
713
|
turnResult.tasks,
|
|
684
714
|
signal,
|
|
685
715
|
);
|
|
716
|
+
// BDG-1: accumulate judge token usage into budget alongside the turn's worker tokens.
|
|
717
|
+
const judgeTokens = judgeUsage?.totalTokens ?? 0;
|
|
718
|
+
const updatedBudget = turnBudget + judgeTokens;
|
|
686
719
|
const historyEntry = {
|
|
687
720
|
runId: created.manifest.runId,
|
|
688
721
|
outcome: verdict.achieved ? "achieved" : "not-achieved",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* per-turn usage stays on each turn's TeamRunManifest/tasks.json.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { closeSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, statSync, unlinkSync
|
|
16
|
+
import { closeSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, statSync, unlinkSync } from "node:fs";
|
|
17
17
|
import { dirname } from "node:path";
|
|
18
18
|
import { atomicWriteJson } from "../state/atomic-write.ts";
|
|
19
19
|
import { appendEvent } from "../state/event-log.ts";
|
|
@@ -54,8 +54,6 @@ export function isValidHandoffSummary(value: unknown): value is HandoffSummary {
|
|
|
54
54
|
* @see docs/pi-boomerang-integration-plan.md
|
|
55
55
|
*/
|
|
56
56
|
|
|
57
|
-
import type { TeamEvent } from "../state/event-log.ts";
|
|
58
|
-
|
|
59
57
|
/**
|
|
60
58
|
* Represents a key decision made during task execution.
|
|
61
59
|
*/
|
|
@@ -8,8 +8,6 @@
|
|
|
8
8
|
* The actual tool registration happens in task-runner.ts.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { logInternalError } from "../utils/internal-error.ts";
|
|
12
|
-
|
|
13
11
|
// ── Types ────────────────────────────────────────────────────────────────
|
|
14
12
|
|
|
15
13
|
export type IntercomUrgency = "low" | "medium" | "high" | "critical";
|
|
@@ -2,6 +2,7 @@ import type { appendEvent } from "../state/event-log.ts";
|
|
|
2
2
|
import { logInternalError } from "../utils/internal-error.ts";
|
|
3
3
|
import type { CrewAgentRecord } from "./crew-agent-runtime.ts";
|
|
4
4
|
import type { IrcMessage } from "./live-irc.ts";
|
|
5
|
+
import { checkProcessLiveness } from "./process-status.ts";
|
|
5
6
|
|
|
6
7
|
const MAX_PENDING_MESSAGES = 1000;
|
|
7
8
|
|
|
@@ -263,10 +264,34 @@ export function evictStaleLiveAgentHandles(now = Date.now()): number {
|
|
|
263
264
|
evicted++;
|
|
264
265
|
}
|
|
265
266
|
} else if (age > STALE_RUNNING_HANDLE_MS) {
|
|
266
|
-
// Active-status handle with no update for 30min —
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
267
|
+
// Active-status handle with no update for 30min — check process liveness
|
|
268
|
+
// before evicting. Only evict if the process is dead, to avoid evicting
|
|
269
|
+
// slow-but-alive agents.
|
|
270
|
+
const sessionPid = (handle.session as Record<string, unknown>)?.pid as number | undefined;
|
|
271
|
+
if (sessionPid === undefined) {
|
|
272
|
+
// In-process live-session agents have no pid — the Pi SDK returns
|
|
273
|
+
// in-process objects, not child processes. checkProcessLiveness(undefined)
|
|
274
|
+
// returns {alive:false} which would wrongly evict a running agent.
|
|
275
|
+
// Check session-level activity signals first. If the session is
|
|
276
|
+
// actively streaming or has pending messages, it is alive — skip.
|
|
277
|
+
// Otherwise, rely on the idle timeout alone: do NOT dispose just
|
|
278
|
+
// because pid is undefined.
|
|
279
|
+
const session = handle.session as Record<string, unknown>;
|
|
280
|
+
const isStreaming = session?.isStreaming === true;
|
|
281
|
+
const pendingCount = (session?.pendingMessageCount as number | undefined) ?? 0;
|
|
282
|
+
if (isStreaming || pendingCount > 0) {
|
|
283
|
+
// Session is active — skip eviction.
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
// No activity signals and no pid — do NOT evict based on PID liveness.
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
const liveness = checkProcessLiveness(sessionPid);
|
|
290
|
+
if (!liveness.alive) {
|
|
291
|
+
liveAgents.delete(agentId);
|
|
292
|
+
safeDisposeLiveSession(handle);
|
|
293
|
+
evicted++;
|
|
294
|
+
}
|
|
270
295
|
}
|
|
271
296
|
}
|
|
272
297
|
return evicted;
|
|
@@ -282,7 +307,7 @@ export function listActiveLiveAgents(): LiveAgentHandle[] {
|
|
|
282
307
|
|
|
283
308
|
export function getLiveAgentContextPercent(agentIdOrTaskId: string): number | null {
|
|
284
309
|
const handle = getLiveAgent(agentIdOrTaskId);
|
|
285
|
-
if (
|
|
310
|
+
if (handle?.status !== "running") return null;
|
|
286
311
|
try {
|
|
287
312
|
return handle.session.getSessionStats?.().contextUsage?.percent ?? null;
|
|
288
313
|
} catch {
|
|
@@ -29,8 +29,6 @@
|
|
|
29
29
|
* - setActiveTools / getActiveTools / getAllTools
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
|
-
import type { YieldResult } from "./yield-handler.ts";
|
|
33
|
-
|
|
34
32
|
export interface ExtensionBridgeApis {
|
|
35
33
|
sendMessage: (message: unknown, options?: Record<string, unknown>) => void;
|
|
36
34
|
sendUserMessage: (content: unknown, options?: Record<string, unknown>) => void;
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
} from "./live-agent-manager.ts";
|
|
28
28
|
import { subscribeLiveControlRealtime } from "./live-control-realtime.ts";
|
|
29
29
|
import { buildExtensionBridge } from "./live-extension-bridge.ts";
|
|
30
|
-
import { collectLiveSessionHealth, formatLiveSessionDiagnostics
|
|
30
|
+
import { collectLiveSessionHealth, formatLiveSessionDiagnostics } from "./live-session-health.ts";
|
|
31
31
|
import { buildMcpProxyFromSession } from "./mcp-proxy.ts";
|
|
32
32
|
import { buildConfiguredModelRouting } from "./model-fallback.ts";
|
|
33
33
|
import { readEnabledModelsPatterns } from "./model-scope.ts";
|
|
@@ -231,7 +231,7 @@ function eventText(event: unknown): string[] {
|
|
|
231
231
|
|
|
232
232
|
function finalAssistantText(event: unknown): string[] {
|
|
233
233
|
const obj = asRecord(event);
|
|
234
|
-
if (
|
|
234
|
+
if (obj?.type !== "message_end") return [];
|
|
235
235
|
const message = asRecord(obj.message);
|
|
236
236
|
if (message?.role !== "assistant") return [];
|
|
237
237
|
return textFromContent(message.content);
|
|
@@ -266,7 +266,7 @@ async function resolveScopeModelsPatterns(cwd: string, agentDir?: string): Promi
|
|
|
266
266
|
}
|
|
267
267
|
|
|
268
268
|
function modelFromRegistry(modelRegistry: unknown, modelId: string | undefined): unknown {
|
|
269
|
-
if (!modelId
|
|
269
|
+
if (!modelId?.includes("/")) return undefined;
|
|
270
270
|
const registry = asRecord(modelRegistry);
|
|
271
271
|
const find = registry?.find;
|
|
272
272
|
if (typeof find !== "function") return undefined;
|
|
@@ -450,7 +450,7 @@ function filterActiveTools(session: LiveSessionLike, agent: AgentConfig, role?:
|
|
|
450
450
|
const allowed = policy.tools?.length ? new Set(policy.tools) : undefined;
|
|
451
451
|
const active = session
|
|
452
452
|
.getActiveToolNames()
|
|
453
|
-
.filter((name) => !recursiveTools.has(name) &&
|
|
453
|
+
.filter((name) => !recursiveTools.has(name) && !disallowed?.has(name) && (!allowed || allowed.has(name)));
|
|
454
454
|
session.setActiveToolsByName(active);
|
|
455
455
|
}
|
|
456
456
|
|
|
@@ -822,61 +822,69 @@ export async function runLiveSessionTask(input: LiveSessionSpawnInput): Promise<
|
|
|
822
822
|
});
|
|
823
823
|
if (typeof session.subscribe === "function") {
|
|
824
824
|
unsubscribe = session.subscribe((event) => {
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
turnCount += 1;
|
|
839
|
-
trackLiveAgentTurnEnd(agentId);
|
|
840
|
-
if (maxTurns !== undefined && !softLimitReached && turnCount >= maxTurns) {
|
|
841
|
-
softLimitReached = true;
|
|
842
|
-
void session?.steer?.("You have reached your turn limit. Wrap up immediately — provide your final answer now.");
|
|
843
|
-
} else if (maxTurns !== undefined && softLimitReached && turnCount >= maxTurns + graceTurns) {
|
|
844
|
-
void session?.abort?.();
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
// Accumulate lifetime usage that survives compaction
|
|
848
|
-
if (obj?.type === "message_end" && extractMessageRole(obj) === "assistant") {
|
|
849
|
-
const u = extractMessageUsage(obj);
|
|
850
|
-
if (u) {
|
|
851
|
-
trackTaskUsage(input.task.id, {
|
|
852
|
-
input: typeof u.input === "number" ? u.input : 0,
|
|
853
|
-
output: typeof u.output === "number" ? u.output : 0,
|
|
854
|
-
cacheWrite: typeof u.cacheWrite === "number" ? u.cacheWrite : 0,
|
|
825
|
+
// Wrap callback body in try/catch to prevent unguarded exceptions from
|
|
826
|
+
// propagating into the session internals and crashing the live session.
|
|
827
|
+
try {
|
|
828
|
+
if (!isCurrent()) return;
|
|
829
|
+
jsonEvents += 1;
|
|
830
|
+
appendTranscript(input.transcriptPath, event);
|
|
831
|
+
const sidechainType = eventToSidechainType(event);
|
|
832
|
+
if (sidechainType)
|
|
833
|
+
writeSidechainEntry(sidechainPath, {
|
|
834
|
+
agentId,
|
|
835
|
+
type: sidechainType,
|
|
836
|
+
message: event,
|
|
837
|
+
cwd: input.task.cwd,
|
|
855
838
|
});
|
|
839
|
+
const obj = asRecord(event);
|
|
840
|
+
if (obj?.type === "turn_end") {
|
|
841
|
+
turnCount += 1;
|
|
842
|
+
trackLiveAgentTurnEnd(agentId);
|
|
843
|
+
if (maxTurns !== undefined && !softLimitReached && turnCount >= maxTurns) {
|
|
844
|
+
softLimitReached = true;
|
|
845
|
+
void session?.steer?.("You have reached your turn limit. Wrap up immediately — provide your final answer now.");
|
|
846
|
+
} else if (maxTurns !== undefined && softLimitReached && turnCount >= maxTurns + graceTurns) {
|
|
847
|
+
void session?.abort?.();
|
|
848
|
+
}
|
|
856
849
|
}
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
850
|
+
// Accumulate lifetime usage that survives compaction
|
|
851
|
+
if (obj?.type === "message_end" && extractMessageRole(obj) === "assistant") {
|
|
852
|
+
const u = extractMessageUsage(obj);
|
|
853
|
+
if (u) {
|
|
854
|
+
trackTaskUsage(input.task.id, {
|
|
855
|
+
input: typeof u.input === "number" ? u.input : 0,
|
|
856
|
+
output: typeof u.output === "number" ? u.output : 0,
|
|
857
|
+
cacheWrite: typeof u.cacheWrite === "number" ? u.cacheWrite : 0,
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
input.onEvent?.(event);
|
|
862
|
+
const text = [...eventText(event), ...finalAssistantText(event)].join("\n");
|
|
863
|
+
if (text.trim()) {
|
|
864
|
+
stdout += `${text}\n`;
|
|
865
|
+
streamOut?.write(text + "\n");
|
|
866
|
+
trackLiveAgentResponseText(agentId, text);
|
|
867
|
+
input.onOutput?.(text);
|
|
868
|
+
}
|
|
869
|
+
// G2: Track tool start/end for activity display
|
|
870
|
+
if (obj?.type === "tool_use" || obj?.type === "tool_execution_start") {
|
|
871
|
+
const toolName = extractToolName(obj) ?? "unknown";
|
|
872
|
+
trackLiveAgentToolStart(agentId, toolName);
|
|
873
|
+
}
|
|
874
|
+
if (obj?.type === "tool_result" || obj?.type === "tool_execution_end") {
|
|
875
|
+
const toolName = extractToolName(obj) ?? "unknown";
|
|
876
|
+
trackLiveAgentToolEnd(agentId, toolName);
|
|
877
|
+
}
|
|
878
|
+
// Phase 1: collect events for yield detection
|
|
879
|
+
if (collectedJsonEvents && event && typeof event === "object" && !Array.isArray(event)) {
|
|
880
|
+
collectedJsonEvents.push(event as Record<string, unknown>);
|
|
881
|
+
if (collectedJsonEvents.length > maxCollectedJsonEvents)
|
|
882
|
+
collectedJsonEvents.splice(0, collectedJsonEvents.length - maxCollectedJsonEvents);
|
|
883
|
+
}
|
|
884
|
+
} catch (error) {
|
|
885
|
+
// Prevent unguarded exceptions in the subscribe callback from crashing
|
|
886
|
+
// the live session or propagating into the session internals.
|
|
887
|
+
logInternalError("live-session.subscribe-callback", error, `agentId=${agentId}`);
|
|
880
888
|
}
|
|
881
889
|
});
|
|
882
890
|
}
|
|
@@ -9,6 +9,15 @@ import { isSafePathId, resolveContainedRelativePath, resolveRealContainedPath }
|
|
|
9
9
|
|
|
10
10
|
export interface ManifestCache {
|
|
11
11
|
list(limit?: number): TeamRunManifest[];
|
|
12
|
+
/**
|
|
13
|
+
* Return ONLY running manifests, capped only by `limit`.
|
|
14
|
+
*
|
|
15
|
+
* RT-F3: unlike `list(limit)`, this filters `status === "running"` BEFORE
|
|
16
|
+
* the limit is applied so stale orphaned runs that have been pushed past
|
|
17
|
+
* the top-N by recent activity are not silently hidden from crash-recovery
|
|
18
|
+
* and zombie-detection scans.
|
|
19
|
+
*/
|
|
20
|
+
listActive(limit: number): TeamRunManifest[];
|
|
12
21
|
get(runId: string): TeamRunManifest | undefined;
|
|
13
22
|
clear(runId?: string): void;
|
|
14
23
|
dispose(): void;
|
|
@@ -203,6 +212,15 @@ export function createManifestCache(cwd: string, options: ManifestCacheOptions =
|
|
|
203
212
|
return undefined;
|
|
204
213
|
}
|
|
205
214
|
|
|
215
|
+
/**
|
|
216
|
+
* NOTE (RT-F10): This function performs a full FS scan + JSON.parse of all
|
|
217
|
+
* manifests on every TTL expiry (default 500ms). This is a known performance
|
|
218
|
+
* tradeoff: fs.watch provides real-time invalidation (see constructor above)
|
|
219
|
+
* so the TTL cache is only consulted when the watcher fires, but the list()
|
|
220
|
+
* call itself still re-reads all entries to pick up changes. Incremental
|
|
221
|
+
* updates via fs.watch-driven delta tracking are planned for a future
|
|
222
|
+
* iteration to avoid the full scan on each expiry.
|
|
223
|
+
*/
|
|
206
224
|
function list(limit = DEFAULT_CACHE.manifestMaxEntries): TeamRunManifest[] {
|
|
207
225
|
const now = Date.now();
|
|
208
226
|
const cached = listCache.get(limit);
|
|
@@ -248,6 +266,46 @@ export function createManifestCache(cwd: string, options: ManifestCacheOptions =
|
|
|
248
266
|
return undefined;
|
|
249
267
|
}
|
|
250
268
|
|
|
269
|
+
/**
|
|
270
|
+
* RT-F3: filter to `status === "running"` BEFORE applying the limit so an
|
|
271
|
+
* orphaned run that has been pushed past the top-N by recent successful
|
|
272
|
+
* runs is still surfaced to crash-recovery. The cap is the LIMIT, not the
|
|
273
|
+
* "top-N most recent"; callers (e.g. zombie detection) explicitly want
|
|
274
|
+
* EVERY currently-running run, in any order.
|
|
275
|
+
*
|
|
276
|
+
* Internally performs a full scan (NOT via list()) so the cap doesn't
|
|
277
|
+
* silently drop "running" runs that fell past the top-N createdAt cutoff.
|
|
278
|
+
* Still goes through parseManifestIfChanged for stat+size memoization, so
|
|
279
|
+
* the per-run I/O cost is the same as list().
|
|
280
|
+
*/
|
|
281
|
+
function listActive(limit: number): TeamRunManifest[] {
|
|
282
|
+
const cap = Math.max(0, limit);
|
|
283
|
+
const parsedEntries = [
|
|
284
|
+
...roots.flatMap((root) => collectRoots(root)),
|
|
285
|
+
...activeRunEntries().map((entry) => ({
|
|
286
|
+
runId: entry.runId,
|
|
287
|
+
path: entry.manifestPath,
|
|
288
|
+
})),
|
|
289
|
+
];
|
|
290
|
+
const unique = new Map<string, CachedManifest | undefined>();
|
|
291
|
+
for (const entry of parsedEntries) {
|
|
292
|
+
if (entry.runId.length === 0) continue;
|
|
293
|
+
let cached = manifestIndex.get(entry.runId);
|
|
294
|
+
const root = path.dirname(path.dirname(entry.path));
|
|
295
|
+
const parsed = parseManifestIfChanged(root, entry.runId, entry.path, cached);
|
|
296
|
+
if (parsed) {
|
|
297
|
+
cached = parsed;
|
|
298
|
+
manifestIndex.set(entry.runId, cached);
|
|
299
|
+
}
|
|
300
|
+
if (cached) unique.set(entry.runId, cached);
|
|
301
|
+
}
|
|
302
|
+
const running = [...unique.values()]
|
|
303
|
+
.filter((value): value is CachedManifest => value !== undefined)
|
|
304
|
+
.map((value) => value.manifest)
|
|
305
|
+
.filter((manifest) => manifest.status === "running");
|
|
306
|
+
return running.slice(0, cap);
|
|
307
|
+
}
|
|
308
|
+
|
|
251
309
|
if (options.watch ?? true) {
|
|
252
310
|
for (const root of roots) {
|
|
253
311
|
const watcher = watchWithErrorHandler(
|
|
@@ -268,6 +326,7 @@ export function createManifestCache(cwd: string, options: ManifestCacheOptions =
|
|
|
268
326
|
|
|
269
327
|
return {
|
|
270
328
|
list,
|
|
329
|
+
listActive,
|
|
271
330
|
get,
|
|
272
331
|
clear(runId) {
|
|
273
332
|
invalidate(runId);
|
package/src/runtime/mcp-proxy.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
20
|
-
import type {
|
|
20
|
+
import type { TSchema } from "@sinclair/typebox";
|
|
21
21
|
|
|
22
22
|
export interface McpProxyConfig {
|
|
23
23
|
/** Whether to enable MCP in the child session. */
|
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
* writes from causing lost updates.
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
import { execFileSync
|
|
25
|
+
import { execFileSync } from "node:child_process";
|
|
26
26
|
import * as fs from "node:fs";
|
|
27
27
|
import * as path from "node:path";
|
|
28
|
-
import { isSymlinkSafePath } from "../state/atomic-write.ts";
|
|
28
|
+
import { atomicWriteFile, isSymlinkSafePath } from "../state/atomic-write.ts";
|
|
29
29
|
import { withFileLockSync } from "../state/locks.ts";
|
|
30
30
|
import { logInternalError } from "../utils/internal-error.ts";
|
|
31
31
|
import { userPiRoot } from "../utils/paths.ts";
|
|
@@ -252,7 +252,7 @@ function writeRegistry(entries: OrphanWorkerEntry[]): void {
|
|
|
252
252
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
253
253
|
}
|
|
254
254
|
try {
|
|
255
|
-
|
|
255
|
+
atomicWriteFile(p, JSON.stringify(entries, null, 2), { mode: 0o600 });
|
|
256
256
|
} catch (error) {
|
|
257
257
|
logInternalError("orphan-worker-registry.write", error, `path=${p} entries=${entries.length}`);
|
|
258
258
|
}
|
|
@@ -42,7 +42,7 @@ export function validateWorkerOutput(role: string, output: string): OutputValida
|
|
|
42
42
|
const issues: string[] = [];
|
|
43
43
|
|
|
44
44
|
// Empty output always fails
|
|
45
|
-
if (!output
|
|
45
|
+
if (!output?.trim()) {
|
|
46
46
|
return {
|
|
47
47
|
valid: false,
|
|
48
48
|
formatMatch: false,
|
|
@@ -124,7 +124,7 @@ export function filterReadyByWriteOverlap(
|
|
|
124
124
|
for (const taskId of readyTaskIds) {
|
|
125
125
|
if (pickedTaskIds.length >= maxCount) break;
|
|
126
126
|
const task = taskById.get(taskId);
|
|
127
|
-
if (!task
|
|
127
|
+
if (!task?.stepId) {
|
|
128
128
|
// No step reference: treat as no declared write path → never conflicts.
|
|
129
129
|
pickedTaskIds.push(taskId);
|
|
130
130
|
continue;
|
package/src/runtime/pi-args.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
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 { fileURLToPath } from "node:url";
|
|
5
4
|
import type { AgentConfig } from "../agents/agent-config.ts";
|
|
6
5
|
import { resolveToolPolicy } from "../agents/agent-config.ts";
|
|
6
|
+
import { atomicWriteFile } from "../state/atomic-write.ts";
|
|
7
7
|
import { packageRoot, userPiRoot } from "../utils/paths.ts";
|
|
8
8
|
|
|
9
9
|
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"];
|
|
@@ -323,7 +323,7 @@ export function buildPiWorkerArgs(input: BuildPiWorkerArgsInput): BuildPiWorkerA
|
|
|
323
323
|
const tmpBase = getPiTempBase();
|
|
324
324
|
tempDir = createSafeTempDir(tmpBase, `pi-crew-${process.pid}-`);
|
|
325
325
|
const promptPath = path.join(tempDir, `${input.agent.name.replace(/[^\w.-]/g, "_")}.md`);
|
|
326
|
-
|
|
326
|
+
atomicWriteFile(promptPath, input.agent.systemPrompt, { mode: 0o600 });
|
|
327
327
|
args.push(input.agent.systemPromptMode === "append" ? "--append-system-prompt" : "--system-prompt", promptPath);
|
|
328
328
|
}
|
|
329
329
|
|
|
@@ -333,7 +333,7 @@ export function buildPiWorkerArgs(input: BuildPiWorkerArgsInput): BuildPiWorkerA
|
|
|
333
333
|
tempDir = createSafeTempDir(tmpBase, `pi-crew-${process.pid}-`);
|
|
334
334
|
}
|
|
335
335
|
const taskPath = path.join(tempDir, "task.md");
|
|
336
|
-
|
|
336
|
+
atomicWriteFile(taskPath, input.task, { mode: 0o600 });
|
|
337
337
|
args.push(`@${taskPath}`);
|
|
338
338
|
} else {
|
|
339
339
|
args.push(`Task: ${input.task}`);
|
|
@@ -55,7 +55,8 @@ function extractUsage(value: unknown): ParsedPiUsage | undefined {
|
|
|
55
55
|
turns: numberField(obj, ["turns", "turnCount", "turn_count"]),
|
|
56
56
|
};
|
|
57
57
|
if (Object.values(direct).some((entry) => entry !== undefined)) return direct;
|
|
58
|
-
|
|
58
|
+
// Pi --mode json nests usage under message for message_end / turn_end events.
|
|
59
|
+
for (const key of ["usage", "message", "tokenUsage", "tokens", "stats"]) {
|
|
59
60
|
const nested = extractUsage(obj[key]);
|
|
60
61
|
if (nested) return nested;
|
|
61
62
|
}
|
|
@@ -151,14 +152,14 @@ function extractErrorMessage(event: unknown): string | undefined {
|
|
|
151
152
|
*/
|
|
152
153
|
function extractPatch(event: unknown, patches: string[]): void {
|
|
153
154
|
const obj = asRecord(event);
|
|
154
|
-
if (
|
|
155
|
+
if (obj?.type !== "tool_result") return;
|
|
155
156
|
|
|
156
157
|
const content = obj.content;
|
|
157
158
|
if (!Array.isArray(content)) return;
|
|
158
159
|
|
|
159
160
|
for (const item of content) {
|
|
160
161
|
const part = asRecord(item);
|
|
161
|
-
if (
|
|
162
|
+
if (part?.type !== "text") continue;
|
|
162
163
|
const text = typeof part.text === "string" ? part.text : "";
|
|
163
164
|
|
|
164
165
|
// Check if this looks like a unified patch (starts with "---" or "+++")
|
package/src/runtime/pi-spawn.ts
CHANGED
|
@@ -189,7 +189,13 @@ function resolvePiCliScript(): string | undefined {
|
|
|
189
189
|
const argv1 = process.argv[1];
|
|
190
190
|
if (argv1) {
|
|
191
191
|
const argvPath = path.isAbsolute(argv1) ? argv1 : path.resolve(argv1);
|
|
192
|
-
if
|
|
192
|
+
// Only trust argv1 if we can confirm the current process is running from
|
|
193
|
+
// the pi-coding-agent package directory. Otherwise, when pi-crew is invoked
|
|
194
|
+
// from a standalone test script (process.argv[1] = test script path),
|
|
195
|
+
// resolvePiCliScript would incorrectly return the test script as the pi
|
|
196
|
+
// CLI. resolvePiPackageRoot walks up from argv1 looking for a package.json
|
|
197
|
+
// named @earendil-works/pi-coding-agent or @mariozechner/pi-coding-agent.
|
|
198
|
+
if (resolvePiPackageRoot() && isRunnableNodeScript(argvPath)) return argvPath;
|
|
193
199
|
}
|
|
194
200
|
|
|
195
201
|
// npm-global package dirs derived from `npm root -g` — placed BEFORE the
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import type { AgentConfig } from "../agents/agent-config.ts";
|
|
2
1
|
import { errors } from "../errors.ts";
|
|
3
2
|
import { appendEventAsync, flushEventLogBuffer } from "../state/event-log.ts";
|
|
4
|
-
import type {
|
|
5
|
-
import type { TeamConfig } from "../teams/team-config.ts";
|
|
6
|
-
import type { WorkflowConfig, WorkflowStep } from "../workflows/workflow-config.ts";
|
|
3
|
+
import type { WorkflowConfig } from "../workflows/workflow-config.ts";
|
|
7
4
|
import { mapConcurrent } from "./parallel-utils.ts";
|
|
8
5
|
|
|
9
6
|
/**
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
* Distilled from pi-autoresearch's post-check / backpressure pattern.
|
|
6
6
|
*/
|
|
7
7
|
import { execFileSync } from "node:child_process";
|
|
8
|
-
import * as path from "node:path";
|
|
9
8
|
import { WINDOWS_ESSENTIAL_ENV_VARS } from "../utils/env-allowlist.ts";
|
|
10
9
|
import { sanitizeEnvSecrets } from "../utils/env-filter.ts";
|
|
11
10
|
import { resolveShellForScript } from "../utils/resolve-shell.ts";
|
|
@@ -70,10 +70,10 @@ export interface TaskRunnerLike {
|
|
|
70
70
|
*/
|
|
71
71
|
export class RetryRunner {
|
|
72
72
|
private _disposed = false;
|
|
73
|
-
private _handoffs: HandoffSummary[] = [];
|
|
74
73
|
|
|
75
74
|
private taskRunner: TaskRunnerLike;
|
|
76
75
|
private handoffManager: HandoffManager;
|
|
76
|
+
private _handoffs: HandoffSummary[] = [];
|
|
77
77
|
|
|
78
78
|
constructor(taskRunner: TaskRunnerLike, handoffManager: HandoffManager) {
|
|
79
79
|
this.taskRunner = taskRunner;
|