pi-extended-teams 2.2.9 → 2.3.0
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/README.md +182 -2
- package/extensions/agents/read-agent-report.ts +11 -2
- package/extensions/agents/read-agent-session-lifecycle.ts +25 -10
- package/extensions/agents/read-agent.ts +387 -65
- package/extensions/agents/write-agent.ts +3 -0
- package/extensions/events/register-events.ts +22 -10
- package/extensions/index.ts +132 -38
- package/extensions/internal/pi-check-operations.ts +24 -0
- package/extensions/internal/session-files.ts +10 -2
- package/extensions/runtime/teammate-interrupt.ts +11 -5
- package/extensions/runtime/types.ts +4 -0
- package/extensions/team/lifecycle.ts +26 -0
- package/extensions/team/session-cost.ts +155 -0
- package/extensions/tools/agent-communication-tools.ts +24 -8
- package/extensions/tools/agent-status-tool.ts +50 -7
- package/extensions/tools/coordination-tools.ts +78 -8
- package/extensions/tools/task-runtime-tools.ts +23 -4
- package/extensions/tools/team-tools.ts +287 -40
- package/extensions/ui/activity-colors.ts +59 -0
- package/extensions/ui/agent-follow-view.ts +22 -12
- package/extensions/ui/agent-navigation.ts +35 -1
- package/extensions/ui/checkpoints-command.ts +28 -0
- package/extensions/ui/extensions-command.ts +5 -1
- package/extensions/ui/onboarding-command.ts +218 -0
- package/extensions/ui/status-widget.ts +22 -14
- package/package.json +1 -1
- package/skills/teams.md +16 -0
- package/src/orchestration/index.ts +19 -1
- package/src/orchestration/status-projection.ts +2 -3
- package/src/orchestration/types.ts +9 -1
- package/src/results/check-journal.ts +149 -0
- package/src/results/check-policy.ts +86 -0
- package/src/results/check-runner.ts +110 -0
- package/src/results/checkpoint-assignment.ts +60 -0
- package/src/results/checkpoint-policy.ts +20 -0
- package/src/results/checkpoint-report.ts +109 -0
- package/src/results/checkpoint-retention.ts +15 -0
- package/src/results/completion-group-delivery.ts +66 -0
- package/src/results/completion-group-recovery.ts +61 -0
- package/src/results/completion-group-wake.ts +62 -0
- package/src/results/completion-group.ts +290 -0
- package/src/results/continuation-recipient.ts +37 -0
- package/src/results/durable-json.ts +29 -0
- package/src/results/repair-policy.ts +24 -0
- package/src/results/report-result.ts +77 -0
- package/src/results/source-identity.ts +115 -0
- package/src/results/specialist-checkpoint.ts +227 -0
- package/src/results/verification-controller.ts +308 -0
- package/src/utils/messaging.ts +47 -10
- package/src/utils/models.ts +13 -0
- package/src/utils/paths.ts +4 -0
- package/src/utils/report-events.ts +95 -10
- package/src/utils/teams.ts +3 -1
- package/src/utils/write-queue.ts +3 -0
|
@@ -97,6 +97,9 @@ export function createWriteAgentRuntime(options: WriteAgentRuntimeOptions) {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
async function startWriteAgent(teamName: string, member: Member, prompt: string): Promise<string> {
|
|
100
|
+
if (["checkpoint", "checkpointAssignment", "continueFrom", "continue_from"].some(field => Reflect.get(member, field) !== undefined)) {
|
|
101
|
+
throw new Error("Checkpoint-enabled agents require in-process lead admission; legacy terminal startup is unsupported.");
|
|
102
|
+
}
|
|
100
103
|
assertWriterUsesConfiguredLevel(member);
|
|
101
104
|
if (!options.terminal) {
|
|
102
105
|
throw new Error("pi-extended-teams requires running inside tmux for write agents.");
|
|
@@ -9,10 +9,12 @@ import { cleanupAgentSessionFolders, cleanupOrphanedTeams } from "../internal/se
|
|
|
9
9
|
import { summarizeSessionUsage } from "../internal/session-usage";
|
|
10
10
|
import { formatElapsed, formatTokenCount } from "../ui/renderers";
|
|
11
11
|
import { isWorkflowSpawnedMember } from "../../src/utils/workflow-metadata";
|
|
12
|
-
import {
|
|
12
|
+
import { globalSettingsPath, projectSettingsPath } from "../../src/utils/settings";
|
|
13
|
+
import { normalizeRepairPolicy } from "../../src/results/repair-policy";
|
|
13
14
|
import { generateLifecycleRunId } from "../../src/utils/lifecycle-tombstone";
|
|
14
15
|
import { cleanupStaleSessionContextReferences } from "../internal/session-context-reference";
|
|
15
16
|
import { cleanupStalePrivateAgentSessions } from "../internal/agent-session-files";
|
|
17
|
+
import { expireCheckpoints } from "../../src/results/checkpoint-retention";
|
|
16
18
|
|
|
17
19
|
export const LEAD_ORCHESTRATION_GUIDANCE = `\n\npi-extended-teams lead orchestration rules:\n- Choose tiers by the agent's intended outcome, not by vague task importance. read-review is the normal default for focused review, verification, and bounded synthesis.\n- Use read-collect when the lane gathers bounded facts without owning the conclusion. Use read-analyze when it must explain behavior or root cause across connected evidence. Reserve read-critical for irreducible high-stakes security, architecture, concurrency, migration, or data-correctness reasoning.\n- For edits, use write-patch for a narrow localized change, write-feature for a bounded feature with a known design, write-system for a cross-cutting integration/refactor within explicitly claimed files, and write-critical only for high-risk security, concurrency, recovery, migration, or data-integrity changes.\n- Prefer the canonical read-*/write-* tiers. Legacy reading-*/writing-* names are compatibility aliases for this minor release, not intent guidance.\n- A spawned agent owns its assigned lane until it reports, blocks, fails, or the user cancels it. Do not duplicate, take over, test, edit, or synthesize that same lane in parallel; work only on clearly unrelated lanes.\n- When no unrelated work remains, end the turn. The extension resumes you when a report arrives. One get_agent_status snapshot is allowed when current status is needed; do not repeatedly call it, sleep, busy-wait, loop on inbox/status, send nudges, do dummy work, or treat healthy silence as failure.\n- Wait for the actual report before synthesizing. Intervene only on a reported blocker/error, actual health failure, or explicit user cancellation/change.\n- For durable bug, security, or testing claims from an agent report or backlog, concrete, reproducible findings with file/line evidence or a focused failing regression may proceed directly to TDD repair.
|
|
18
20
|
- Use a separate read-only confirmation only when evidence is missing or weak, the claim is disputed, or irreducible high-risk uncertainty remains; never reconfirm an already confirmed finding.`;
|
|
@@ -42,6 +44,13 @@ export function isInboxFileWatchEvent(inboxFile: string, filename: string | Buff
|
|
|
42
44
|
return !changedName || changedName === inboxBase || changedName === `${inboxBase}.lock`;
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
function hasPersistedTeamSettings(ctx: any): boolean {
|
|
48
|
+
if (fs.existsSync(globalSettingsPath())) return true;
|
|
49
|
+
return typeof ctx.cwd === "string"
|
|
50
|
+
&& ctx.isProjectTrusted?.() === true
|
|
51
|
+
&& fs.existsSync(projectSettingsPath(ctx.cwd));
|
|
52
|
+
}
|
|
53
|
+
|
|
45
54
|
export function registerExtensionEvents(pi: any, options: RegisterEventsOptions): void {
|
|
46
55
|
let teammateWakeIfUnread: (() => Promise<void>) | null = null;
|
|
47
56
|
let teammatePendingInboxWake = false;
|
|
@@ -163,14 +172,14 @@ export function registerExtensionEvents(pi: any, options: RegisterEventsOptions)
|
|
|
163
172
|
} catch {
|
|
164
173
|
// Session-start janitors are best-effort and must not block initialization.
|
|
165
174
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
if (
|
|
175
|
+
try {
|
|
176
|
+
for (const diagnostic of await expireCheckpoints()) ctx.ui?.notify?.(diagnostic, "warning");
|
|
177
|
+
} catch (error) {
|
|
178
|
+
ctx.ui?.notify?.(`Checkpoint expiry: ${String(error)}`, "warning");
|
|
179
|
+
}
|
|
180
|
+
if (!hasPersistedTeamSettings(ctx)) {
|
|
172
181
|
ctx.ui?.notify?.(
|
|
173
|
-
"
|
|
182
|
+
"pi-extended-teams is not configured yet. Run /pi-extended-teams-onboard for agent-led model and shared-extension setup.",
|
|
174
183
|
"warning"
|
|
175
184
|
);
|
|
176
185
|
}
|
|
@@ -399,13 +408,16 @@ export function registerExtensionEvents(pi: any, options: RegisterEventsOptions)
|
|
|
399
408
|
if (member.thinking) modelInfo += ` with thinking level: ${member.thinking}`;
|
|
400
409
|
modelInfo += `. When reporting your model or thinking level, use these exact values.`;
|
|
401
410
|
}
|
|
411
|
+
const repairGuidance = normalizeRepairPolicy(member?.repairPolicy)
|
|
412
|
+
? "Call report_and_exit when finished. If it returns an unaccepted repair request, remain active, repair only the assigned scope, and resubmit. Report blocked or failed if repair is unsafe. Claims and shutdown wait for an accepted final report."
|
|
413
|
+
: undefined;
|
|
402
414
|
if ((member?.role ?? "write") === "write") {
|
|
403
415
|
const workflowGuard = member && isWorkflowSpawnedMember(member)
|
|
404
416
|
? "\n- Workflow mode: do not create helper fanout yourself. Ask team-lead with send_message for an explicit workflow assignment."
|
|
405
417
|
: "\n- If you need read-only help, ask team-lead with send_message. The lead decides whether to spawn another agent.";
|
|
406
|
-
roleSpecificGuidance = `\n\nEdit-agent rules:\n- Before editing or writing any repository file, call claim_file with every path you intend to change and wait for the claim to be granted.\n- If claim_file reports conflicts, do not edit those files; coordinate with your lead instead.${workflowGuard}\n- Release claims with release_file as soon as you are done editing those paths.\n- When your work is finished, call report_and_exit. It sends your final report, releases any remaining file claims, and shuts you down. Do not wait for the lead to kill you
|
|
418
|
+
roleSpecificGuidance = `\n\nEdit-agent rules:\n- Before editing or writing any repository file, call claim_file with every path you intend to change and wait for the claim to be granted.\n- If claim_file reports conflicts, do not edit those files; coordinate with your lead instead.${workflowGuard}\n- Release claims with release_file as soon as you are done editing those paths.\n- ${repairGuidance ?? "When your work is finished, call report_and_exit. It sends your final report, releases any remaining file claims, and shuts you down. Do not wait for the lead to kill you."}`;
|
|
407
419
|
} else {
|
|
408
|
-
roleSpecificGuidance = `\n\nRead-agent rules:\n- You are read-only: investigate and report. Do not edit files or make any mutating changes.\n- When finished, produce your final report and stop. Do not wait for the lead to kill you
|
|
420
|
+
roleSpecificGuidance = `\n\nRead-agent rules:\n- You are read-only: investigate and report. Do not edit files or make any mutating changes.\n- ${repairGuidance ?? "When finished, produce your final report and stop. Do not wait for the lead to kill you."}`;
|
|
409
421
|
}
|
|
410
422
|
rosterInfo = `\n\n${options.formatRosterForPrompt(await options.buildRoster(teamName))}\nUse this roster as a snapshot. If you need updated roster or liveness details, ask team-lead with send_message. Do not poll.`;
|
|
411
423
|
} catch {
|
package/extensions/index.ts
CHANGED
|
@@ -8,11 +8,14 @@ import { runReadAgentInProcess, sendMessageToRunningReadAgent } from "./agents/r
|
|
|
8
8
|
import { createWriteAgentRuntime } from "./agents/write-agent.js";
|
|
9
9
|
import { registerExtensionEvents } from "./events/register-events.js";
|
|
10
10
|
import { createLifecycleRuntime } from "./team/lifecycle.js";
|
|
11
|
+
import { createCombinedSessionCost } from "./team/session-cost.js";
|
|
11
12
|
import { buildRoster as buildTeamRoster, formatRosterForPrompt, releaseAllClaimsForAgent, requireTeamContext as resolveTeamContext, requireWriteAgentTeam as resolveWriteAgentTeam } from "./team/roster.js";
|
|
12
13
|
import { createWriterScreenState, registerWriterScreenShortcut, removeWriterScreenTab, upsertWriterScreenTab, type ActiveWriterTab } from "./team/writer-screens.js";
|
|
13
14
|
import { registerFavoriteModelsCommand } from "./ui/favorite-models-command.js";
|
|
14
15
|
import { registerExtensionsCommand } from "./ui/extensions-command.js";
|
|
15
|
-
import {
|
|
16
|
+
import { registerOnboardingCommand } from "./ui/onboarding-command.js";
|
|
17
|
+
import { registerCheckpointsCommand } from "./ui/checkpoints-command.js";
|
|
18
|
+
import { hasActiveReadAgentLifecycle, installAgentNavigation, orderAgentNavigationEntries } from "./ui/agent-navigation.js";
|
|
16
19
|
import { buildReadHelperPrompt, registerCoordinationTools } from "./tools/coordination-tools.js";
|
|
17
20
|
import { createReportProgressTool } from "./tools/agent-communication-tools.js";
|
|
18
21
|
import { registerTaskRuntimeTools } from "./tools/task-runtime-tools.js";
|
|
@@ -26,11 +29,14 @@ import { createActiveAgentSleepController, runWithActiveAgentSleepAssertion } fr
|
|
|
26
29
|
import { createTeammateInterrupter } from "./runtime/teammate-interrupt.js";
|
|
27
30
|
export { panelBgFill, framePanel, frameWidget, frameWidgetFullWidth, logWindowStart } from "./ui/frame.js";
|
|
28
31
|
import * as messaging from "../src/utils/messaging";
|
|
32
|
+
import { requestCompletionGroupWake, observeCompletionGroupWakes } from "../src/results/completion-group-wake";
|
|
33
|
+
import { recoverCompletionGroups } from "../src/results/completion-group-recovery";
|
|
29
34
|
import * as teams from "../src/utils/teams";
|
|
30
35
|
import * as runtime from "../src/utils/runtime";
|
|
31
36
|
import * as teamPaths from "../src/utils/paths";
|
|
32
37
|
import { listReadHelperQueue, removeQueuedReadHelperRequest } from "../src/utils/read-helper-queue";
|
|
33
38
|
import type { Member } from "../src/utils/models";
|
|
39
|
+
import { projectAgentStatus, projectLifecycleFence } from "../src/orchestration/status-projection";
|
|
34
40
|
import { getTerminalAdapter } from "../src/adapters/terminal-registry";
|
|
35
41
|
import { createSpawnResourcePlan, parentProjectTrustForSpawn } from "./resources/spawn-resource-plan.js";
|
|
36
42
|
import { generateExtensionInstanceId, listLifecycleTombstones, onLifecycleTombstoneCleared, readLifecycleTombstone } from "../src/utils/lifecycle-tombstone";
|
|
@@ -40,6 +46,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
40
46
|
const agentName = process.env.PI_AGENT_NAME || "team-lead";
|
|
41
47
|
const extensionInstanceId = generateExtensionInstanceId();
|
|
42
48
|
const envTeamName = process.env.PI_TEAM_NAME;
|
|
49
|
+
const combinedSessionCost = isTeammate ? undefined : createCombinedSessionCost(pi);
|
|
43
50
|
|
|
44
51
|
// Teammates are explicitly bound by PI_TEAM_NAME. Lead sessions are adopted
|
|
45
52
|
// later at session_start only when the persisted Pi session id matches.
|
|
@@ -172,11 +179,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
172
179
|
targetTeamName: string,
|
|
173
180
|
targetAgentName: string,
|
|
174
181
|
content: string,
|
|
175
|
-
expectedRecipientRunId?: string
|
|
182
|
+
expectedRecipientRunId?: string,
|
|
183
|
+
requireReceipt = false
|
|
176
184
|
): Promise<boolean> {
|
|
177
185
|
const target = runningReadAgents.get(readAgentKey(targetTeamName, targetAgentName));
|
|
178
186
|
if (expectedRecipientRunId !== undefined && target?.runId !== expectedRecipientRunId) return false;
|
|
179
|
-
const delivered = await sendMessageToRunningReadAgent(target, content);
|
|
187
|
+
const delivered = await sendMessageToRunningReadAgent(target, content, requireReceipt);
|
|
180
188
|
if (delivered) renderReadAgentStatus();
|
|
181
189
|
return delivered;
|
|
182
190
|
}
|
|
@@ -229,13 +237,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
229
237
|
return !!status.lastHeartbeatAt && (now - status.lastHeartbeatAt) <= runtime.HEARTBEAT_STALE_MS;
|
|
230
238
|
}
|
|
231
239
|
|
|
232
|
-
function hasActiveReadAgentLifecycle(agent: RunningReadAgent): boolean {
|
|
233
|
-
return agent.teardownState !== "stopping"
|
|
234
|
-
&& agent.teardownState !== "quarantined"
|
|
235
|
-
&& agent.teardownState !== "persistence_failed"
|
|
236
|
-
&& agent.teardownState !== "finalized";
|
|
237
|
-
}
|
|
238
|
-
|
|
239
240
|
function isVisibleRuntimeOnlyMember(
|
|
240
241
|
member: Member,
|
|
241
242
|
runtimeStatus: runtime.AgentRuntimeStatus | null,
|
|
@@ -337,11 +338,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
337
338
|
// message with display:false still reaches the model as a user turn (see
|
|
338
339
|
// convertToLlm) but is never rendered, so team coordination stays silent.
|
|
339
340
|
// Falls back to a visible user message on older pi builds without sendMessage.
|
|
340
|
-
function quietTrigger(content: string): void {
|
|
341
|
+
function quietTrigger(content: string, details?: Record<string, unknown>): void {
|
|
341
342
|
const api = pi as any;
|
|
342
343
|
if (typeof api.sendMessage === "function") {
|
|
343
344
|
api.sendMessage(
|
|
344
|
-
{ customType: "pi-extended-teams-wake", content, display: false },
|
|
345
|
+
{ customType: "pi-extended-teams-wake", content, display: false, ...(details ? { details } : {}) },
|
|
345
346
|
{ triggerTurn: true, deliverAs: "followUp" }
|
|
346
347
|
);
|
|
347
348
|
} else {
|
|
@@ -378,9 +379,46 @@ export default function (pi: ExtensionAPI) {
|
|
|
378
379
|
pi.events?.emit?.("pi-extended-teams:agent-progress", { teamName: progressTeamName, name, status, updatedAt });
|
|
379
380
|
}
|
|
380
381
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
382
|
+
const currentCompletionGroups = new Set<string>();
|
|
383
|
+
const observedWakeEntries = new Set<string>();
|
|
384
|
+
const groupWarnings = new Set<string>();
|
|
385
|
+
const sessionEntries = (ctx: any) => ctx?.sessionManager?.getEntries?.() ?? ctx?.sessionManager?.getBranch?.() ?? [];
|
|
386
|
+
function warnGroup(error: unknown): void {
|
|
387
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
388
|
+
if (!groupWarnings.has(message)) {
|
|
389
|
+
groupWarnings.add(message);
|
|
390
|
+
sessionCtx?.ui?.notify?.(message, "warning");
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
async function observeGroupHistory(ctx: any): Promise<void> {
|
|
394
|
+
if (!teamName || extensionShuttingDown) return;
|
|
395
|
+
const target = teamName;
|
|
396
|
+
const entries = sessionEntries(ctx).filter((entry: any) => !observedWakeEntries.has(`${target}:${entry.id}`));
|
|
397
|
+
const result = await observeCompletionGroupWakes(target, getPiSessionId(ctx) ?? "", entries);
|
|
398
|
+
for (const id of result.observed) observedWakeEntries.add(`${target}:${id}`);
|
|
399
|
+
for (const error of result.errors) warnGroup(error);
|
|
400
|
+
}
|
|
401
|
+
async function requestGroupInboxWakes(unread: any[]): Promise<void> {
|
|
402
|
+
if (!teamName || !sessionCtx || extensionShuttingDown) return;
|
|
403
|
+
const target = teamName;
|
|
404
|
+
const ctx = sessionCtx;
|
|
405
|
+
for (const message of unread.filter(item => item.metadata?.completionGroup)) {
|
|
406
|
+
try {
|
|
407
|
+
if (typeof (pi as any).sendMessage !== "function") throw new Error("Grouped wakes require Pi custom messages; use read_inbox for the saved index.");
|
|
408
|
+
await requestCompletionGroupWake(target, getPiSessionId(ctx) ?? "", message, (content, details) => {
|
|
409
|
+
if (extensionShuttingDown || ctx !== sessionCtx) throw new Error("Group wake unconfirmed: the lead session is closing.");
|
|
410
|
+
quietTrigger(content, details);
|
|
411
|
+
});
|
|
412
|
+
} catch (error) { warnGroup(error); }
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
async function wakeLeadForInboxReports(unread: any[]): Promise<void> {
|
|
416
|
+
if (!teamName || extensionShuttingDown) return;
|
|
417
|
+
const target = teamName;
|
|
418
|
+
const ctx = sessionCtx;
|
|
419
|
+
await requestGroupInboxWakes(unread);
|
|
420
|
+
if (extensionShuttingDown || target !== teamName || ctx !== sessionCtx) return;
|
|
421
|
+
const keyedUnread = unread.filter(message => !message.metadata?.completionGroup).map((message) => ({
|
|
384
422
|
message,
|
|
385
423
|
key: String(message?.id || `${message?.from || "unknown"}:${message?.timestamp || ""}:${message?.text || ""}`),
|
|
386
424
|
}));
|
|
@@ -412,7 +450,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
412
450
|
if (isTeammate) return;
|
|
413
451
|
const unread = await messaging.peekInbox(targetTeamName, agentName, true);
|
|
414
452
|
await renderLeadInboxStatus();
|
|
415
|
-
wakeLeadForInboxReports(unread);
|
|
453
|
+
await wakeLeadForInboxReports(unread);
|
|
416
454
|
}
|
|
417
455
|
|
|
418
456
|
function getIdleReadAgentNudgeMessage(agent: RunningReadAgent, status: ReadAgentStatusDescription): string | null {
|
|
@@ -567,11 +605,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
567
605
|
|
|
568
606
|
const now = Date.now();
|
|
569
607
|
const activityTeamName = teamName || null;
|
|
570
|
-
const
|
|
608
|
+
const sessionAgents = activityTeamName
|
|
571
609
|
? Array.from(runningReadAgents.values())
|
|
572
|
-
.filter(agent => agent.teamName === activityTeamName
|
|
610
|
+
.filter(agent => agent.teamName === activityTeamName)
|
|
573
611
|
.sort((a, b) => a.name.localeCompare(b.name))
|
|
574
612
|
: [];
|
|
613
|
+
const runningAgents = sessionAgents.filter(hasActiveReadAgentLifecycle);
|
|
575
614
|
const readAgents = runningAgents.filter(agent => (agent.role || "read") === "read");
|
|
576
615
|
const writeAgents = runningAgents.filter(agent => (agent.role || "read") === "write");
|
|
577
616
|
const activityConfig = activityTeamName ? await teams.readConfig(activityTeamName).catch(() => null) : null;
|
|
@@ -579,17 +618,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
579
618
|
const activityTombstones = activityTeamName
|
|
580
619
|
? await listLifecycleTombstones(activityTeamName).catch(() => [])
|
|
581
620
|
: [];
|
|
582
|
-
const
|
|
583
|
-
|
|
621
|
+
const unsettledAgents = sessionAgents.filter(agent => !hasActiveReadAgentLifecycle(agent)
|
|
622
|
+
&& agent.teardownState !== "finalized"
|
|
623
|
+
&& !activityTombstones.some(fence => fence.agentName === agent.name));
|
|
624
|
+
const runtimeOnlyCandidates = activityTeamName
|
|
625
|
+
? await Promise.all(activityMembers
|
|
584
626
|
.filter(member => member.name !== "team-lead")
|
|
585
627
|
.filter(member => !runningReadAgents.has(readAgentKey(activityTeamName, member.name)))
|
|
586
628
|
.map(async (member) => ({
|
|
587
629
|
member,
|
|
588
630
|
runtimeStatus: await runtime.readRuntimeStatus(activityTeamName, member.name).catch(() => null),
|
|
589
|
-
})))
|
|
590
|
-
.filter((entry): entry is { member: Member; runtimeStatus: runtime.AgentRuntimeStatus } => isVisibleRuntimeOnlyMember(entry.member, entry.runtimeStatus, now))
|
|
591
|
-
.filter(({ member, runtimeStatus }) => member.lifecycleRunId === runtimeStatus.lifecycleRunId)
|
|
631
|
+
})))
|
|
592
632
|
: [];
|
|
633
|
+
const runtimeOnlyMembers = runtimeOnlyCandidates
|
|
634
|
+
.filter((entry): entry is { member: Member; runtimeStatus: runtime.AgentRuntimeStatus } => isVisibleRuntimeOnlyMember(entry.member, entry.runtimeStatus, now))
|
|
635
|
+
.filter(({ member, runtimeStatus }) => member.lifecycleRunId === runtimeStatus.lifecycleRunId);
|
|
593
636
|
const runtimeOnlyMemberNames = new Set(runtimeOnlyMembers.map(({ member }) => member.name));
|
|
594
637
|
const runtimeOnlyReadMembers = runtimeOnlyMembers.filter(({ member }) => memberActivityRole(member) === "read");
|
|
595
638
|
const runtimeOnlyWriteMembers = runtimeOnlyMembers.filter(({ member }) => memberActivityRole(member) === "write");
|
|
@@ -618,6 +661,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
618
661
|
.filter(member => member.name !== "team-lead" && member.isActive !== false && memberActivityRole(member) === "write")
|
|
619
662
|
.filter(member => !!(member.tmuxPaneId && terminal?.isAlive?.(member.tmuxPaneId)) && !runningReadAgents.has(readAgentKey(activityTeamName, member.name)) && !runtimeOnlyMemberNames.has(member.name)) ?? []
|
|
620
663
|
: [];
|
|
664
|
+
const unobservedMembers = runtimeOnlyCandidates.filter(({ member }) => member.isActive !== false
|
|
665
|
+
&& !runtimeOnlyMemberNames.has(member.name)
|
|
666
|
+
&& !activeWriteMembers.some(writer => writer.name === member.name)
|
|
667
|
+
&& !activityTombstones.some(fence => fence.agentName === member.name));
|
|
668
|
+
const activeCount = runningAgents.length + runtimeOnlyMembers.length + activeWriteMembers.length + unobservedMembers.length;
|
|
621
669
|
navigationRuntimeAgents.clear();
|
|
622
670
|
for (const { member, runtimeStatus } of runtimeOnlyReadMembers) {
|
|
623
671
|
if (member.lifecycleRunId === runtimeStatus.lifecycleRunId) {
|
|
@@ -633,11 +681,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
633
681
|
leadInboxUnreadCount = unreadLeadMessages.length;
|
|
634
682
|
clearLeadInboxWidgetOnce();
|
|
635
683
|
|
|
636
|
-
|
|
684
|
+
const hasVisibleAgents = activeCount > 0 || unsettledAgents.length > 0 || activityTombstones.length > 0;
|
|
685
|
+
if (hasVisibleAgents && !readAgentStatusTimer) {
|
|
637
686
|
readAgentStatusTimer = setInterval(renderReadAgentStatus, 1000);
|
|
638
687
|
}
|
|
639
688
|
|
|
640
|
-
if (
|
|
689
|
+
if (!hasVisibleAgents) {
|
|
641
690
|
sessionCtx.ui.setStatus?.("01-pi-extended-teams-read", undefined);
|
|
642
691
|
clearTeamActivityWidget();
|
|
643
692
|
sessionCtx.ui.setWidget?.("01-pi-extended-teams-status", undefined);
|
|
@@ -648,7 +697,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
648
697
|
return;
|
|
649
698
|
}
|
|
650
699
|
|
|
651
|
-
const activeCount = readAgents.length + writeAgents.length + runtimeOnlyReadMembers.length + runtimeOnlyWriteMembers.length + activeWriteMembers.length;
|
|
652
700
|
const entries: TeamActivityStatusEntry[] = [];
|
|
653
701
|
const footerStatuses: string[] = [];
|
|
654
702
|
const statusCounts: Record<string, number> = {};
|
|
@@ -742,16 +790,43 @@ export default function (pi: ExtensionAPI) {
|
|
|
742
790
|
footerStatuses.push(formatAgentProgressStatus(agent.name, agent.model, agent.thinking, agent.modelSlot, elapsed, contextUsage, agent.latestProgress, now));
|
|
743
791
|
}
|
|
744
792
|
|
|
793
|
+
for (const { member, runtimeStatus } of unobservedMembers) {
|
|
794
|
+
const projected = projectAgentStatus({ member, runtime: runtimeStatus, fence: { status: "absent" }, terminalAlive: null, now });
|
|
795
|
+
const telemetry = !projected.runtime ? "runtime telemetry unavailable"
|
|
796
|
+
: runtimeHeartbeatIsRecent(projected.runtime, now) ? "awaiting runtime readiness" : "heartbeat stale";
|
|
797
|
+
countStatus(projected.phase);
|
|
798
|
+
entries.push({ name: member.name, role: memberActivityRole(member), status: projected.phase, detail: telemetry });
|
|
799
|
+
footerStatuses.push(`(${member.name}) ${projected.phase} · ${telemetry}`);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
for (const agent of unsettledAgents) {
|
|
803
|
+
const status = agent.teardownState === "persistence_failed" ? "persistence-failed"
|
|
804
|
+
: agent.teardownState === "quarantined" ? "quarantined" : "stopping";
|
|
805
|
+
const reason = agent.teardownError instanceof Error ? agent.teardownError.message
|
|
806
|
+
: agent.teardownResult?.error || "cleanup has not settled";
|
|
807
|
+
const detail = status === "stopping" ? "Finishing cleanup" : `Cleanup blocked: ${reason} · use check_teammate`;
|
|
808
|
+
countStatus(status);
|
|
809
|
+
entries.push({ name: agent.name, role: agent.role === "write" ? "write" : "read", status, detail });
|
|
810
|
+
}
|
|
811
|
+
|
|
745
812
|
for (const { agentName: quarantinedName, result } of activityTombstones) {
|
|
813
|
+
if (result.status === "occupied" && result.tombstone.phase === "persistence_closed"
|
|
814
|
+
&& runningAgents.some(agent => agent.name === quarantinedName && agent.runId === result.tombstone.runId)) {
|
|
815
|
+
const entryIndex = entries.findIndex(entry => entry.name === quarantinedName);
|
|
816
|
+
footerStatuses[entryIndex] += " · Finishing cleanup";
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
746
819
|
const persistedMember = activityMembers.find(member => member.name === quarantinedName);
|
|
747
820
|
const role = result.status === "occupied"
|
|
748
821
|
? result.tombstone.role
|
|
749
822
|
: persistedMember ? memberActivityRole(persistedMember) : "read";
|
|
750
|
-
const
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
823
|
+
const projected = projectLifecycleFence(persistedMember ?? {}, result);
|
|
824
|
+
const reason = projected.error || (result.status === "occupied" && result.tombstone.phase === "timed_out" ? "cleanup timed out" : "cleanup failed");
|
|
825
|
+
const detail = projected.phase === "stopping"
|
|
826
|
+
? "Finishing cleanup"
|
|
827
|
+
: `Cleanup blocked: ${reason} · use check_teammate`;
|
|
828
|
+
countStatus(projected.phase);
|
|
829
|
+
entries.push({ name: quarantinedName, role, status: projected.phase, detail });
|
|
755
830
|
}
|
|
756
831
|
|
|
757
832
|
wakeLeadForIdleReadAgents(idleNudgeMessages);
|
|
@@ -762,8 +837,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
762
837
|
sessionCtx.ui.setWidget?.("01-pi-extended-teams-status", undefined);
|
|
763
838
|
updateTeamActivityWidget({
|
|
764
839
|
activeCount,
|
|
765
|
-
readCount: readAgents.length + runtimeOnlyReadMembers.length,
|
|
766
|
-
writeCount: writeAgents.length + runtimeOnlyWriteMembers.length + activeWriteMembers.length,
|
|
840
|
+
readCount: readAgents.length + runtimeOnlyReadMembers.length + unobservedMembers.filter(({ member }) => memberActivityRole(member) === "read").length,
|
|
841
|
+
writeCount: writeAgents.length + runtimeOnlyWriteMembers.length + activeWriteMembers.length + unobservedMembers.filter(({ member }) => memberActivityRole(member) === "write").length,
|
|
767
842
|
unreadCount: leadInboxUnreadCount,
|
|
768
843
|
entries,
|
|
769
844
|
statusCounts,
|
|
@@ -951,7 +1026,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
951
1026
|
try {
|
|
952
1027
|
const unread = await messaging.readInbox(teamName, agentName, true, false);
|
|
953
1028
|
await renderLeadInboxStatus();
|
|
954
|
-
wakeLeadForInboxReports(unread);
|
|
1029
|
+
await wakeLeadForInboxReports(unread);
|
|
955
1030
|
} catch {
|
|
956
1031
|
// Ignore errors for lead polling
|
|
957
1032
|
}
|
|
@@ -965,10 +1040,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
965
1040
|
function adoptTeamAsLead(name: string, ctx?: any): void {
|
|
966
1041
|
if (isTeammate || !name) return;
|
|
967
1042
|
const sessionId = getPiSessionId(ctx ?? sessionCtx);
|
|
1043
|
+
registerLeadSession(name, sessionId);
|
|
1044
|
+
// Private jobs retain cleanup ownership, not the public roster/inbox/UI scope.
|
|
1045
|
+
if (name.startsWith("prompt-build-")) return;
|
|
968
1046
|
if (teamName !== name) {
|
|
969
1047
|
teamName = name;
|
|
970
1048
|
}
|
|
971
|
-
registerLeadSession(name, sessionId);
|
|
972
1049
|
ensureReadAgentStatusTicker();
|
|
973
1050
|
startLeadInboxPolling();
|
|
974
1051
|
startReadHelperQueueDraining();
|
|
@@ -1091,6 +1168,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1091
1168
|
try {
|
|
1092
1169
|
await stopRunningAgentsForShutdown(reason);
|
|
1093
1170
|
} finally {
|
|
1171
|
+
combinedSessionCost?.deactivate();
|
|
1094
1172
|
activeAgentSleepController.dispose();
|
|
1095
1173
|
}
|
|
1096
1174
|
|
|
@@ -1121,14 +1199,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
1121
1199
|
});
|
|
1122
1200
|
|
|
1123
1201
|
if (!isTeammate) {
|
|
1202
|
+
pi.on("context", async (_event: any, ctx: any) => { await observeGroupHistory(ctx); });
|
|
1124
1203
|
pi.on("session_start", async (_event: any, ctx: any) => {
|
|
1204
|
+
await observeGroupHistory(ctx);
|
|
1205
|
+
if (teamName) {
|
|
1206
|
+
try {
|
|
1207
|
+
const config = await teams.readConfig(teamName);
|
|
1208
|
+
for (const error of await recoverCompletionGroups(teamName, getPiSessionId(ctx) ?? "", config.members, sessionEntries(ctx), currentCompletionGroups)) warnGroup(error);
|
|
1209
|
+
await requestGroupInboxWakes(await messaging.peekInbox(teamName, agentName, true));
|
|
1210
|
+
} catch (error) { warnGroup(error); }
|
|
1211
|
+
}
|
|
1125
1212
|
installAgentNavigation(ctx, {
|
|
1126
1213
|
getAgents: () => {
|
|
1127
1214
|
const readers = Array.from(runningReadAgents.values())
|
|
1128
1215
|
.filter(agent => !teamName || agent.teamName === teamName);
|
|
1129
|
-
|
|
1130
|
-
.filter(agent => !teamName || agent.teamName === teamName)
|
|
1131
|
-
|
|
1216
|
+
const runtimeAgents = Array.from(navigationRuntimeAgents.values())
|
|
1217
|
+
.filter(agent => !teamName || agent.teamName === teamName);
|
|
1218
|
+
return orderAgentNavigationEntries(readers, runtimeAgents);
|
|
1132
1219
|
},
|
|
1133
1220
|
interruptAgent: async (name: string) => {
|
|
1134
1221
|
const result = await interruptTeammate(name);
|
|
@@ -1188,6 +1275,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1188
1275
|
|
|
1189
1276
|
registerFavoriteModelsCommand(pi);
|
|
1190
1277
|
registerExtensionsCommand(pi);
|
|
1278
|
+
if (!isTeammate) registerOnboardingCommand(pi);
|
|
1279
|
+
if (!isTeammate) registerCheckpointsCommand(pi);
|
|
1191
1280
|
|
|
1192
1281
|
registerWriterScreenShortcut(pi, {
|
|
1193
1282
|
getTeamName: () => teamName,
|
|
@@ -1218,7 +1307,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
1218
1307
|
createNestedReadAgentTools: (binding: Parameters<TeamToolsRuntime["createNestedReadAgentTools"]>[0]) => {
|
|
1219
1308
|
return teamToolsRuntime?.createNestedReadAgentTools(binding) ?? [];
|
|
1220
1309
|
},
|
|
1310
|
+
nestedChildSnapshot: (binding: Parameters<TeamToolsRuntime["nestedChildSnapshot"]>[0]) => {
|
|
1311
|
+
return teamToolsRuntime?.nestedChildSnapshot(binding) ?? { running: 0, queued: 0 };
|
|
1312
|
+
},
|
|
1221
1313
|
pendingChildController,
|
|
1314
|
+
beginCostRun: combinedSessionCost?.begin,
|
|
1222
1315
|
};
|
|
1223
1316
|
}
|
|
1224
1317
|
|
|
@@ -1233,6 +1326,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1233
1326
|
startWriteAgent,
|
|
1234
1327
|
shutdownTeammate,
|
|
1235
1328
|
adoptTeamAsLead,
|
|
1329
|
+
onCompletionGroupUse: groupId => { currentCompletionGroups.add(groupId); },
|
|
1236
1330
|
buildRoster,
|
|
1237
1331
|
isTeammate,
|
|
1238
1332
|
agentName,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CheckOperations } from "../../src/results/check-runner";
|
|
2
|
+
import { loadPiRuntimeApi } from "./pi-runtime-api";
|
|
3
|
+
|
|
4
|
+
export async function loadNativeCheckOperations(suppliedApi?: unknown): Promise<CheckOperations | undefined> {
|
|
5
|
+
const api: unknown = suppliedApi === undefined ? await loadPiRuntimeApi() : suppliedApi;
|
|
6
|
+
if (typeof api !== "object" || api === null) return undefined;
|
|
7
|
+
const factory: unknown = Reflect.get(api, "createLocalBashOperations");
|
|
8
|
+
if (typeof factory !== "function") return undefined;
|
|
9
|
+
const operations: unknown = factory();
|
|
10
|
+
if (typeof operations !== "object" || operations === null) return undefined;
|
|
11
|
+
const exec: unknown = Reflect.get(operations, "exec");
|
|
12
|
+
if (typeof exec !== "function") return undefined;
|
|
13
|
+
return {
|
|
14
|
+
async exec(command, cwd, options) {
|
|
15
|
+
const result: unknown = await Reflect.apply(exec, operations, [command, cwd, options]);
|
|
16
|
+
if (typeof result !== "object" || result === null) throw new Error("Native check returned no exit evidence.");
|
|
17
|
+
const exitCode: unknown = Reflect.get(result, "exitCode");
|
|
18
|
+
if (exitCode !== null && (typeof exitCode !== "number" || !Number.isInteger(exitCode))) {
|
|
19
|
+
throw new Error("Native check returned invalid exit evidence.");
|
|
20
|
+
}
|
|
21
|
+
return { exitCode };
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -31,18 +31,22 @@ export function findLeadTeamForSession(sessionId?: string): string | null {
|
|
|
31
31
|
const teamsDir = path.dirname(paths.teamDir("__probe__"));
|
|
32
32
|
if (!fs.existsSync(teamsDir)) return null;
|
|
33
33
|
|
|
34
|
+
let latest: { name: string; selectedAt: number } | undefined;
|
|
34
35
|
for (const teamDir of fs.readdirSync(teamsDir)) {
|
|
36
|
+
if (teamDir.startsWith("prompt-build-")) continue;
|
|
35
37
|
try {
|
|
36
38
|
const sessionFile = paths.leadSessionPath(teamDir);
|
|
37
39
|
if (!fs.existsSync(sessionFile)) continue;
|
|
38
40
|
const session = JSON.parse(fs.readFileSync(sessionFile, "utf-8"));
|
|
39
41
|
if (session.sessionId === sessionId && session.pid === process.pid) {
|
|
40
|
-
|
|
42
|
+
const selectedAt = typeof session.startedAt === "number" && Number.isFinite(session.startedAt) ? session.startedAt : 0;
|
|
43
|
+
if (!latest || selectedAt > latest.selectedAt) latest = { name: teamDir, selectedAt };
|
|
41
44
|
}
|
|
42
45
|
} catch {
|
|
43
46
|
// ignore entries that are not valid team names, and invalid session files
|
|
44
47
|
}
|
|
45
48
|
}
|
|
49
|
+
return latest?.name ?? null;
|
|
46
50
|
} catch {
|
|
47
51
|
// ignore errors
|
|
48
52
|
}
|
|
@@ -50,13 +54,17 @@ export function findLeadTeamForSession(sessionId?: string): string | null {
|
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
export function registerLeadSession(teamName: string, sessionId?: string) {
|
|
57
|
+
const previousTeam = findLeadTeamForSession(sessionId);
|
|
58
|
+
const previous = previousTeam ? readJsonFile(paths.leadSessionPath(previousTeam)) : null;
|
|
59
|
+
// Ownership selections stay ordered even when two adoptions share a clock tick.
|
|
60
|
+
const selectedAt = Math.max(Date.now(), typeof previous?.startedAt === "number" && Number.isFinite(previous.startedAt) ? previous.startedAt + 1 : 0);
|
|
53
61
|
const sessionFile = paths.leadSessionPath(teamName);
|
|
54
62
|
const dir = path.dirname(sessionFile);
|
|
55
63
|
fs.mkdirSync(dir, { recursive: true });
|
|
56
64
|
fs.writeFileSync(sessionFile, JSON.stringify({
|
|
57
65
|
pid: process.pid,
|
|
58
66
|
sessionId,
|
|
59
|
-
startedAt:
|
|
67
|
+
startedAt: selectedAt,
|
|
60
68
|
}));
|
|
61
69
|
}
|
|
62
70
|
|
|
@@ -12,7 +12,7 @@ export interface TeammateInterruptResult {
|
|
|
12
12
|
message: string;
|
|
13
13
|
agentKind?: "read" | "write";
|
|
14
14
|
lifecycleRunId?: string;
|
|
15
|
-
mechanism?: "agent-session-abort" | "tmux-escape";
|
|
15
|
+
mechanism?: "agent-session-abort" | "assigned-check-abort" | "tmux-escape";
|
|
16
16
|
reason?: "no-active-session" | "session-not-created" | "lifecycle-closing"
|
|
17
17
|
| "missing-pane" | "missing-runtime-proof" | "terminal-interrupt-unavailable";
|
|
18
18
|
error?: string;
|
|
@@ -73,15 +73,18 @@ async function interruptReadAgent(
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
const generation = state.activeOperationGeneration;
|
|
76
|
+
const checkOperation = state.checkOperation;
|
|
76
77
|
const toolName = state.activeToolName?.trim();
|
|
77
|
-
if (!
|
|
78
|
+
if (!toolName || state.status !== "working" || (!checkOperation && (!generation || !state.session.isStreaming))) {
|
|
78
79
|
return result("no_command", state.name, `Agent ${state.name} has no running tool command to interrupt.`, fields);
|
|
79
80
|
}
|
|
80
81
|
if (state.operationInterruptPromise) {
|
|
81
82
|
return result("pending", state.name, `An interrupt is already pending for ${state.name}'s running ${toolName} command.`, fields);
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
const operationSettlement =
|
|
85
|
+
const operationSettlement = checkOperation
|
|
86
|
+
? Promise.all([checkOperation.settled, state.activeOperationSettlementPromise]).then(() => {})
|
|
87
|
+
: state.activeOperationSettlementPromise;
|
|
85
88
|
if (!operationSettlement) {
|
|
86
89
|
return result("no_command", state.name, `Agent ${state.name} has no running tool command to interrupt.`, fields);
|
|
87
90
|
}
|
|
@@ -89,7 +92,10 @@ async function interruptReadAgent(
|
|
|
89
92
|
state.interruptRequestedGeneration = generation;
|
|
90
93
|
let nativeAbort: Promise<void>;
|
|
91
94
|
try {
|
|
92
|
-
|
|
95
|
+
checkOperation?.controller.abort();
|
|
96
|
+
nativeAbort = checkOperation && !state.session.isStreaming
|
|
97
|
+
? Promise.resolve()
|
|
98
|
+
: Promise.resolve(state.session.abort());
|
|
93
99
|
} catch (error) {
|
|
94
100
|
if (state.interruptRequestedGeneration === generation) state.interruptRequestedGeneration = undefined;
|
|
95
101
|
const message = errorText(error);
|
|
@@ -130,7 +136,7 @@ async function interruptReadAgent(
|
|
|
130
136
|
});
|
|
131
137
|
}
|
|
132
138
|
return result("interrupted", state.name, `Interrupted ${state.name}'s running ${toolName} command. The agent is still active and can receive a follow-up message.`, {
|
|
133
|
-
...fields, mechanism: "agent-session-abort",
|
|
139
|
+
...fields, mechanism: checkOperation ? "assigned-check-abort" : "agent-session-abort",
|
|
134
140
|
});
|
|
135
141
|
}
|
|
136
142
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentSession } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import type { ReportResult } from "../../src/results/report-result";
|
|
2
3
|
import type { ContextUsageSnapshot, RuntimeError } from "../../src/utils/runtime";
|
|
3
4
|
import type { ManagedReadAgentLifecycleState } from "../agents/read-agent-session-lifecycle";
|
|
4
5
|
|
|
@@ -46,6 +47,8 @@ export interface RunningReadAgent extends ManagedReadAgentLifecycleState {
|
|
|
46
47
|
cleanupPrivateSessionOnFinalize?: boolean;
|
|
47
48
|
/** Fail finalization closed so runtime, member, fence, and transcript remain recoverable. */
|
|
48
49
|
finalizationBlockedReason?: string;
|
|
50
|
+
/** Optional accounting observer, called only after exact successful lifecycle settlement. */
|
|
51
|
+
onCostSettled?(): void;
|
|
49
52
|
session?: AgentSession;
|
|
50
53
|
moveToHerdr?(): Promise<void>;
|
|
51
54
|
finished?: Promise<void>;
|
|
@@ -63,6 +66,7 @@ export interface CompletedAgentReport {
|
|
|
63
66
|
name: string;
|
|
64
67
|
role: string;
|
|
65
68
|
status: "completed" | "failed";
|
|
69
|
+
result?: ReportResult;
|
|
66
70
|
report: string;
|
|
67
71
|
summary?: string;
|
|
68
72
|
completedAt: number;
|
|
@@ -4,6 +4,12 @@ import * as paths from "../../src/utils/paths";
|
|
|
4
4
|
import * as runtime from "../../src/utils/runtime";
|
|
5
5
|
import * as messaging from "../../src/utils/messaging";
|
|
6
6
|
import * as teams from "../../src/utils/teams";
|
|
7
|
+
import { readStoredTeamReportEvent } from "../../src/utils/report-events";
|
|
8
|
+
import { resyncReportCheckpoint } from "../../src/results/checkpoint-report";
|
|
9
|
+
import { createReportResult } from "../../src/results/report-result";
|
|
10
|
+
import { VerificationController } from "../../src/results/verification-controller";
|
|
11
|
+
import { CompletionGroup } from "../../src/results/completion-group";
|
|
12
|
+
import { enqueueCompletionGroupDeliveries } from "../../src/results/completion-group-delivery";
|
|
7
13
|
import { loadSettings } from "../../src/utils/settings";
|
|
8
14
|
import type { Member } from "../../src/utils/models";
|
|
9
15
|
import type { RunningReadAgent } from "../runtime/types";
|
|
@@ -259,6 +265,25 @@ export function createLifecycleRuntime(options: LifecycleRuntimeOptions) {
|
|
|
259
265
|
return;
|
|
260
266
|
}
|
|
261
267
|
|
|
268
|
+
const bound = createReportResult(teamName, member.name, expectedRunId, {});
|
|
269
|
+
const report = await readStoredTeamReportEvent(teamName, bound.reportId);
|
|
270
|
+
const result = { ...bound, verification: report?.result?.verification ?? bound.verification, repair: report?.result?.repair };
|
|
271
|
+
const candidate = currentMember ?? member;
|
|
272
|
+
const controller = new VerificationController({ teamName, result, cwd: candidate.cwd,
|
|
273
|
+
checks: candidate.assignedChecks, repair: candidate.repairPolicy });
|
|
274
|
+
if (candidate.repairPolicy || result.repair || fs.existsSync(controller.journalPath)) {
|
|
275
|
+
await controller.cancelAndRequireSettled();
|
|
276
|
+
}
|
|
277
|
+
if ((candidate.checkpointAssignment || member.checkpointAssignment) && !report?.checkpoint) throw new Error("Checkpoint report provenance is unavailable.");
|
|
278
|
+
if (report?.checkpoint) await resyncReportCheckpoint(report);
|
|
279
|
+
const groupBinding = candidate.completionGroup ?? member.completionGroup ?? report?.completionGroup;
|
|
280
|
+
if (groupBinding) {
|
|
281
|
+
const group = new CompletionGroup(teamName, groupBinding.groupId);
|
|
282
|
+
if (report?.result) await group.recordReport(groupBinding.slotId, report.result, report.status);
|
|
283
|
+
else await group.apply({ type: reason === "quit" ? "cancelled" : "interrupted", ...groupBinding,
|
|
284
|
+
runId: expectedRunId, reason: `Agent lifecycle ended (${reason}) without a final report.` });
|
|
285
|
+
await enqueueCompletionGroupDeliveries(group);
|
|
286
|
+
}
|
|
262
287
|
releasedClaims = await releaseClaims(teamName, member.name);
|
|
263
288
|
|
|
264
289
|
// Private transcript cleanup is the first destructive recovery step.
|
|
@@ -327,6 +352,7 @@ export function createLifecycleRuntime(options: LifecycleRuntimeOptions) {
|
|
|
327
352
|
if (drainQueue && (member.role ?? "write") === "write") {
|
|
328
353
|
void options.drainWriteQueue(teamName).catch(() => {});
|
|
329
354
|
}
|
|
355
|
+
try { expectedState?.onCostSettled?.(); } catch { /* Accounting cannot alter settlement. */ }
|
|
330
356
|
try {
|
|
331
357
|
options.onTeammateSettled?.(teamName, { ...member, lifecycleRunId: expectedRunId });
|
|
332
358
|
} catch {
|