pi-extended-teams 2.2.10 → 2.3.1
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 +37 -21
- package/extensions/index.ts +257 -60
- package/extensions/internal/pi-check-operations.ts +24 -0
- package/extensions/internal/session-files.ts +10 -2
- package/extensions/runtime/lead-attachment.ts +91 -0
- package/extensions/runtime/reload-owner.ts +107 -0
- package/extensions/runtime/team-host.ts +124 -0
- 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 +225 -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/public-team-tools.ts +51 -0
- package/extensions/tools/task-runtime-tools.ts +23 -4
- package/extensions/tools/team-tools.ts +365 -107
- package/extensions/ui/activity-colors.ts +50 -0
- package/extensions/ui/agent-follow-view.ts +15 -11
- package/extensions/ui/agent-navigation.ts +35 -1
- package/extensions/ui/checkpoints-command.ts +28 -0
- 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/settings.ts +24 -0
- package/src/utils/teams.ts +3 -1
- package/src/utils/write-queue.ts +3 -0
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";
|
|
@@ -22,24 +25,38 @@ import { parseQualifiedModel } from "../src/utils/model-resolution";
|
|
|
22
25
|
import { formatAnimatedProgress, formatContextUsage, formatElapsed, formatModelLabel } from "./ui/renderers.js";
|
|
23
26
|
import type { CompletedAgentReport, RunningReadAgent } from "./runtime/types.js";
|
|
24
27
|
import { createPendingChildController } from "./runtime/pending-child-controller.js";
|
|
28
|
+
import { createTeamHost } from "./runtime/team-host";
|
|
29
|
+
import { registerLeadAttachment, type TeamExecutionOwner } from "./runtime/lead-attachment";
|
|
25
30
|
import { createActiveAgentSleepController, runWithActiveAgentSleepAssertion } from "./runtime/active-agent-sleep.js";
|
|
26
31
|
import { createTeammateInterrupter } from "./runtime/teammate-interrupt.js";
|
|
27
32
|
export { panelBgFill, framePanel, frameWidget, frameWidgetFullWidth, logWindowStart } from "./ui/frame.js";
|
|
28
33
|
import * as messaging from "../src/utils/messaging";
|
|
34
|
+
import { requestCompletionGroupWake, observeCompletionGroupWakes } from "../src/results/completion-group-wake";
|
|
35
|
+
import { recoverCompletionGroups } from "../src/results/completion-group-recovery";
|
|
29
36
|
import * as teams from "../src/utils/teams";
|
|
30
37
|
import * as runtime from "../src/utils/runtime";
|
|
31
38
|
import * as teamPaths from "../src/utils/paths";
|
|
32
39
|
import { listReadHelperQueue, removeQueuedReadHelperRequest } from "../src/utils/read-helper-queue";
|
|
33
40
|
import type { Member } from "../src/utils/models";
|
|
41
|
+
import { projectAgentStatus, projectLifecycleFence } from "../src/orchestration/status-projection";
|
|
34
42
|
import { getTerminalAdapter } from "../src/adapters/terminal-registry";
|
|
35
43
|
import { createSpawnResourcePlan, parentProjectTrustForSpawn } from "./resources/spawn-resource-plan.js";
|
|
36
|
-
import { generateExtensionInstanceId, listLifecycleTombstones, onLifecycleTombstoneCleared, readLifecycleTombstone } from "../src/utils/lifecycle-tombstone";
|
|
44
|
+
import { generateExtensionInstanceId, generateLifecycleRunId, listLifecycleTombstones, onLifecycleTombstoneCleared, readLifecycleTombstone } from "../src/utils/lifecycle-tombstone";
|
|
37
45
|
|
|
38
|
-
export default function (pi: ExtensionAPI) {
|
|
46
|
+
export default function (pi: ExtensionAPI): void {
|
|
47
|
+
if (process.env.PI_AGENT_NAME) { createExecutionRuntime(pi); return; }
|
|
48
|
+
registerLeadAttachment(pi, () => {
|
|
49
|
+
const host = createTeamHost();
|
|
50
|
+
return createExecutionRuntime(host.api as unknown as ExtensionAPI, host);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function createExecutionRuntime(pi: ExtensionAPI, host?: ReturnType<typeof createTeamHost>): TeamExecutionOwner {
|
|
39
55
|
const isTeammate = !!process.env.PI_AGENT_NAME;
|
|
40
56
|
const agentName = process.env.PI_AGENT_NAME || "team-lead";
|
|
41
57
|
const extensionInstanceId = generateExtensionInstanceId();
|
|
42
58
|
const envTeamName = process.env.PI_TEAM_NAME;
|
|
59
|
+
const combinedSessionCost = isTeammate ? undefined : createCombinedSessionCost(pi);
|
|
43
60
|
|
|
44
61
|
// Teammates are explicitly bound by PI_TEAM_NAME. Lead sessions are adopted
|
|
45
62
|
// later at session_start only when the persisted Pi session id matches.
|
|
@@ -60,6 +77,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
60
77
|
let readHelperQueueDrainPending = false;
|
|
61
78
|
let sessionCtx: any = null;
|
|
62
79
|
let extensionShuttingDown = false;
|
|
80
|
+
let executionClosing = false;
|
|
81
|
+
const pendingRootAdmissions = new Set<Promise<void>>();
|
|
82
|
+
function trackRootAdmission(): () => void {
|
|
83
|
+
let settle!: () => void;
|
|
84
|
+
const pending = new Promise<void>(resolve => { settle = resolve; });
|
|
85
|
+
pendingRootAdmissions.add(pending);
|
|
86
|
+
return () => { pendingRootAdmissions.delete(pending); settle(); };
|
|
87
|
+
}
|
|
63
88
|
const lifecycleFenceClearUnsubscribe = onLifecycleTombstoneCleared((clearedTeamName) => {
|
|
64
89
|
if (clearedTeamName === teamName) void drainReadHelperQueueOnce();
|
|
65
90
|
});
|
|
@@ -75,6 +100,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
75
100
|
let teamActivityStatusSnapshotSignature: string | null = null;
|
|
76
101
|
let teamActivityWidgetMounted = false;
|
|
77
102
|
let teamActivityWidgetTui: { requestRender?: () => void } | null = null;
|
|
103
|
+
let presentationGeneration = 0;
|
|
78
104
|
let teamActivityRenderInFlight = false;
|
|
79
105
|
let teamActivityRenderDirty = false;
|
|
80
106
|
let teamActivityRenderTimer: NodeJS.Timeout | null = null;
|
|
@@ -86,8 +112,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
86
112
|
// instead of unread count prevents a read message being replaced by a new one
|
|
87
113
|
// at the same count from silently suppressing the new wake.
|
|
88
114
|
const leadWakeNotifiedMessageKeys = new Set<string>();
|
|
89
|
-
const createCurrentSpawnResourcePlan = (input: { cwd: string; projectTrusted: boolean }) => {
|
|
90
|
-
|
|
115
|
+
const createCurrentSpawnResourcePlan = async (input: { cwd: string; projectTrusted: boolean }) => {
|
|
116
|
+
while (host && !host.ready) await host.whenAttached();
|
|
117
|
+
const projectTrusted = host ? parentProjectTrustForSpawn(sessionCtx, input.cwd) : input.projectTrusted;
|
|
118
|
+
return createSpawnResourcePlan({ ...input, projectTrusted, pi });
|
|
91
119
|
};
|
|
92
120
|
let teamToolsRuntime: TeamToolsRuntime | undefined;
|
|
93
121
|
const pendingChildController = createPendingChildController();
|
|
@@ -172,11 +200,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
172
200
|
targetTeamName: string,
|
|
173
201
|
targetAgentName: string,
|
|
174
202
|
content: string,
|
|
175
|
-
expectedRecipientRunId?: string
|
|
203
|
+
expectedRecipientRunId?: string,
|
|
204
|
+
requireReceipt = false
|
|
176
205
|
): Promise<boolean> {
|
|
177
206
|
const target = runningReadAgents.get(readAgentKey(targetTeamName, targetAgentName));
|
|
178
207
|
if (expectedRecipientRunId !== undefined && target?.runId !== expectedRecipientRunId) return false;
|
|
179
|
-
const delivered = await sendMessageToRunningReadAgent(target, content);
|
|
208
|
+
const delivered = await sendMessageToRunningReadAgent(target, content, requireReceipt);
|
|
180
209
|
if (delivered) renderReadAgentStatus();
|
|
181
210
|
return delivered;
|
|
182
211
|
}
|
|
@@ -229,13 +258,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
229
258
|
return !!status.lastHeartbeatAt && (now - status.lastHeartbeatAt) <= runtime.HEARTBEAT_STALE_MS;
|
|
230
259
|
}
|
|
231
260
|
|
|
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
261
|
function isVisibleRuntimeOnlyMember(
|
|
240
262
|
member: Member,
|
|
241
263
|
runtimeStatus: runtime.AgentRuntimeStatus | null,
|
|
@@ -337,11 +359,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
337
359
|
// message with display:false still reaches the model as a user turn (see
|
|
338
360
|
// convertToLlm) but is never rendered, so team coordination stays silent.
|
|
339
361
|
// Falls back to a visible user message on older pi builds without sendMessage.
|
|
340
|
-
function quietTrigger(content: string): void {
|
|
362
|
+
function quietTrigger(content: string, details?: Record<string, unknown>): void {
|
|
341
363
|
const api = pi as any;
|
|
342
364
|
if (typeof api.sendMessage === "function") {
|
|
343
365
|
api.sendMessage(
|
|
344
|
-
{ customType: "pi-extended-teams-wake", content, display: false },
|
|
366
|
+
{ customType: "pi-extended-teams-wake", content, display: false, ...(details ? { details } : {}) },
|
|
345
367
|
{ triggerTurn: true, deliverAs: "followUp" }
|
|
346
368
|
);
|
|
347
369
|
} else {
|
|
@@ -378,9 +400,46 @@ export default function (pi: ExtensionAPI) {
|
|
|
378
400
|
pi.events?.emit?.("pi-extended-teams:agent-progress", { teamName: progressTeamName, name, status, updatedAt });
|
|
379
401
|
}
|
|
380
402
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
403
|
+
const currentCompletionGroups = new Set<string>();
|
|
404
|
+
const observedWakeEntries = new Set<string>();
|
|
405
|
+
const groupWarnings = new Set<string>();
|
|
406
|
+
const sessionEntries = (ctx: any) => ctx?.sessionManager?.getEntries?.() ?? ctx?.sessionManager?.getBranch?.() ?? [];
|
|
407
|
+
function warnGroup(error: unknown): void {
|
|
408
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
409
|
+
if (!groupWarnings.has(message)) {
|
|
410
|
+
groupWarnings.add(message);
|
|
411
|
+
sessionCtx?.ui?.notify?.(message, "warning");
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
async function observeGroupHistory(ctx: any): Promise<void> {
|
|
415
|
+
if (!teamName || extensionShuttingDown) return;
|
|
416
|
+
const target = teamName;
|
|
417
|
+
const entries = sessionEntries(ctx).filter((entry: any) => !observedWakeEntries.has(`${target}:${entry.id}`));
|
|
418
|
+
const result = await observeCompletionGroupWakes(target, getPiSessionId(ctx) ?? "", entries);
|
|
419
|
+
for (const id of result.observed) observedWakeEntries.add(`${target}:${id}`);
|
|
420
|
+
for (const error of result.errors) warnGroup(error);
|
|
421
|
+
}
|
|
422
|
+
async function requestGroupInboxWakes(unread: any[]): Promise<void> {
|
|
423
|
+
if (!teamName || !sessionCtx || extensionShuttingDown) return;
|
|
424
|
+
const target = teamName;
|
|
425
|
+
const ctx = sessionCtx;
|
|
426
|
+
for (const message of unread.filter(item => item.metadata?.completionGroup)) {
|
|
427
|
+
try {
|
|
428
|
+
if (typeof (pi as any).sendMessage !== "function") throw new Error("Grouped wakes require Pi custom messages; use read_inbox for the saved index.");
|
|
429
|
+
await requestCompletionGroupWake(target, getPiSessionId(ctx) ?? "", message, (content, details) => {
|
|
430
|
+
if (extensionShuttingDown || ctx !== sessionCtx) throw new Error("Group wake unconfirmed: the lead session is closing.");
|
|
431
|
+
quietTrigger(content, details);
|
|
432
|
+
});
|
|
433
|
+
} catch (error) { warnGroup(error); }
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
async function wakeLeadForInboxReports(unread: any[]): Promise<void> {
|
|
437
|
+
if (!teamName || extensionShuttingDown) return;
|
|
438
|
+
const target = teamName;
|
|
439
|
+
const ctx = sessionCtx;
|
|
440
|
+
await requestGroupInboxWakes(unread);
|
|
441
|
+
if (extensionShuttingDown || target !== teamName || ctx !== sessionCtx) return;
|
|
442
|
+
const keyedUnread = unread.filter(message => !message.metadata?.completionGroup).map((message) => ({
|
|
384
443
|
message,
|
|
385
444
|
key: String(message?.id || `${message?.from || "unknown"}:${message?.timestamp || ""}:${message?.text || ""}`),
|
|
386
445
|
}));
|
|
@@ -409,10 +468,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
409
468
|
}
|
|
410
469
|
|
|
411
470
|
async function notifyLeadOfInboxReports(targetTeamName: string): Promise<void> {
|
|
412
|
-
if (isTeammate) return;
|
|
471
|
+
if (isTeammate || extensionShuttingDown) return;
|
|
472
|
+
const generation = presentationGeneration;
|
|
413
473
|
const unread = await messaging.peekInbox(targetTeamName, agentName, true);
|
|
474
|
+
if (generation !== presentationGeneration) return;
|
|
414
475
|
await renderLeadInboxStatus();
|
|
415
|
-
|
|
476
|
+
if (generation !== presentationGeneration) return;
|
|
477
|
+
await wakeLeadForInboxReports(unread);
|
|
416
478
|
}
|
|
417
479
|
|
|
418
480
|
function getIdleReadAgentNudgeMessage(agent: RunningReadAgent, status: ReadAgentStatusDescription): string | null {
|
|
@@ -492,6 +554,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
492
554
|
return;
|
|
493
555
|
}
|
|
494
556
|
|
|
557
|
+
const generation = presentationGeneration;
|
|
495
558
|
teamActivityRenderInFlight = true;
|
|
496
559
|
let renderError: unknown;
|
|
497
560
|
try {
|
|
@@ -500,6 +563,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
500
563
|
} catch (error) {
|
|
501
564
|
renderError = error;
|
|
502
565
|
} finally {
|
|
566
|
+
if (generation !== presentationGeneration) return;
|
|
503
567
|
teamActivityRenderInFlight = false;
|
|
504
568
|
if (renderError) {
|
|
505
569
|
teamActivityRenderDirty = false;
|
|
@@ -564,14 +628,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
564
628
|
|
|
565
629
|
async function renderTeamActivityStatusNow() {
|
|
566
630
|
if (!sessionCtx?.ui) return;
|
|
631
|
+
const generation = presentationGeneration;
|
|
632
|
+
const isCurrent = () => generation === presentationGeneration && !extensionShuttingDown;
|
|
567
633
|
|
|
568
634
|
const now = Date.now();
|
|
569
635
|
const activityTeamName = teamName || null;
|
|
570
|
-
const
|
|
636
|
+
const sessionAgents = activityTeamName
|
|
571
637
|
? Array.from(runningReadAgents.values())
|
|
572
|
-
.filter(agent => agent.teamName === activityTeamName
|
|
638
|
+
.filter(agent => agent.teamName === activityTeamName)
|
|
573
639
|
.sort((a, b) => a.name.localeCompare(b.name))
|
|
574
640
|
: [];
|
|
641
|
+
const runningAgents = sessionAgents.filter(hasActiveReadAgentLifecycle);
|
|
575
642
|
const readAgents = runningAgents.filter(agent => (agent.role || "read") === "read");
|
|
576
643
|
const writeAgents = runningAgents.filter(agent => (agent.role || "read") === "write");
|
|
577
644
|
const activityConfig = activityTeamName ? await teams.readConfig(activityTeamName).catch(() => null) : null;
|
|
@@ -579,17 +646,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
579
646
|
const activityTombstones = activityTeamName
|
|
580
647
|
? await listLifecycleTombstones(activityTeamName).catch(() => [])
|
|
581
648
|
: [];
|
|
582
|
-
const
|
|
583
|
-
|
|
649
|
+
const unsettledAgents = sessionAgents.filter(agent => !hasActiveReadAgentLifecycle(agent)
|
|
650
|
+
&& agent.teardownState !== "finalized"
|
|
651
|
+
&& !activityTombstones.some(fence => fence.agentName === agent.name));
|
|
652
|
+
const runtimeOnlyCandidates = activityTeamName
|
|
653
|
+
? await Promise.all(activityMembers
|
|
584
654
|
.filter(member => member.name !== "team-lead")
|
|
585
655
|
.filter(member => !runningReadAgents.has(readAgentKey(activityTeamName, member.name)))
|
|
586
656
|
.map(async (member) => ({
|
|
587
657
|
member,
|
|
588
658
|
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)
|
|
659
|
+
})))
|
|
592
660
|
: [];
|
|
661
|
+
if (!isCurrent()) return;
|
|
662
|
+
const runtimeOnlyMembers = runtimeOnlyCandidates
|
|
663
|
+
.filter((entry): entry is { member: Member; runtimeStatus: runtime.AgentRuntimeStatus } => isVisibleRuntimeOnlyMember(entry.member, entry.runtimeStatus, now))
|
|
664
|
+
.filter(({ member, runtimeStatus }) => member.lifecycleRunId === runtimeStatus.lifecycleRunId);
|
|
593
665
|
const runtimeOnlyMemberNames = new Set(runtimeOnlyMembers.map(({ member }) => member.name));
|
|
594
666
|
const runtimeOnlyReadMembers = runtimeOnlyMembers.filter(({ member }) => memberActivityRole(member) === "read");
|
|
595
667
|
const runtimeOnlyWriteMembers = runtimeOnlyMembers.filter(({ member }) => memberActivityRole(member) === "write");
|
|
@@ -618,6 +690,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
618
690
|
.filter(member => member.name !== "team-lead" && member.isActive !== false && memberActivityRole(member) === "write")
|
|
619
691
|
.filter(member => !!(member.tmuxPaneId && terminal?.isAlive?.(member.tmuxPaneId)) && !runningReadAgents.has(readAgentKey(activityTeamName, member.name)) && !runtimeOnlyMemberNames.has(member.name)) ?? []
|
|
620
692
|
: [];
|
|
693
|
+
const unobservedMembers = runtimeOnlyCandidates.filter(({ member }) => member.isActive !== false
|
|
694
|
+
&& !runtimeOnlyMemberNames.has(member.name)
|
|
695
|
+
&& !activeWriteMembers.some(writer => writer.name === member.name)
|
|
696
|
+
&& !activityTombstones.some(fence => fence.agentName === member.name));
|
|
697
|
+
const activeCount = runningAgents.length + runtimeOnlyMembers.length + activeWriteMembers.length + unobservedMembers.length;
|
|
621
698
|
navigationRuntimeAgents.clear();
|
|
622
699
|
for (const { member, runtimeStatus } of runtimeOnlyReadMembers) {
|
|
623
700
|
if (member.lifecycleRunId === runtimeStatus.lifecycleRunId) {
|
|
@@ -630,14 +707,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
630
707
|
}
|
|
631
708
|
}
|
|
632
709
|
const unreadLeadMessages = activityTeamName ? await messaging.readInbox(activityTeamName, agentName, true, false).catch(() => []) : [];
|
|
710
|
+
if (!isCurrent()) return;
|
|
633
711
|
leadInboxUnreadCount = unreadLeadMessages.length;
|
|
634
712
|
clearLeadInboxWidgetOnce();
|
|
635
713
|
|
|
636
|
-
|
|
714
|
+
const hasVisibleAgents = activeCount > 0 || unsettledAgents.length > 0 || activityTombstones.length > 0;
|
|
715
|
+
if (hasVisibleAgents && !readAgentStatusTimer) {
|
|
637
716
|
readAgentStatusTimer = setInterval(renderReadAgentStatus, 1000);
|
|
638
717
|
}
|
|
639
718
|
|
|
640
|
-
if (
|
|
719
|
+
if (!hasVisibleAgents) {
|
|
641
720
|
sessionCtx.ui.setStatus?.("01-pi-extended-teams-read", undefined);
|
|
642
721
|
clearTeamActivityWidget();
|
|
643
722
|
sessionCtx.ui.setWidget?.("01-pi-extended-teams-status", undefined);
|
|
@@ -648,7 +727,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
648
727
|
return;
|
|
649
728
|
}
|
|
650
729
|
|
|
651
|
-
const activeCount = readAgents.length + writeAgents.length + runtimeOnlyReadMembers.length + runtimeOnlyWriteMembers.length + activeWriteMembers.length;
|
|
652
730
|
const entries: TeamActivityStatusEntry[] = [];
|
|
653
731
|
const footerStatuses: string[] = [];
|
|
654
732
|
const statusCounts: Record<string, number> = {};
|
|
@@ -698,6 +776,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
698
776
|
joinedAt: member.joinedAt,
|
|
699
777
|
});
|
|
700
778
|
const runtimeStatus = await runtime.readRuntimeStatus(activityTeamName!, member.name).catch(() => null);
|
|
779
|
+
if (!isCurrent()) return;
|
|
701
780
|
const elapsed = formatElapsed(now - (runtimeStatus?.startedAt || member.joinedAt));
|
|
702
781
|
const modelLabel = formatModelLabel(member.model, member.thinking);
|
|
703
782
|
const contextUsage = formatRunningContextUsage(runtimeStatus?.contextUsage);
|
|
@@ -742,16 +821,43 @@ export default function (pi: ExtensionAPI) {
|
|
|
742
821
|
footerStatuses.push(formatAgentProgressStatus(agent.name, agent.model, agent.thinking, agent.modelSlot, elapsed, contextUsage, agent.latestProgress, now));
|
|
743
822
|
}
|
|
744
823
|
|
|
824
|
+
for (const { member, runtimeStatus } of unobservedMembers) {
|
|
825
|
+
const projected = projectAgentStatus({ member, runtime: runtimeStatus, fence: { status: "absent" }, terminalAlive: null, now });
|
|
826
|
+
const telemetry = !projected.runtime ? "runtime telemetry unavailable"
|
|
827
|
+
: runtimeHeartbeatIsRecent(projected.runtime, now) ? "awaiting runtime readiness" : "heartbeat stale";
|
|
828
|
+
countStatus(projected.phase);
|
|
829
|
+
entries.push({ name: member.name, role: memberActivityRole(member), status: projected.phase, detail: telemetry });
|
|
830
|
+
footerStatuses.push(`(${member.name}) ${projected.phase} · ${telemetry}`);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
for (const agent of unsettledAgents) {
|
|
834
|
+
const status = agent.teardownState === "persistence_failed" ? "persistence-failed"
|
|
835
|
+
: agent.teardownState === "quarantined" ? "quarantined" : "stopping";
|
|
836
|
+
const reason = agent.teardownError instanceof Error ? agent.teardownError.message
|
|
837
|
+
: agent.teardownResult?.error || "cleanup has not settled";
|
|
838
|
+
const detail = status === "stopping" ? "Finishing cleanup" : `Cleanup blocked: ${reason} · use check_teammate`;
|
|
839
|
+
countStatus(status);
|
|
840
|
+
entries.push({ name: agent.name, role: agent.role === "write" ? "write" : "read", status, detail });
|
|
841
|
+
}
|
|
842
|
+
|
|
745
843
|
for (const { agentName: quarantinedName, result } of activityTombstones) {
|
|
844
|
+
if (result.status === "occupied" && result.tombstone.phase === "persistence_closed"
|
|
845
|
+
&& runningAgents.some(agent => agent.name === quarantinedName && agent.runId === result.tombstone.runId)) {
|
|
846
|
+
const entryIndex = entries.findIndex(entry => entry.name === quarantinedName);
|
|
847
|
+
footerStatuses[entryIndex] += " · Finishing cleanup";
|
|
848
|
+
continue;
|
|
849
|
+
}
|
|
746
850
|
const persistedMember = activityMembers.find(member => member.name === quarantinedName);
|
|
747
851
|
const role = result.status === "occupied"
|
|
748
852
|
? result.tombstone.role
|
|
749
853
|
: persistedMember ? memberActivityRole(persistedMember) : "read";
|
|
750
|
-
const
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
854
|
+
const projected = projectLifecycleFence(persistedMember ?? {}, result);
|
|
855
|
+
const reason = projected.error || (result.status === "occupied" && result.tombstone.phase === "timed_out" ? "cleanup timed out" : "cleanup failed");
|
|
856
|
+
const detail = projected.phase === "stopping"
|
|
857
|
+
? "Finishing cleanup"
|
|
858
|
+
: `Cleanup blocked: ${reason} · use check_teammate`;
|
|
859
|
+
countStatus(projected.phase);
|
|
860
|
+
entries.push({ name: quarantinedName, role, status: projected.phase, detail });
|
|
755
861
|
}
|
|
756
862
|
|
|
757
863
|
wakeLeadForIdleReadAgents(idleNudgeMessages);
|
|
@@ -762,8 +868,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
762
868
|
sessionCtx.ui.setWidget?.("01-pi-extended-teams-status", undefined);
|
|
763
869
|
updateTeamActivityWidget({
|
|
764
870
|
activeCount,
|
|
765
|
-
readCount: readAgents.length + runtimeOnlyReadMembers.length,
|
|
766
|
-
writeCount: writeAgents.length + runtimeOnlyWriteMembers.length + activeWriteMembers.length,
|
|
871
|
+
readCount: readAgents.length + runtimeOnlyReadMembers.length + unobservedMembers.filter(({ member }) => memberActivityRole(member) === "read").length,
|
|
872
|
+
writeCount: writeAgents.length + runtimeOnlyWriteMembers.length + activeWriteMembers.length + unobservedMembers.filter(({ member }) => memberActivityRole(member) === "write").length,
|
|
767
873
|
unreadCount: leadInboxUnreadCount,
|
|
768
874
|
entries,
|
|
769
875
|
statusCounts,
|
|
@@ -795,12 +901,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
795
901
|
}
|
|
796
902
|
|
|
797
903
|
async function drainReadHelperQueueOnce() {
|
|
798
|
-
if (isTeammate || !sessionCtx || !teamName) return;
|
|
904
|
+
if (isTeammate || !sessionCtx || !teamName || (host && !host.ready)) return;
|
|
799
905
|
if (readHelperQueueDraining) {
|
|
800
906
|
readHelperQueueDrainPending = true;
|
|
801
907
|
return;
|
|
802
908
|
}
|
|
803
909
|
readHelperQueueDraining = true;
|
|
910
|
+
const settled = trackRootAdmission();
|
|
804
911
|
try {
|
|
805
912
|
for (let drained = 0; drained < 10 && teamName; drained++) {
|
|
806
913
|
const [nextQueued] = await listReadHelperQueue(teamName);
|
|
@@ -811,6 +918,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
811
918
|
|
|
812
919
|
try {
|
|
813
920
|
const config = await teams.readConfig(queued.teamName);
|
|
921
|
+
const launchContext = sessionCtx;
|
|
922
|
+
if (!launchContext || (host && !host.ready)) return;
|
|
814
923
|
if (config.members.some(member => member.name === queued.name)) {
|
|
815
924
|
throw new Error(`Teammate ${queued.name} already exists in team ${queued.teamName}.`);
|
|
816
925
|
}
|
|
@@ -819,7 +928,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
819
928
|
const parsedModel = parseQualifiedModel(level.model);
|
|
820
929
|
const provider = parsedModel?.provider;
|
|
821
930
|
const modelId = parsedModel?.model;
|
|
822
|
-
const model = provider && modelId ?
|
|
931
|
+
const model = provider && modelId ? launchContext.modelRegistry?.find?.(provider, modelId) : undefined;
|
|
823
932
|
if (!model) throw new Error(`Read helper model \"${level.model}\" from intent tier ${level.slot} is not available in the lead session.`);
|
|
824
933
|
|
|
825
934
|
const helperPrompt = buildReadHelperPrompt(queued.teamName, queued.requester, queued.prompt);
|
|
@@ -841,9 +950,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
841
950
|
helperKind: "read_helper",
|
|
842
951
|
};
|
|
843
952
|
|
|
953
|
+
member.lifecycleRunId = generateLifecycleRunId();
|
|
844
954
|
await teams.addMember(queued.teamName, member);
|
|
955
|
+
if (executionClosing) {
|
|
956
|
+
await shutdownTeammate(queued.teamName, member);
|
|
957
|
+
await removeQueuedReadHelperRequest(queued.teamName, queued.id);
|
|
958
|
+
break;
|
|
959
|
+
}
|
|
845
960
|
observeReadAgentLaunch(
|
|
846
|
-
runInProcessAgentWithSleepAssertion(queued.teamName, member, helperPrompt,
|
|
961
|
+
runInProcessAgentWithSleepAssertion(queued.teamName, member, helperPrompt, launchContext, readAgentOptions())
|
|
847
962
|
);
|
|
848
963
|
await removeQueuedReadHelperRequest(teamName, queued.id);
|
|
849
964
|
} catch (e) {
|
|
@@ -896,6 +1011,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
896
1011
|
}
|
|
897
1012
|
}
|
|
898
1013
|
} finally {
|
|
1014
|
+
settled();
|
|
899
1015
|
readHelperQueueDraining = false;
|
|
900
1016
|
if (readHelperQueueDrainPending) {
|
|
901
1017
|
readHelperQueueDrainPending = false;
|
|
@@ -948,10 +1064,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
948
1064
|
|
|
949
1065
|
leadInboxPollTimer = setInterval(async () => {
|
|
950
1066
|
if (!teamName) return;
|
|
1067
|
+
const generation = presentationGeneration;
|
|
951
1068
|
try {
|
|
952
1069
|
const unread = await messaging.readInbox(teamName, agentName, true, false);
|
|
1070
|
+
if (generation !== presentationGeneration) return;
|
|
953
1071
|
await renderLeadInboxStatus();
|
|
954
|
-
|
|
1072
|
+
if (generation !== presentationGeneration) return;
|
|
1073
|
+
await wakeLeadForInboxReports(unread);
|
|
955
1074
|
} catch {
|
|
956
1075
|
// Ignore errors for lead polling
|
|
957
1076
|
}
|
|
@@ -965,10 +1084,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
965
1084
|
function adoptTeamAsLead(name: string, ctx?: any): void {
|
|
966
1085
|
if (isTeammate || !name) return;
|
|
967
1086
|
const sessionId = getPiSessionId(ctx ?? sessionCtx);
|
|
1087
|
+
registerLeadSession(name, sessionId);
|
|
1088
|
+
// Private jobs retain cleanup ownership, not the public roster/inbox/UI scope.
|
|
1089
|
+
if (name.startsWith("prompt-build-")) return;
|
|
968
1090
|
if (teamName !== name) {
|
|
969
1091
|
teamName = name;
|
|
970
1092
|
}
|
|
971
|
-
registerLeadSession(name, sessionId);
|
|
972
1093
|
ensureReadAgentStatusTicker();
|
|
973
1094
|
startLeadInboxPolling();
|
|
974
1095
|
startReadHelperQueueDraining();
|
|
@@ -978,11 +1099,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
978
1099
|
pi.events?.on?.("pi-prompt:prompt-build:start", async (payload: any) => {
|
|
979
1100
|
if (isTeammate || !sessionCtx) return;
|
|
980
1101
|
const requestedTeamName = teamPaths.sanitizeName(payload?.teamName || `prompt-build-${Date.now()}`);
|
|
981
|
-
|
|
1102
|
+
const launchContext = sessionCtx;
|
|
1103
|
+
const settled = trackRootAdmission();
|
|
982
1104
|
try {
|
|
983
1105
|
const prompts = Array.isArray(payload?.prompts) ? payload.prompts : [];
|
|
984
1106
|
activePromptBuildTeamName = requestedTeamName;
|
|
985
|
-
const cwd = payload?.cwd ||
|
|
1107
|
+
const cwd = payload?.cwd || launchContext.cwd;
|
|
986
1108
|
const agentNamePrefix = teamPaths.sanitizeName(payload?.agentNamePrefix || "prompt-branch");
|
|
987
1109
|
if (payload?.model || payload?.thinking || payload?.role) {
|
|
988
1110
|
throw new Error("Prompt-build agents must use model_slot only; direct model, thinking, or role is not allowed.");
|
|
@@ -992,7 +1114,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
992
1114
|
if (level.role !== "read") throw new Error(`Prompt-build requires a read-* intent tier configured via /agents-favorite-models, got ${level.slot}.`);
|
|
993
1115
|
|
|
994
1116
|
if (!teams.teamExists(requestedTeamName)) {
|
|
995
|
-
teams.createTeam(requestedTeamName, getPiSessionId(
|
|
1117
|
+
teams.createTeam(requestedTeamName, getPiSessionId(launchContext) || "local-session", "lead-agent", payload?.description || "pi-prompt prompt-build", level.model);
|
|
996
1118
|
}
|
|
997
1119
|
// Prompt-build teams are private fanout jobs owned by pi-prompt. Do not
|
|
998
1120
|
// adopt them as the lead's current team, or normal session context and
|
|
@@ -1017,21 +1139,25 @@ export default function (pi: ExtensionAPI) {
|
|
|
1017
1139
|
thinking: level.thinking,
|
|
1018
1140
|
modelSlot: level.slot,
|
|
1019
1141
|
};
|
|
1142
|
+
if (executionClosing) break;
|
|
1143
|
+
member.lifecycleRunId = generateLifecycleRunId();
|
|
1020
1144
|
await teams.addMember(requestedTeamName, member);
|
|
1145
|
+
if (executionClosing) { await shutdownTeammate(requestedTeamName, member); break; }
|
|
1021
1146
|
observeReadAgentLaunch(
|
|
1022
|
-
runInProcessAgentWithSleepAssertion(requestedTeamName, member, prompt,
|
|
1147
|
+
runInProcessAgentWithSleepAssertion(requestedTeamName, member, prompt, launchContext, readAgentOptions())
|
|
1023
1148
|
);
|
|
1024
1149
|
pi.events?.emit?.("pi-prompt:prompt-build:progress", { teamName: requestedTeamName, status: "spawned", started: i + 1, total: prompts.length, text: `building prompt — ${i + 1}/${prompts.length} branches started` });
|
|
1025
1150
|
}
|
|
1026
1151
|
} catch (err) {
|
|
1027
1152
|
const message = err instanceof Error ? err.message : String(err);
|
|
1028
1153
|
pi.events?.emit?.("pi-prompt:prompt-build:error", { teamName: requestedTeamName, error: message });
|
|
1029
|
-
}
|
|
1154
|
+
} finally { settled(); }
|
|
1030
1155
|
});
|
|
1031
1156
|
|
|
1032
1157
|
function setSessionCtx(ctx: any): void {
|
|
1158
|
+
if (host && !host.attached) return;
|
|
1033
1159
|
extensionShuttingDown = false;
|
|
1034
|
-
sessionCtx = ctx;
|
|
1160
|
+
sessionCtx = host?.context ?? ctx;
|
|
1035
1161
|
if (!isTeammate && !teamName) {
|
|
1036
1162
|
const foundTeam = findLeadTeamForSession(getPiSessionId(ctx));
|
|
1037
1163
|
if (foundTeam) adoptTeamAsLead(foundTeam, ctx);
|
|
@@ -1065,7 +1191,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
1065
1191
|
}
|
|
1066
1192
|
}
|
|
1067
1193
|
|
|
1068
|
-
|
|
1194
|
+
function detachPresentation(): void {
|
|
1195
|
+
presentationGeneration++;
|
|
1196
|
+
teamActivityRenderInFlight = false;
|
|
1197
|
+
navigationRuntimeAgents.clear();
|
|
1069
1198
|
extensionShuttingDown = true;
|
|
1070
1199
|
sessionCtx = null;
|
|
1071
1200
|
stopLeadWatchdog();
|
|
@@ -1081,16 +1210,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
1081
1210
|
readHelperQueueFallbackTimer = null;
|
|
1082
1211
|
readHelperQueueWatcher = null;
|
|
1083
1212
|
leadPollingStarted = false;
|
|
1084
|
-
leadWakeNotifiedMessageKeys.clear();
|
|
1085
1213
|
readHelperQueueFallbackStarted = false;
|
|
1086
1214
|
readHelperQueueWatchedTeam = null;
|
|
1087
|
-
lifecycleFenceClearUnsubscribe();
|
|
1088
1215
|
teamActivityRenderDirty = false;
|
|
1089
1216
|
settleTeamActivityRenderWaiters();
|
|
1217
|
+
teamActivityStatusSnapshot = null;
|
|
1218
|
+
teamActivityStatusSnapshotSignature = null;
|
|
1219
|
+
teamActivityWidgetMounted = false;
|
|
1220
|
+
teamActivityWidgetTui = null;
|
|
1221
|
+
}
|
|
1090
1222
|
|
|
1223
|
+
async function cleanupExtensionRuntime(ctx: any, reason: unknown): Promise<void> {
|
|
1224
|
+
detachPresentation();
|
|
1225
|
+
leadWakeNotifiedMessageKeys.clear();
|
|
1226
|
+
lifecycleFenceClearUnsubscribe();
|
|
1091
1227
|
try {
|
|
1092
1228
|
await stopRunningAgentsForShutdown(reason);
|
|
1093
1229
|
} finally {
|
|
1230
|
+
combinedSessionCost?.deactivate();
|
|
1094
1231
|
activeAgentSleepController.dispose();
|
|
1095
1232
|
}
|
|
1096
1233
|
|
|
@@ -1121,14 +1258,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
1121
1258
|
});
|
|
1122
1259
|
|
|
1123
1260
|
if (!isTeammate) {
|
|
1261
|
+
pi.on("context", async (_event: any, ctx: any) => { await observeGroupHistory(ctx); });
|
|
1124
1262
|
pi.on("session_start", async (_event: any, ctx: any) => {
|
|
1263
|
+
await observeGroupHistory(ctx);
|
|
1264
|
+
if (teamName) {
|
|
1265
|
+
try {
|
|
1266
|
+
const config = await teams.readConfig(teamName);
|
|
1267
|
+
for (const error of await recoverCompletionGroups(teamName, getPiSessionId(ctx) ?? "", config.members, sessionEntries(ctx), currentCompletionGroups)) warnGroup(error);
|
|
1268
|
+
await requestGroupInboxWakes(await messaging.peekInbox(teamName, agentName, true));
|
|
1269
|
+
} catch (error) { warnGroup(error); }
|
|
1270
|
+
}
|
|
1125
1271
|
installAgentNavigation(ctx, {
|
|
1126
1272
|
getAgents: () => {
|
|
1127
1273
|
const readers = Array.from(runningReadAgents.values())
|
|
1128
1274
|
.filter(agent => !teamName || agent.teamName === teamName);
|
|
1129
|
-
|
|
1130
|
-
.filter(agent => !teamName || agent.teamName === teamName)
|
|
1131
|
-
|
|
1275
|
+
const runtimeAgents = Array.from(navigationRuntimeAgents.values())
|
|
1276
|
+
.filter(agent => !teamName || agent.teamName === teamName);
|
|
1277
|
+
return orderAgentNavigationEntries(readers, runtimeAgents);
|
|
1132
1278
|
},
|
|
1133
1279
|
interruptAgent: async (name: string) => {
|
|
1134
1280
|
const result = await interruptTeammate(name);
|
|
@@ -1188,6 +1334,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1188
1334
|
|
|
1189
1335
|
registerFavoriteModelsCommand(pi);
|
|
1190
1336
|
registerExtensionsCommand(pi);
|
|
1337
|
+
if (!isTeammate) registerOnboardingCommand(pi);
|
|
1338
|
+
if (!isTeammate) registerCheckpointsCommand(pi);
|
|
1191
1339
|
|
|
1192
1340
|
registerWriterScreenShortcut(pi, {
|
|
1193
1341
|
getTeamName: () => teamName,
|
|
@@ -1218,7 +1366,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
1218
1366
|
createNestedReadAgentTools: (binding: Parameters<TeamToolsRuntime["createNestedReadAgentTools"]>[0]) => {
|
|
1219
1367
|
return teamToolsRuntime?.createNestedReadAgentTools(binding) ?? [];
|
|
1220
1368
|
},
|
|
1369
|
+
nestedChildSnapshot: (binding: Parameters<TeamToolsRuntime["nestedChildSnapshot"]>[0]) => {
|
|
1370
|
+
return teamToolsRuntime?.nestedChildSnapshot(binding) ?? { running: 0, queued: 0 };
|
|
1371
|
+
},
|
|
1221
1372
|
pendingChildController,
|
|
1373
|
+
beginCostRun: combinedSessionCost?.begin,
|
|
1222
1374
|
};
|
|
1223
1375
|
}
|
|
1224
1376
|
|
|
@@ -1233,6 +1385,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1233
1385
|
startWriteAgent,
|
|
1234
1386
|
shutdownTeammate,
|
|
1235
1387
|
adoptTeamAsLead,
|
|
1388
|
+
onCompletionGroupUse: groupId => { currentCompletionGroups.add(groupId); },
|
|
1236
1389
|
buildRoster,
|
|
1237
1390
|
isTeammate,
|
|
1238
1391
|
agentName,
|
|
@@ -1240,6 +1393,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1240
1393
|
getSessionCtx: () => sessionCtx,
|
|
1241
1394
|
setSessionCtx,
|
|
1242
1395
|
pendingChildController,
|
|
1396
|
+
isHostAttached: () => !host || host.ready,
|
|
1397
|
+
waitForHost: () => host?.whenAttached() ?? Promise.resolve(),
|
|
1243
1398
|
});
|
|
1244
1399
|
|
|
1245
1400
|
if (isTeammate) {
|
|
@@ -1251,7 +1406,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1251
1406
|
}));
|
|
1252
1407
|
}
|
|
1253
1408
|
|
|
1254
|
-
registerCoordinationTools
|
|
1409
|
+
const coordination: Parameters<typeof registerCoordinationTools>[1] = {
|
|
1255
1410
|
agentName,
|
|
1256
1411
|
isTeammate,
|
|
1257
1412
|
terminal,
|
|
@@ -1266,9 +1421,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
1266
1421
|
resetLeadWakeNotifiedCount: () => { leadWakeNotifiedMessageKeys.clear(); },
|
|
1267
1422
|
deliverMessageToActiveAgent,
|
|
1268
1423
|
extensionInstanceId,
|
|
1269
|
-
}
|
|
1424
|
+
};
|
|
1425
|
+
if (isTeammate) registerCoordinationTools(pi, coordination);
|
|
1270
1426
|
|
|
1271
|
-
registerTaskRuntimeTools
|
|
1427
|
+
const taskTools: Parameters<typeof registerTaskRuntimeTools>[1] = {
|
|
1272
1428
|
isTeammate,
|
|
1273
1429
|
terminal,
|
|
1274
1430
|
runningReadAgents,
|
|
@@ -1277,6 +1433,47 @@ export default function (pi: ExtensionAPI) {
|
|
|
1277
1433
|
cancelQueuedAgent: (targetTeamName, targetAgentName) => teamToolsRuntime?.cancelQueuedAgent(targetTeamName, targetAgentName) ?? false,
|
|
1278
1434
|
shutdownTeammate,
|
|
1279
1435
|
getTeamName: () => teamName,
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1436
|
+
};
|
|
1437
|
+
if (isTeammate) registerTaskRuntimeTools(pi, taskTools);
|
|
1438
|
+
|
|
1439
|
+
return {
|
|
1440
|
+
host: host!, coordination, taskTools,
|
|
1441
|
+
hasWork: () => runningReadAgents.size > 0 || activeWritersTabs.length > 0 || pendingRootAdmissions.size > 0 || teamToolsRuntime?.hasPendingWork() === true,
|
|
1442
|
+
async attach(binding) {
|
|
1443
|
+
host!.attach(binding);
|
|
1444
|
+
const ctx = host!.snapshot(binding.ctx);
|
|
1445
|
+
const costAttached = combinedSessionCost?.attach(binding.pi, ctx);
|
|
1446
|
+
await host!.emit("session_start", binding.event, ctx);
|
|
1447
|
+
if (!host!.attached) return;
|
|
1448
|
+
if (!costAttached) combinedSessionCost?.attach(binding.pi, ctx);
|
|
1449
|
+
},
|
|
1450
|
+
activate() {
|
|
1451
|
+
host!.flush();
|
|
1452
|
+
teamToolsRuntime?.resumeQueues();
|
|
1453
|
+
void drainReadHelperQueueOnce();
|
|
1454
|
+
if (teamName) void notifyLeadOfInboxReports(teamName).catch(warnGroup);
|
|
1455
|
+
},
|
|
1456
|
+
detach() {
|
|
1457
|
+
sessionCtx?.ui?.setWidget?.("01-pi-extended-teams-readers", undefined);
|
|
1458
|
+
sessionCtx?.ui?.setWidget?.("01-pi-extended-teams-status", undefined);
|
|
1459
|
+
detachPresentation();
|
|
1460
|
+
combinedSessionCost?.detach();
|
|
1461
|
+
host?.detach();
|
|
1462
|
+
},
|
|
1463
|
+
async close(reason) {
|
|
1464
|
+
teamToolsRuntime.closeAdmission();
|
|
1465
|
+
executionClosing = true;
|
|
1466
|
+
sessionCtx?.ui?.setWidget?.("01-pi-extended-teams-readers", undefined);
|
|
1467
|
+
sessionCtx?.ui?.setWidget?.("01-pi-extended-teams-status", undefined);
|
|
1468
|
+
detachPresentation();
|
|
1469
|
+
host?.close();
|
|
1470
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1471
|
+
const admissions = Promise.race([
|
|
1472
|
+
Promise.all([...pendingRootAdmissions]),
|
|
1473
|
+
new Promise<never>((_resolve, reject) => { timer = setTimeout(() => reject(new Error("Team admission cleanup is still pending.")), 2_500); }),
|
|
1474
|
+
]).finally(() => clearTimeout(timer));
|
|
1475
|
+
await Promise.all([host!.emit("session_shutdown", { reason }, undefined), admissions]);
|
|
1476
|
+
if (runningReadAgents.size) throw new Error("Team cleanup is still fenced by unsettled agents.");
|
|
1477
|
+
},
|
|
1478
|
+
};
|
|
1282
1479
|
}
|