pi-crew 0.9.40 → 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 +140 -0
- package/dist/build-meta.json +7089 -6728
- package/dist/index.mjs +45845 -45192
- package/dist/index.mjs.map +4 -4
- package/docs/migration/atomic-write-v2-migration.md +2 -0
- package/docs/optimization-plan-2026-07.md +366 -0
- package/docs/phase4-triage.md +58 -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 +8 -8
- 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 +157 -71
- 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/heartbeat-watcher.ts +5 -0
- package/src/runtime/intercom-bridge.ts +0 -2
- package/src/runtime/live-agent-manager.ts +12 -32
- 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
|
@@ -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
|
|
|
@@ -83,14 +84,6 @@ export function listLiveAgentsByWorkspace(workspaceId: string): LiveAgentHandle[
|
|
|
83
84
|
return listLiveAgents().filter((a) => a.workspaceId === workspaceId);
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
/**
|
|
87
|
-
* List only active agents (running/queued/waiting) for a specific workspace.
|
|
88
|
-
*/
|
|
89
|
-
/** @internal */
|
|
90
|
-
function listActiveLiveAgentsByWorkspace(workspaceId: string): LiveAgentHandle[] {
|
|
91
|
-
return listActiveLiveAgents().filter((a) => a.workspaceId === workspaceId);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
87
|
export function registerLiveAgent(
|
|
95
88
|
input: Omit<LiveAgentHandle, "createdAt" | "updatedAt" | "pendingSteers" | "pendingFollowUps" | "pendingMessages" | "activity"> & {
|
|
96
89
|
workspaceId: string;
|
|
@@ -189,15 +182,6 @@ function safeDisposeLiveSession(handle: LiveAgentHandle): void {
|
|
|
189
182
|
}
|
|
190
183
|
}
|
|
191
184
|
|
|
192
|
-
/** @internal */
|
|
193
|
-
function removeLiveAgentHandle(agentId: string): LiveAgentHandle | undefined {
|
|
194
|
-
const handle = liveAgents.get(agentId);
|
|
195
|
-
if (!handle) return undefined;
|
|
196
|
-
liveAgents.delete(agentId);
|
|
197
|
-
safeDisposeLiveSession(handle);
|
|
198
|
-
return handle;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
185
|
export function disposeLiveAgentSession(agentIdOrTaskId: string): void {
|
|
202
186
|
const handle = getLiveAgent(agentIdOrTaskId);
|
|
203
187
|
if (!handle) return;
|
|
@@ -280,10 +264,16 @@ export function evictStaleLiveAgentHandles(now = Date.now()): number {
|
|
|
280
264
|
evicted++;
|
|
281
265
|
}
|
|
282
266
|
} else if (age > STALE_RUNNING_HANDLE_MS) {
|
|
283
|
-
// Active-status handle with no update for 30min —
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
+
}
|
|
287
277
|
}
|
|
288
278
|
}
|
|
289
279
|
return evicted;
|
|
@@ -299,7 +289,7 @@ export function listActiveLiveAgents(): LiveAgentHandle[] {
|
|
|
299
289
|
|
|
300
290
|
export function getLiveAgentContextPercent(agentIdOrTaskId: string): number | null {
|
|
301
291
|
const handle = getLiveAgent(agentIdOrTaskId);
|
|
302
|
-
if (
|
|
292
|
+
if (handle?.status !== "running") return null;
|
|
303
293
|
try {
|
|
304
294
|
return handle.session.getSessionStats?.().contextUsage?.percent ?? null;
|
|
305
295
|
} catch {
|
|
@@ -492,16 +482,6 @@ export function broadcastIrcMessage(fromAgentId: string, message: IrcMessage): s
|
|
|
492
482
|
return recipients;
|
|
493
483
|
}
|
|
494
484
|
|
|
495
|
-
/** Phase 7: Get pending IRC messages for an agent (and clear them). */
|
|
496
|
-
/** @internal */
|
|
497
|
-
function drainIrcMessages(agentIdOrTaskId: string): IrcMessage[] {
|
|
498
|
-
const handle = getLiveAgent(agentIdOrTaskId);
|
|
499
|
-
if (!handle) return [];
|
|
500
|
-
const messages = [...handle.pendingMessages];
|
|
501
|
-
handle.pendingMessages.length = 0;
|
|
502
|
-
return messages;
|
|
503
|
-
}
|
|
504
|
-
|
|
505
485
|
/* ── IRC reply support (side-channel Q&A) ─────────────────────────── */
|
|
506
486
|
|
|
507
487
|
/** Default timeout for awaiting a side-channel reply (60s). */
|
|
@@ -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);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
+
import { atomicWriteJson } from "../state/atomic-write.ts";
|
|
5
|
+
import { withFileLockSync } from "../state/locks.ts";
|
|
4
6
|
import { logInternalError } from "../utils/internal-error.ts";
|
|
5
7
|
import type { JoinMode } from "./group-join.ts";
|
|
6
8
|
|
|
@@ -98,13 +100,41 @@ export function saveCrewSettings(s: CrewSettings, cwd: string = process.cwd()):
|
|
|
98
100
|
const p = projectPath(cwd);
|
|
99
101
|
try {
|
|
100
102
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
101
|
-
|
|
103
|
+
// Atomic (temp + rename + fsync) instead of raw writeFileSync: a crash/SIGKILL
|
|
104
|
+
// mid-write would otherwise truncate <cwd>/.pi/crew-settings.json, and the
|
|
105
|
+
// reader silently resets to {} on parse failure — losing ALL crew settings +
|
|
106
|
+
// scheduledJobs.
|
|
107
|
+
atomicWriteJson(p, s);
|
|
102
108
|
return true;
|
|
103
109
|
} catch {
|
|
104
110
|
return false;
|
|
105
111
|
}
|
|
106
112
|
}
|
|
107
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Atomically read-modify-write crew settings under a file lock.
|
|
116
|
+
*
|
|
117
|
+
* Fixes two issues vs a plain loadCrewSettings → saveCrewSettings sequence:
|
|
118
|
+
* 1. Cross-session lost-update race: <cwd>/.pi/crew-settings.json is project-scoped,
|
|
119
|
+
* so two Pi sessions on the same project concurrently scheduling jobs would each
|
|
120
|
+
* load-modify-save and the second save clobbers the first (lost job). The lock
|
|
121
|
+
* serializes the whole transaction.
|
|
122
|
+
* 2. Crash truncation: the write uses atomicWriteJson (temp + rename + fsync).
|
|
123
|
+
*
|
|
124
|
+
* `mutator` receives the freshly-loaded (merged) settings and returns the new value
|
|
125
|
+
* to persist. Returns the persisted settings.
|
|
126
|
+
*/
|
|
127
|
+
export function updateCrewSettings(cwd: string, mutator: (settings: CrewSettings) => CrewSettings): CrewSettings {
|
|
128
|
+
const p = projectPath(cwd);
|
|
129
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
130
|
+
return withFileLockSync(p, () => {
|
|
131
|
+
const fresh = loadCrewSettings(cwd); // re-read inside the lock (TOCTOU-safe vs other updaters)
|
|
132
|
+
const next = mutator(fresh);
|
|
133
|
+
atomicWriteJson(p, next);
|
|
134
|
+
return next;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
108
138
|
export function applyCrewSettingsToConfig(
|
|
109
139
|
config: {
|
|
110
140
|
limits?: { maxConcurrentWorkers?: number };
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* @module skill-effectiveness
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
14
|
-
import { dirname, join } from "path";
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { dirname, join } from "node:path";
|
|
15
15
|
import { projectCrewRoot } from "../utils/paths.ts";
|
|
16
16
|
import { crewHooks } from "./crew-hooks.ts";
|
|
17
17
|
|