pi-crew 0.9.41 → 0.9.42
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 +106 -0
- package/dist/build-meta.json +7087 -6726
- package/dist/index.mjs +43043 -42426
- 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/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/config/config.ts +19 -0
- package/src/config/drift-detector.ts +97 -15
- package/src/config/resilient-parser.ts +4 -4
- package/src/extension/crew-cleanup.ts +17 -3
- package/src/extension/crew-vibes/index.ts +1 -1
- package/src/extension/cross-extension-rpc.ts +18 -5
- 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 +2 -1
- 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 +210 -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 +64 -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/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.ts +22 -2
- package/src/runtime/coalesce-tasks.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/foreground-control.ts +3 -2
- package/src/runtime/goal-loop-runner.ts +6 -2
- 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 +12 -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 +2 -2
- 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 +0 -13
- 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 +1 -1
- 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 +3 -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 +3 -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/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/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/project-detector.ts +1 -1
- package/src/state/atomic-write-v2.ts +0 -108
|
@@ -21,7 +21,6 @@ export interface DeliveryCoordinatorDeps {
|
|
|
21
21
|
const PENDING_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
22
22
|
|
|
23
23
|
export class DeliveryCoordinator {
|
|
24
|
-
private ownerSessionId: string | undefined;
|
|
25
24
|
private active = false;
|
|
26
25
|
private generation = 0;
|
|
27
26
|
private pending: PendingDelivery[] = [];
|
|
@@ -29,6 +28,7 @@ export class DeliveryCoordinator {
|
|
|
29
28
|
private readonly deps: DeliveryCoordinatorDeps;
|
|
30
29
|
private ttlTimer: ReturnType<typeof setInterval> | undefined;
|
|
31
30
|
private timerStarted = false;
|
|
31
|
+
private ownerSessionId: string | undefined = undefined;
|
|
32
32
|
|
|
33
33
|
constructor(deps: DeliveryCoordinatorDeps) {
|
|
34
34
|
this.deps = deps;
|
|
@@ -120,9 +120,9 @@ function isNewDateExpression(node: AstNode): boolean {
|
|
|
120
120
|
*/
|
|
121
121
|
function isMemberExpression(node: AstNode, childKey: string, objectName: string, propertyName: string): boolean {
|
|
122
122
|
const child = asAstNode(node[childKey]);
|
|
123
|
-
if (
|
|
123
|
+
if (child?.type !== "MemberExpression") return false;
|
|
124
124
|
const object = asAstNode(child.object);
|
|
125
|
-
if (
|
|
125
|
+
if (object?.type !== "Identifier" || object.name !== objectName) return false;
|
|
126
126
|
return propertyNameOf(child) === propertyName;
|
|
127
127
|
}
|
|
128
128
|
|
|
@@ -3,6 +3,7 @@ import * as path from "node:path";
|
|
|
3
3
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import type { MetricRegistry } from "../observability/metric-registry.ts";
|
|
5
5
|
import type { MetricSnapshot } from "../observability/metrics-primitives.ts";
|
|
6
|
+
import { atomicWriteFile } from "../state/atomic-write.ts";
|
|
6
7
|
import { readEvents, type TeamEvent } from "../state/event-log.ts";
|
|
7
8
|
import { loadRunManifestById } from "../state/state-store.ts";
|
|
8
9
|
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
@@ -148,7 +149,7 @@ export async function exportDiagnostic(
|
|
|
148
149
|
const dir = path.join(loaded.manifest.artifactsRoot, "diagnostic");
|
|
149
150
|
fs.mkdirSync(dir, { recursive: true });
|
|
150
151
|
const filePath = path.join(dir, `diagnostic-${safeTimestamp}.json`);
|
|
151
|
-
|
|
152
|
+
atomicWriteFile(filePath, `${JSON.stringify(report, null, 2)}\n`);
|
|
152
153
|
return { path: filePath, report };
|
|
153
154
|
}
|
|
154
155
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import { atomicWriteFile } from "../state/atomic-write.ts";
|
|
3
4
|
import { appendEvent } from "../state/event-log.ts";
|
|
4
5
|
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
5
6
|
import { logInternalError } from "../utils/internal-error.ts";
|
|
@@ -82,7 +83,7 @@ export function writeForegroundInterruptRequest(
|
|
|
82
83
|
try {
|
|
83
84
|
fs.mkdirSync(lockDir, { recursive: true });
|
|
84
85
|
try {
|
|
85
|
-
|
|
86
|
+
atomicWriteFile(pidFile, String(process.pid));
|
|
86
87
|
} catch {
|
|
87
88
|
/* best-effort */
|
|
88
89
|
}
|
|
@@ -178,7 +179,7 @@ export function writeForegroundInterruptRequest(
|
|
|
178
179
|
acknowledged: false,
|
|
179
180
|
};
|
|
180
181
|
fs.mkdirSync(path.dirname(controlPath), { recursive: true });
|
|
181
|
-
|
|
182
|
+
atomicWriteFile(controlPath, `${JSON.stringify({ requests: [...requests, request] }, null, 2)}\n`);
|
|
182
183
|
appendEvent(manifest.eventsPath, {
|
|
183
184
|
type: "foreground.interrupt_requested",
|
|
184
185
|
runId: manifest.runId,
|
|
@@ -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";
|
|
@@ -260,8 +261,11 @@ function resolveGoalTurnWorkflow(goal: GoalLoopState): WorkflowConfig {
|
|
|
260
261
|
const wrapName = (goal as GoalLoopState & { goalWrapWorkflow?: string }).goalWrapWorkflow;
|
|
261
262
|
if (!wrapName) return buildTurnWorkflow();
|
|
262
263
|
try {
|
|
263
|
-
|
|
264
|
-
|
|
264
|
+
// LAZY: createRequire for synchronous module resolution in ESM context
|
|
265
|
+
const requireFromHere = createRequire(import.meta.url);
|
|
266
|
+
const { discoverWorkflows, allWorkflows } = requireFromHere(
|
|
267
|
+
"../workflows/discover-workflows.ts",
|
|
268
|
+
) as typeof import("../workflows/discover-workflows.ts");
|
|
265
269
|
const found = allWorkflows(discoverWorkflows(goal.cwd)).find((w) => w.name === wrapName && w.source === "builtin");
|
|
266
270
|
if (found) return found;
|
|
267
271
|
logInternalError(
|
|
@@ -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,16 @@ 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
|
+
const liveness = checkProcessLiveness(sessionPid);
|
|
272
|
+
if (!liveness.alive) {
|
|
273
|
+
liveAgents.delete(agentId);
|
|
274
|
+
safeDisposeLiveSession(handle);
|
|
275
|
+
evicted++;
|
|
276
|
+
}
|
|
270
277
|
}
|
|
271
278
|
}
|
|
272
279
|
return evicted;
|
|
@@ -282,7 +289,7 @@ export function listActiveLiveAgents(): LiveAgentHandle[] {
|
|
|
282
289
|
|
|
283
290
|
export function getLiveAgentContextPercent(agentIdOrTaskId: string): number | null {
|
|
284
291
|
const handle = getLiveAgent(agentIdOrTaskId);
|
|
285
|
-
if (
|
|
292
|
+
if (handle?.status !== "running") return null;
|
|
286
293
|
try {
|
|
287
294
|
return handle.session.getSessionStats?.().contextUsage?.percent ?? null;
|
|
288
295
|
} 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}`);
|
|
@@ -151,14 +151,14 @@ function extractErrorMessage(event: unknown): string | undefined {
|
|
|
151
151
|
*/
|
|
152
152
|
function extractPatch(event: unknown, patches: string[]): void {
|
|
153
153
|
const obj = asRecord(event);
|
|
154
|
-
if (
|
|
154
|
+
if (obj?.type !== "tool_result") return;
|
|
155
155
|
|
|
156
156
|
const content = obj.content;
|
|
157
157
|
if (!Array.isArray(content)) return;
|
|
158
158
|
|
|
159
159
|
for (const item of content) {
|
|
160
160
|
const part = asRecord(item);
|
|
161
|
-
if (
|
|
161
|
+
if (part?.type !== "text") continue;
|
|
162
162
|
const text = typeof part.text === "string" ? part.text : "";
|
|
163
163
|
|
|
164
164
|
// Check if this looks like a unified patch (starts with "---" or "+++")
|
|
@@ -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;
|
|
@@ -1,16 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* run-coalesced-task-group.ts — M6 real-dispatch MVP worker spawn.
|
|
3
|
-
*
|
|
4
|
-
* Spawns ONE child Pi process for an entire coalesced group of N tasks
|
|
5
|
-
* (sharing role + cwd per `planCoalescedGroups`). The combined prompt
|
|
6
|
-
* instructs the worker to wrap each task's result in `<<<TASK_RESULT:id>>>`
|
|
7
|
-
* ... `<<<END_TASK_RESULT>>>` delimiters. Output is split back into N
|
|
8
|
-
* per-task results via `splitCoalescedOutput`, each written to its own
|
|
9
|
-
* result artifact + state update.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { writeFile } from "node:fs/promises";
|
|
13
|
-
import path from "node:path";
|
|
14
1
|
import type { AgentConfig } from "../agents/agent-config.ts";
|
|
15
2
|
import { writeArtifact } from "../state/artifact-store.ts";
|
|
16
3
|
import { appendEventAsync } from "../state/event-log.ts";
|
package/src/runtime/scheduler.ts
CHANGED
|
@@ -111,6 +111,7 @@ export class CrewScheduler {
|
|
|
111
111
|
if (this.timers.has(job.id)) return;
|
|
112
112
|
if (job.scheduleType === "interval" && job.intervalMs) {
|
|
113
113
|
const t = setInterval(() => this.fire(job.id), job.intervalMs);
|
|
114
|
+
t.unref();
|
|
114
115
|
this.timers.set(job.id, t);
|
|
115
116
|
} else if (job.scheduleType === "once") {
|
|
116
117
|
const target = new Date(job.schedule).getTime();
|
|
@@ -120,6 +121,7 @@ export class CrewScheduler {
|
|
|
120
121
|
this.fire(job.id);
|
|
121
122
|
this.update(job.id, { enabled: false });
|
|
122
123
|
}, delay);
|
|
124
|
+
t.unref();
|
|
123
125
|
this.timers.set(job.id, t);
|
|
124
126
|
} else {
|
|
125
127
|
this.update(job.id, { enabled: false, lastStatus: "error" });
|
|
@@ -9,7 +9,7 @@ import { logInternalError } from "../utils/internal-error.ts";
|
|
|
9
9
|
*/
|
|
10
10
|
export function tryRegisterSessionCleanup(pi: ExtensionAPI, cleanup: () => void): (() => void) | undefined {
|
|
11
11
|
const api = pi as unknown as Record<string, unknown>;
|
|
12
|
-
const registerFn = api
|
|
12
|
+
const registerFn = api.registerSessionResourceCleanup;
|
|
13
13
|
if (typeof registerFn === "function") {
|
|
14
14
|
try {
|
|
15
15
|
const unregister = (registerFn as (fn: () => void) => (() => void) | void)(cleanup);
|