pi-extended-teams 2.2.10 → 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.
Files changed (53) hide show
  1. package/README.md +182 -2
  2. package/extensions/agents/read-agent-report.ts +11 -2
  3. package/extensions/agents/read-agent-session-lifecycle.ts +25 -10
  4. package/extensions/agents/read-agent.ts +387 -65
  5. package/extensions/agents/write-agent.ts +3 -0
  6. package/extensions/events/register-events.ts +22 -10
  7. package/extensions/index.ts +132 -38
  8. package/extensions/internal/pi-check-operations.ts +24 -0
  9. package/extensions/internal/session-files.ts +10 -2
  10. package/extensions/runtime/teammate-interrupt.ts +11 -5
  11. package/extensions/runtime/types.ts +4 -0
  12. package/extensions/team/lifecycle.ts +26 -0
  13. package/extensions/team/session-cost.ts +155 -0
  14. package/extensions/tools/agent-communication-tools.ts +24 -8
  15. package/extensions/tools/agent-status-tool.ts +50 -7
  16. package/extensions/tools/coordination-tools.ts +78 -8
  17. package/extensions/tools/task-runtime-tools.ts +23 -4
  18. package/extensions/tools/team-tools.ts +287 -40
  19. package/extensions/ui/activity-colors.ts +59 -0
  20. package/extensions/ui/agent-follow-view.ts +15 -11
  21. package/extensions/ui/agent-navigation.ts +35 -1
  22. package/extensions/ui/checkpoints-command.ts +28 -0
  23. package/extensions/ui/onboarding-command.ts +218 -0
  24. package/extensions/ui/status-widget.ts +22 -14
  25. package/package.json +1 -1
  26. package/skills/teams.md +16 -0
  27. package/src/orchestration/index.ts +19 -1
  28. package/src/orchestration/status-projection.ts +2 -3
  29. package/src/orchestration/types.ts +9 -1
  30. package/src/results/check-journal.ts +149 -0
  31. package/src/results/check-policy.ts +86 -0
  32. package/src/results/check-runner.ts +110 -0
  33. package/src/results/checkpoint-assignment.ts +60 -0
  34. package/src/results/checkpoint-policy.ts +20 -0
  35. package/src/results/checkpoint-report.ts +109 -0
  36. package/src/results/checkpoint-retention.ts +15 -0
  37. package/src/results/completion-group-delivery.ts +66 -0
  38. package/src/results/completion-group-recovery.ts +61 -0
  39. package/src/results/completion-group-wake.ts +62 -0
  40. package/src/results/completion-group.ts +290 -0
  41. package/src/results/continuation-recipient.ts +37 -0
  42. package/src/results/durable-json.ts +29 -0
  43. package/src/results/repair-policy.ts +24 -0
  44. package/src/results/report-result.ts +77 -0
  45. package/src/results/source-identity.ts +115 -0
  46. package/src/results/specialist-checkpoint.ts +227 -0
  47. package/src/results/verification-controller.ts +308 -0
  48. package/src/utils/messaging.ts +47 -10
  49. package/src/utils/models.ts +13 -0
  50. package/src/utils/paths.ts +4 -0
  51. package/src/utils/report-events.ts +95 -10
  52. package/src/utils/teams.ts +3 -1
  53. package/src/utils/write-queue.ts +3 -0
@@ -0,0 +1,66 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import * as messaging from "../utils/messaging";
3
+ import { resyncStoredTeamReportEvent } from "../utils/report-events";
4
+ import type { TeamReportEvent } from "../utils/models";
5
+ import { effectiveTaskOutcome } from "./report-result";
6
+ import { CompletionGroup, type CompletionGroupState } from "./completion-group";
7
+
8
+ type GroupMember = CompletionGroupState["members"][number];
9
+ const compact = (text: string | undefined, limit = 400) => text && text.length > limit ? `${text.slice(0, limit)}…` : text;
10
+
11
+ function assertReportBinding(group: CompletionGroup, member: GroupMember, event: TeamReportEvent): void {
12
+ if (event.teamName !== group.teamName || event.agentName !== member.name || !event.result
13
+ || event.result.reportId !== event.id || event.result.runId !== member.runId
14
+ || !isDeepStrictEqual(event.completionGroup, { groupId: group.groupId, slotId: member.slotId })) {
15
+ throw new Error("Completion report provenance binding has changed.");
16
+ }
17
+ }
18
+
19
+ async function indexMember(group: CompletionGroup, member: GroupMember) {
20
+ const identity = { name: member.name, slotId: member.slotId, status: member.status, runId: member.runId };
21
+ if (!member.report) return { ...identity, reason: compact(member.reason) };
22
+ const event = await resyncStoredTeamReportEvent(group.teamName, member.report.reportId);
23
+ assertReportBinding(group, member, event);
24
+ const result = event.result!;
25
+ if (result.outcome !== member.report.outcome || effectiveTaskOutcome(result) !== member.report.effectiveOutcome
26
+ || result.verification.state !== member.report.verification || event.status !== member.report.runtimeStatus) {
27
+ throw new Error("Completion report evidence differs from its receipt.");
28
+ }
29
+ return { ...identity, taskId: result.taskId, runtimeStatus: event.status, role: event.role, summary: compact(event.summary),
30
+ outcome: result.outcome, effectiveOutcome: effectiveTaskOutcome(result), verification: result.verification,
31
+ acceptance: result.acceptance, findings: result.findings?.map(finding => ({ ...finding, text: compact(finding.text) })),
32
+ questions: result.questions, report: { id: event.id, path: event.reportPath } };
33
+ }
34
+
35
+ export async function enqueueCompletionGroupDeliveries(group: CompletionGroup): Promise<number> {
36
+ let enqueued = 0;
37
+ for (const delivery of await group.prepareDeliveries()) {
38
+ const state = group.read();
39
+ const members = await Promise.all(delivery.slotIds.map(slotId => {
40
+ const member = state.members.find(candidate => candidate.slotId === slotId);
41
+ if (!member || member.suppressed) throw new Error("Invalid completion index member.");
42
+ return indexMember(group, member);
43
+ }));
44
+ const index = { version: 1, groupId: group.groupId, deliveryId: delivery.id, kind: delivery.kind, members };
45
+ const receipt = await messaging.sendPlainMessageOnce(group.teamName, "system", "team-lead", JSON.stringify(index),
46
+ `Completion group ${delivery.kind}: ${members.length} result(s)`, { id: delivery.id, operationId: delivery.id,
47
+ metadata: { finalReport: true, completionGroup: { groupId: group.groupId, deliveryId: delivery.id, journalPath: group.journalPath } } });
48
+ await group.markEnqueued(delivery.id);
49
+ if (receipt.delivered) enqueued++;
50
+ }
51
+ return enqueued;
52
+ }
53
+
54
+ export async function deliverCompletionGroupReport(event: TeamReportEvent, onDurableReport?: () => void): Promise<boolean> {
55
+ const binding = event.completionGroup;
56
+ if (!binding) return false;
57
+ const stored = await resyncStoredTeamReportEvent(event.teamName, event.id);
58
+ const group = new CompletionGroup(event.teamName, binding.groupId);
59
+ const member = group.read().members.find(candidate => candidate.slotId === binding.slotId);
60
+ if (!member) throw new Error("Unknown completion report slot.");
61
+ assertReportBinding(group, member, stored);
62
+ onDurableReport?.();
63
+ await group.recordReport(binding.slotId, stored.result!, stored.status);
64
+ await enqueueCompletionGroupDeliveries(group);
65
+ return true;
66
+ }
@@ -0,0 +1,61 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import * as paths from "../utils/paths";
4
+ import { peekInbox } from "../utils/messaging";
5
+ import { listStoredTeamReportEvents } from "../utils/report-events";
6
+ import type { Member } from "../utils/models";
7
+ import { CompletionGroup } from "./completion-group";
8
+ import { deliverCompletionGroupReport, enqueueCompletionGroupDeliveries } from "./completion-group-delivery";
9
+
10
+ type Reference = { session?: string; completionGroup?: { groupId?: string; teamName?: string } };
11
+ type HistoryReference = { type?: string; details?: Reference; message?: { role?: string; toolName?: string; details?: Reference } };
12
+ export async function recoverCompletionGroups(
13
+ teamName: string, sessionId: string, members: readonly Pick<Member, "name" | "lifecycleRunId" | "completionGroup">[],
14
+ entries: readonly HistoryReference[], currentGroupIds: ReadonlySet<string>,
15
+ ): Promise<string[]> {
16
+ const root = paths.teamDir(teamName);
17
+ const directory = path.join(root, "completion-groups");
18
+ const reports = fs.existsSync(path.join(root, "reports.json")) ? await listStoredTeamReportEvents(teamName) : [];
19
+ const inbox = await peekInbox(teamName, "team-lead");
20
+ const ids = new Set(fs.existsSync(directory) ? fs.readdirSync(directory)
21
+ .filter(file => /^[a-f0-9]{64}\.json$/.test(file)).map(file => `group:${file.slice(0, -5)}`) : []);
22
+ const references = [...members.map(member => member.completionGroup), ...reports.map(report => report.completionGroup),
23
+ ...inbox.map(message => message.metadata?.completionGroup), ...entries.flatMap(entry => [
24
+ entry.type === "custom_message" && entry.details?.completionGroup?.teamName === teamName ? entry.details.completionGroup : undefined,
25
+ entry.message?.role === "toolResult" && entry.message.toolName === "spawn_swarm_agents" && entry.message.details?.session === teamName
26
+ ? entry.message.details.completionGroup : undefined])];
27
+ for (const reference of references) if (reference?.groupId) ids.add(reference.groupId);
28
+ const errors: string[] = [];
29
+ for (const id of ids) {
30
+ try {
31
+ const group = new CompletionGroup(teamName, id);
32
+ const state = group.read();
33
+ if (state.sessionId !== sessionId || currentGroupIds.has(id)) continue;
34
+ for (const slot of state.members) {
35
+ if (["cancelled", "interrupted", "rejected"].includes(slot.status)) continue;
36
+ const owner = members.find(member => member.name === slot.name
37
+ && member.completionGroup?.groupId === id && member.completionGroup.slotId === slot.slotId);
38
+ if (!slot.runId && owner?.lifecycleRunId) {
39
+ await group.apply({ type: "running", slotId: slot.slotId, runId: owner.lifecycleRunId });
40
+ }
41
+ const current = group.read().members.find(member => member.slotId === slot.slotId)!;
42
+ const report = reports.find(event => event.agentName === current.name && event.result?.runId === current.runId
43
+ && event.completionGroup?.groupId === id && event.completionGroup.slotId === current.slotId);
44
+ if (report) await deliverCompletionGroupReport(report);
45
+ else if (["pending", "queued"].includes(current.status) && !owner) {
46
+ await group.apply({ type: "interrupted", slotId: current.slotId,
47
+ reason: "Assignment was not retained across extension reload; no work was restarted." });
48
+ } else if (current.status === "reported") throw new Error(`Full grouped report is unavailable: ${current.report?.reportId}.`);
49
+ else errors.push(`Completion group ${id}: ${current.name} (${current.runId ?? "unbound"}) remains unresolved after reload; no work was restarted.`);
50
+ }
51
+ await group.seal();
52
+ await enqueueCompletionGroupDeliveries(group);
53
+ for (const delivery of group.read().deliveries) if (delivery.wake?.state === "pending") {
54
+ errors.push(`Completion group wake is unconfirmed (${delivery.id}). Use read_inbox for the saved index; no automatic retry. Journal: ${group.journalPath}`);
55
+ }
56
+ } catch (error) {
57
+ errors.push(error instanceof Error ? error.message : String(error));
58
+ }
59
+ }
60
+ return errors;
61
+ }
@@ -0,0 +1,62 @@
1
+ import { findInboxMessageByOperation, markInboxMessagesRead } from "../utils/messaging";
2
+ import type { InboxMessage } from "../utils/models";
3
+ import { CompletionGroup } from "./completion-group";
4
+
5
+ type WakeBinding = { teamName: string; groupId: string; deliveryId: string };
6
+ export interface CompletionWakeEntry {
7
+ type: string; id: string; customType?: string; content?: unknown;
8
+ details?: { completionGroup?: WakeBinding };
9
+ }
10
+ const wakeContent = (message: InboxMessage) => `Completion group index:\n${message.text}\nUse the referenced full reports as needed, integrate the evidence, and continue the active task. Group settlement is not task success or lead acceptance. Do not stop self-exiting reporting agents.`;
11
+
12
+ async function boundIndex(teamName: string, sessionId: string, binding: Pick<WakeBinding, "groupId" | "deliveryId">) {
13
+ const group = new CompletionGroup(teamName, binding?.groupId);
14
+ const state = group.read();
15
+ const delivery = state.deliveries.find(item => item.id === binding.deliveryId);
16
+ const message = await findInboxMessageByOperation(teamName, "team-lead", binding.deliveryId);
17
+ if (state.sessionId !== sessionId || delivery?.status !== "enqueued" || !message
18
+ || message.id !== delivery.id || message.operationId !== delivery.id
19
+ || message.metadata?.completionGroup?.groupId !== group.groupId
20
+ || message.metadata?.completionGroup?.deliveryId !== delivery.id) {
21
+ throw new Error("Completion wake index binding is unavailable or changed.");
22
+ }
23
+ return { group, delivery, message };
24
+ }
25
+
26
+ export async function requestCompletionGroupWake(
27
+ teamName: string, sessionId: string, message: InboxMessage,
28
+ send: (content: string, details: { completionGroup: WakeBinding }) => void,
29
+ ): Promise<boolean> {
30
+ const binding = message.metadata?.completionGroup as WakeBinding;
31
+ const bound = await boundIndex(teamName, sessionId, binding);
32
+ if (message.id !== bound.message.id || message.text !== bound.message.text) throw new Error("Completion wake message binding has changed.");
33
+ if (!await bound.group.reserveWake(binding.deliveryId)) return false;
34
+ try {
35
+ send(wakeContent(bound.message), { completionGroup: { teamName, groupId: bound.group.groupId, deliveryId: binding.deliveryId } });
36
+ } catch (error) {
37
+ await bound.group.recordWakeEvidence(binding.deliveryId, { error: error instanceof Error ? error.message : String(error) });
38
+ throw error;
39
+ }
40
+ return true;
41
+ }
42
+
43
+ export async function observeCompletionGroupWakes(
44
+ teamName: string, sessionId: string, entries: readonly CompletionWakeEntry[],
45
+ ): Promise<{ observed: string[]; errors: string[] }> {
46
+ const observed: string[] = [];
47
+ const errors: string[] = [];
48
+ for (const entry of entries) {
49
+ const binding = entry.details?.completionGroup;
50
+ if (entry.type !== "custom_message" || entry.customType !== "pi-extended-teams-wake" || binding?.teamName !== teamName) continue;
51
+ try {
52
+ const bound = await boundIndex(teamName, sessionId, binding);
53
+ if (entry.content !== wakeContent(bound.message)) throw new Error("Completion wake history binding has changed.");
54
+ await bound.group.recordWakeEvidence(binding.deliveryId, { entryId: entry.id });
55
+ await markInboxMessagesRead(teamName, "team-lead", [binding.deliveryId]);
56
+ observed.push(entry.id);
57
+ } catch (error) {
58
+ errors.push(error instanceof Error ? error.message : String(error));
59
+ }
60
+ }
61
+ return { observed, errors };
62
+ }
@@ -0,0 +1,290 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { isDeepStrictEqual } from "node:util";
5
+ import { Type, type Static } from "@sinclair/typebox";
6
+ import { Value } from "@sinclair/typebox/value";
7
+ import * as paths from "../utils/paths";
8
+ import { withLock } from "../utils/lock";
9
+ import { createReportResult, effectiveTaskOutcome, ReportedTaskDetailsSchema, type ReportResult } from "./report-result";
10
+ import { syncPathAndParents, writeJsonDurably } from "./durable-json";
11
+
12
+ const reference = Type.String({ minLength: 1 });
13
+ const variants = <T extends string>(...values: T[]) => Type.Union(values.map(value => Type.Literal(value)));
14
+ export const CompletionGroupPolicySchema = Type.Object({
15
+ delivery: variants("immediate", "all-settled"),
16
+ }, { additionalProperties: false });
17
+ export type CompletionGroupPolicy = Static<typeof CompletionGroupPolicySchema>;
18
+ export function normalizeCompletionGroupPolicy(value: unknown): CompletionGroupPolicy | undefined {
19
+ if (value === undefined) return undefined;
20
+ if (!Value.Check(CompletionGroupPolicySchema, value)) throw new Error("Invalid completion group policy.");
21
+ return structuredClone(value);
22
+ }
23
+ const SeedSchema = Type.Object({
24
+ name: reference, suppressed: Type.Optional(Type.Boolean()), assignmentKey: Type.Optional(reference),
25
+ }, { additionalProperties: false });
26
+ const seed = (member: Static<typeof SeedSchema>) => ({
27
+ name: member.name, suppressed: member.suppressed === true,
28
+ ...(member.assignmentKey ? { assignmentKey: member.assignmentKey } : {}),
29
+ });
30
+ const OptionsSchema = Type.Object({
31
+ teamName: reference, sessionId: reference, submissionId: reference,
32
+ policy: CompletionGroupPolicySchema, members: Type.Array(SeedSchema, { minItems: 1 }),
33
+ }, { additionalProperties: false });
34
+ export interface CompletionGroupOptions extends Omit<Static<typeof OptionsSchema>, "policy"> {
35
+ policy?: CompletionGroupPolicy;
36
+ }
37
+ export interface CompletionGroupBinding { groupId: string; slotId: string; }
38
+ const ReportSchema = Type.Object({
39
+ runId: reference, reportId: reference, runtimeStatus: Type.Optional(variants("completed", "failed")),
40
+ outcome: ReportedTaskDetailsSchema.properties.outcome,
41
+ effectiveOutcome: ReportedTaskDetailsSchema.properties.outcome,
42
+ verification: variants("not-requested", "pending", "passed", "failed", "stale"),
43
+ }, { additionalProperties: false });
44
+ const MemberSchema = Type.Object({
45
+ slotId: reference, name: reference, suppressed: Type.Boolean(), assignmentKey: Type.Optional(reference),
46
+ status: variants("pending", "queued", "running", "reported", "rejected", "cancelled", "interrupted"),
47
+ queueId: Type.Optional(reference), runId: Type.Optional(reference),
48
+ report: Type.Optional(ReportSchema), reason: Type.Optional(reference),
49
+ }, { additionalProperties: false });
50
+ const DeliverySchema = Type.Object({
51
+ id: reference, kind: variants("member", "urgent", "settled"),
52
+ slotIds: Type.Array(reference, { minItems: 1, uniqueItems: true }), status: variants("pending", "enqueued"),
53
+ wake: Type.Optional(Type.Object({ state: variants("pending", "observed"), entryId: Type.Optional(reference),
54
+ error: Type.Optional(reference) }, { additionalProperties: false })),
55
+ }, { additionalProperties: false });
56
+ const LedgerSchema = Type.Object({
57
+ version: Type.Literal(1), groupId: reference, teamName: reference, sessionId: reference, submissionId: reference,
58
+ policy: CompletionGroupPolicySchema, sealed: Type.Boolean(),
59
+ members: Type.Array(MemberSchema, { minItems: 1 }), deliveries: Type.Array(DeliverySchema),
60
+ }, { additionalProperties: false });
61
+ export type CompletionGroupState = Static<typeof LedgerSchema>;
62
+ export type CompletionDelivery = Static<typeof DeliverySchema>;
63
+ const eventBinding = { slotId: reference, groupId: Type.Optional(reference) };
64
+ const EventSchema = Type.Union([
65
+ Type.Object({ ...eventBinding, type: Type.Literal("queued"), queueId: reference }, { additionalProperties: false }),
66
+ Type.Object({ ...eventBinding, type: Type.Literal("running"), runId: reference }, { additionalProperties: false }),
67
+ Type.Object({ ...eventBinding, type: Type.Literal("reported"), report: ReportSchema }, { additionalProperties: false }),
68
+ Type.Object({ ...eventBinding, type: variants("rejected", "cancelled", "interrupted"),
69
+ runId: Type.Optional(reference), reason: reference }, { additionalProperties: false }),
70
+ Type.Object({ ...eventBinding, type: variants("rejected", "cancelled"),
71
+ queueId: reference, reason: reference }, { additionalProperties: false }),
72
+ ]);
73
+ export type CompletionGroupEvent = Static<typeof EventSchema>;
74
+ const digest = (value: unknown) => crypto.createHash("sha256").update(JSON.stringify(value)).digest("hex");
75
+ export const completionGroupIdentity = (options: Pick<CompletionGroupOptions, "teamName" | "sessionId" | "submissionId">) =>
76
+ `group:${digest([options.teamName, options.sessionId, options.submissionId])}`;
77
+ const terminal = (member: CompletionGroupState["members"][number]) =>
78
+ !["pending", "queued", "running"].includes(member.status);
79
+ const urgent = (member: CompletionGroupState["members"][number]) => member.status === "rejected" || (!!member.report &&
80
+ (member.report.runtimeStatus === "failed" || ["blocked", "failed"].includes(member.report.effectiveOutcome ?? member.report.outcome ?? "") || ["failed", "stale"].includes(member.report.verification)));
81
+ const deliveryIdentity = (groupId: string, kind: CompletionDelivery["kind"], slots: string[]) =>
82
+ `delivery:${digest([groupId, kind, slots])}`;
83
+
84
+ export class CompletionGroup {
85
+ readonly journalPath: string;
86
+ private newlyCreated = false;
87
+ get created(): boolean { return this.newlyCreated; }
88
+
89
+ constructor(readonly teamName: string, readonly groupId: string) {
90
+ if (!teamName || !/^group:[a-f0-9]{64}$/.test(groupId)) throw new Error("Invalid completion group identity.");
91
+ this.journalPath = path.join(paths.teamDir(teamName), "completion-groups", `${groupId.slice(6)}.json`);
92
+ }
93
+
94
+ static async create(options: CompletionGroupOptions): Promise<CompletionGroup | undefined> {
95
+ if (normalizeCompletionGroupPolicy(options.policy) === undefined) return undefined;
96
+ if (!Value.Check(OptionsSchema, options)) throw new Error("Invalid completion group assignment.");
97
+ const bound = structuredClone(options);
98
+ const group = new CompletionGroup(bound.teamName, completionGroupIdentity(bound));
99
+ const seeds = bound.members.map(seed);
100
+ fs.mkdirSync(path.dirname(group.journalPath), { recursive: true, mode: 0o700 });
101
+ await withLock(group.journalPath, async () => {
102
+ if (fs.existsSync(group.journalPath)) {
103
+ const existing = group.read();
104
+ if (!isDeepStrictEqual(existing.policy, bound.policy)
105
+ || !isDeepStrictEqual(existing.members.map(seed), seeds)) {
106
+ throw new Error("Completion group assignment binding has changed.");
107
+ }
108
+ syncPathAndParents(group.journalPath);
109
+ return;
110
+ }
111
+ writeJsonDurably(group.journalPath, {
112
+ version: 1, groupId: group.groupId, teamName: bound.teamName, sessionId: bound.sessionId,
113
+ submissionId: bound.submissionId, policy: bound.policy, sealed: false, deliveries: [],
114
+ members: seeds.map((member, index) => ({ ...member, slotId: `${group.groupId}:${index}`, status: "pending" })),
115
+ });
116
+ group.newlyCreated = true;
117
+ });
118
+ return group;
119
+ }
120
+
121
+ read(): CompletionGroupState {
122
+ let state: unknown;
123
+ try { state = JSON.parse(fs.readFileSync(this.journalPath, "utf8")); }
124
+ catch (error) {
125
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
126
+ throw new Error(`Referenced completion group is unavailable; inspect ${this.journalPath}.`);
127
+ }
128
+ throw new Error(`Corrupt completion group; inspect ${this.journalPath}.`);
129
+ }
130
+ if (!Value.Check(LedgerSchema, state) || state.groupId !== this.groupId
131
+ || state.teamName !== this.teamName || completionGroupIdentity(state) !== this.groupId) {
132
+ throw new Error("Corrupt completion group binding.");
133
+ }
134
+ for (const [index, member] of state.members.entries()) {
135
+ const reportId = member.runId && createReportResult(state.teamName, member.name, member.runId, {}).reportId;
136
+ if (member.slotId !== `${this.groupId}:${index}`
137
+ || (member.status === "pending" && (member.runId || member.queueId))
138
+ || (member.status === "queued" && (!member.queueId || member.runId))
139
+ || (member.status === "running" && !member.runId)
140
+ || ((member.status === "reported") !== !!member.report)
141
+ || (member.report && (member.report.runId !== member.runId || member.report.reportId !== reportId))) {
142
+ throw new Error("Corrupt completion member binding.");
143
+ }
144
+ }
145
+ if (new Set(state.deliveries.map(delivery => delivery.id)).size !== state.deliveries.length) {
146
+ throw new Error("Corrupt completion delivery identity.");
147
+ }
148
+ for (const delivery of state.deliveries) {
149
+ if ((delivery.wake && (delivery.status !== "enqueued" || ((delivery.wake.state === "observed") !== !!delivery.wake.entryId)))
150
+ || delivery.id !== deliveryIdentity(this.groupId, delivery.kind, delivery.slotIds)
151
+ || (delivery.kind !== "settled" && delivery.slotIds.length !== 1)
152
+ || (delivery.kind === "settled" && (!state.sealed || !state.members.every(terminal)))
153
+ || delivery.slotIds.some(id => !state.members.some(member => member.slotId === id && terminal(member) && !member.suppressed))) {
154
+ throw new Error("Corrupt completion delivery binding.");
155
+ }
156
+ }
157
+ return state;
158
+ }
159
+
160
+ binding(index: number): CompletionGroupBinding {
161
+ const member = this.read().members[index];
162
+ if (!member) throw new Error("Unknown completion group slot.");
163
+ return { groupId: this.groupId, slotId: member.slotId };
164
+ }
165
+
166
+ private plan(state: CompletionGroupState): void {
167
+ const visible = state.members.filter(member => terminal(member) && !member.suppressed);
168
+ const covered = new Set(state.deliveries.flatMap(delivery => delivery.slotIds));
169
+ const due = visible.filter(member => !covered.has(member.slotId));
170
+ const append = (kind: CompletionDelivery["kind"], slotIds: string[]) => {
171
+ state.deliveries.push({ id: deliveryIdentity(this.groupId, kind, slotIds), kind, slotIds, status: "pending" });
172
+ };
173
+ if (due.length === 0) return;
174
+ if (state.policy.delivery === "all-settled" && state.sealed && state.members.every(terminal)) {
175
+ append("settled", visible.map(member => member.slotId));
176
+ return;
177
+ }
178
+ for (const member of due) {
179
+ if (state.policy.delivery === "immediate") append("member", [member.slotId]);
180
+ else if (urgent(member)) append("urgent", [member.slotId]);
181
+ }
182
+ }
183
+
184
+ private async update(change: (state: CompletionGroupState) => void): Promise<CompletionGroupState> {
185
+ this.read();
186
+ return withLock(this.journalPath, async () => {
187
+ const state = this.read();
188
+ const before = JSON.stringify(state);
189
+ change(state);
190
+ this.plan(state);
191
+ if (JSON.stringify(state) !== before) writeJsonDurably(this.journalPath, state);
192
+ else syncPathAndParents(this.journalPath);
193
+ return state;
194
+ });
195
+ }
196
+
197
+ async apply(event: CompletionGroupEvent): Promise<void> {
198
+ if (!Value.Check(EventSchema, event) || (event.groupId && event.groupId !== this.groupId)) {
199
+ throw new Error("Invalid completion event binding.");
200
+ }
201
+ const input = structuredClone(event);
202
+ if (input.type === "reported") {
203
+ if (input.report.outcome === undefined) delete input.report.outcome;
204
+ if (input.report.effectiveOutcome === undefined) delete input.report.effectiveOutcome;
205
+ }
206
+ await this.update(state => {
207
+ const member = state.members.find(item => item.slotId === input.slotId);
208
+ if (!member) throw new Error("Unknown completion slot binding.");
209
+ if (input.type === "queued") {
210
+ if (member.queueId === input.queueId) return;
211
+ if (member.status !== "pending") throw new Error("Completion slot is already admitted or settled.");
212
+ Object.assign(member, { queueId: input.queueId, status: "queued" });
213
+ } else if (input.type === "running") {
214
+ if (member.runId) {
215
+ if (member.runId !== input.runId) throw new Error("Completion run binding has changed.");
216
+ return;
217
+ }
218
+ if (terminal(member)) throw new Error("Completion slot is already settled.");
219
+ Object.assign(member, { runId: input.runId, status: "running" });
220
+ } else if (input.type === "reported") {
221
+ const expected = member.runId && createReportResult(this.teamName, member.name, member.runId, {}).reportId;
222
+ if (input.report.runId !== member.runId || input.report.reportId !== expected) {
223
+ throw new Error("Completion report binding has changed.");
224
+ }
225
+ if (member.report) {
226
+ if (!isDeepStrictEqual(member.report, input.report)) throw new Error("Conflicting completion report binding.");
227
+ return;
228
+ }
229
+ if (member.status !== "running") throw new Error("Completion slot is already settled.");
230
+ Object.assign(member, { report: input.report, status: "reported" });
231
+ } else {
232
+ if ("queueId" in input) {
233
+ // Queue identity is immutable across admission. Resolve its terminal
234
+ // state under this lock, retaining any run bound while removal waited.
235
+ if (member.queueId !== input.queueId) throw new Error("Completion settlement queue binding has changed.");
236
+ } else if (member.runId !== input.runId) throw new Error("Completion settlement run binding has changed.");
237
+ if (terminal(member)) return;
238
+ Object.assign(member, { status: input.type, reason: input.reason });
239
+ }
240
+ });
241
+ }
242
+
243
+ async recordReport(slotId: string, result: ReportResult, runtimeStatus?: "completed" | "failed"): Promise<void> {
244
+ await this.apply({ type: "reported", slotId, report: { reportId: result.reportId, runId: result.runId,
245
+ ...(runtimeStatus ? { runtimeStatus } : {}),
246
+ outcome: result.outcome, effectiveOutcome: effectiveTaskOutcome(result), verification: result.verification.state } });
247
+ }
248
+
249
+ async seal(): Promise<void> { await this.update(state => { state.sealed = true; }); }
250
+
251
+ async prepareDeliveries(): Promise<CompletionDelivery[]> {
252
+ const state = await this.update(() => {});
253
+ return state.deliveries.filter(delivery => delivery.status === "pending");
254
+ }
255
+
256
+ async reserveWake(deliveryId: string): Promise<boolean> {
257
+ let reserved = false;
258
+ await this.update(state => {
259
+ const delivery = state.deliveries.find(item => item.id === deliveryId);
260
+ if (delivery?.status !== "enqueued") throw new Error("Completion wake requires an enqueued index.");
261
+ if (!delivery.wake) {
262
+ delivery.wake = { state: "pending" };
263
+ reserved = true;
264
+ }
265
+ });
266
+ return reserved;
267
+ }
268
+
269
+ async recordWakeEvidence(deliveryId: string, evidence: { entryId: string } | { error: string }): Promise<void> {
270
+ await this.update(state => {
271
+ const wake = state.deliveries.find(item => item.id === deliveryId)?.wake;
272
+ if (!wake) throw new Error("Completion wake has no reserved request.");
273
+ if ("entryId" in evidence) {
274
+ if (!evidence.entryId || (wake.entryId && wake.entryId !== evidence.entryId)) throw new Error("Completion wake observation binding has changed.");
275
+ Object.assign(wake, { state: "observed", entryId: evidence.entryId });
276
+ } else {
277
+ if (!evidence.error) throw new Error("Completion wake error is unavailable.");
278
+ wake.error = evidence.error;
279
+ }
280
+ });
281
+ }
282
+
283
+ async markEnqueued(deliveryId: string): Promise<void> {
284
+ await this.update(state => {
285
+ const delivery = state.deliveries.find(item => item.id === deliveryId);
286
+ if (!delivery) throw new Error("Unknown completion delivery identity.");
287
+ delivery.status = "enqueued";
288
+ });
289
+ }
290
+ }
@@ -0,0 +1,37 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import * as paths from "../utils/paths";
5
+
6
+ export function freshContinuationName(prefix: string, nonce?: string): string {
7
+ paths.sanitizeName(prefix);
8
+ if (!prefix || prefix.length > 128) throw new Error("Continuation name prefix must contain 1–128 characters.");
9
+ const suffix = nonce === undefined ? crypto.randomUUID() : crypto.createHash("sha256").update(nonce).digest("hex").slice(0, 32);
10
+ return `${prefix}-${suffix}`;
11
+ }
12
+
13
+ export function assertUnusedContinuationRecipient(teamName: string, candidate: string): void {
14
+ paths.sanitizeName(candidate);
15
+ const directory = paths.teamDir(teamName);
16
+ const collision = () => { throw new Error(`Continuation recipient ${candidate} is not unused; choose a new prefix and retry.`); };
17
+ const exists = (file: string) => !!fs.lstatSync(file, { throwIfNoEntry: false });
18
+ for (const file of [paths.inboxPath(teamName, candidate), paths.runtimeStatusPath(teamName, candidate), paths.lifecycleTombstonePath(teamName, candidate),
19
+ path.join(directory, `${candidate}.pid`), path.join(directory, "agent-sessions", candidate)]) if (exists(file)) collision();
20
+ for (const [folder, matches] of [
21
+ [path.join(directory, "session-context"), (name: string) => name.startsWith(`${candidate.toLowerCase()}--`)],
22
+ [path.join(paths.reportFilesDir(), paths.sanitizeName(teamName)), (name: string) => name === `${candidate.toLowerCase()}.md` || name.startsWith(`${candidate.toLowerCase()}-v`)],
23
+ ] as const) if (exists(folder) && fs.readdirSync(folder).some(name => matches(name.toLowerCase()))) collision();
24
+ for (const [file, key, array] of [[paths.claimsPath(teamName), "agent", false], [paths.writeQueuePath(teamName), "name", true],
25
+ [paths.readHelperQueuePath(teamName), "name", true], [path.join(directory, "reports.json"), "agentName", true]] as const) {
26
+ if (!exists(file)) continue;
27
+ let rows: unknown[];
28
+ try {
29
+ if (!fs.lstatSync(file).isFile() || fs.lstatSync(file).isSymbolicLink()) throw new Error("Not a regular namespace record");
30
+ const value = JSON.parse(fs.readFileSync(file, "utf8"));
31
+ if (!value || typeof value !== "object" || Array.isArray(value) !== array) throw new Error("Invalid namespace record");
32
+ rows = array ? value : Object.values(value);
33
+ if (rows.some(row => !row || typeof row !== "object" || typeof Reflect.get(row, key) !== "string")) throw new Error("Invalid namespace owner");
34
+ } catch { throw new Error(`Cannot establish an unused continuation namespace; inspect ${file}.`); }
35
+ if (rows.some(row => String(Reflect.get(row as object, key)).toLowerCase() === candidate.toLowerCase())) collision();
36
+ }
37
+ }
@@ -0,0 +1,29 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ function syncPath(file: string): void {
6
+ const descriptor = fs.openSync(file, "r");
7
+ try { fs.fsyncSync(descriptor); }
8
+ finally { fs.closeSync(descriptor); }
9
+ }
10
+
11
+ export function syncPathAndParents(file: string): void {
12
+ for (let current = file; ; current = path.dirname(current)) {
13
+ syncPath(current);
14
+ if (path.dirname(current) === current) return;
15
+ }
16
+ }
17
+
18
+ export function writeJsonDurably(file: string, value: unknown): void {
19
+ const temporary = path.join(path.dirname(file), `.${path.basename(file)}.${crypto.randomUUID()}.tmp`);
20
+ try {
21
+ fs.writeFileSync(temporary, JSON.stringify(value, null, 2), { flag: "wx", mode: 0o600 });
22
+ syncPath(temporary);
23
+ fs.renameSync(temporary, file);
24
+ syncPathAndParents(path.dirname(file));
25
+ } catch (error) {
26
+ try { fs.unlinkSync(temporary); } catch {}
27
+ throw error;
28
+ }
29
+ }
@@ -0,0 +1,24 @@
1
+ import { Type, type Static } from "@sinclair/typebox";
2
+ import { Value } from "@sinclair/typebox/value";
3
+
4
+ export const RepairPolicySchema = Type.Object({
5
+ maxAttempts: Type.Integer({ minimum: 0, maximum: 5, description: "Maximum additional repair attempts after initial verification. Zero disables automatic repair. Only the lead or a trusted integration may authorize this policy." }),
6
+ }, { additionalProperties: false });
7
+ export type RepairPolicy = Static<typeof RepairPolicySchema>;
8
+ export type RepairState = "pending" | "not-needed" | "requested" | "repaired" | "exhausted" | "cancelled" | "declined" | "blocked";
9
+ export interface RepairResult {
10
+ controllerId: string;
11
+ journalPath?: string;
12
+ state: RepairState;
13
+ attemptsUsed?: number;
14
+ maxAttempts?: number;
15
+ requestIds?: string[];
16
+ outcome?: "blocked";
17
+ error?: string;
18
+ }
19
+
20
+ export function normalizeRepairPolicy(value: unknown): RepairPolicy | undefined {
21
+ if (value === undefined) return undefined;
22
+ if (!Value.Check(RepairPolicySchema, value)) throw new Error("Invalid repair policy.");
23
+ return value.maxAttempts ? structuredClone(value) : undefined;
24
+ }
@@ -0,0 +1,77 @@
1
+ import { Type, type Static } from "@sinclair/typebox";
2
+ import { Value } from "@sinclair/typebox/value";
3
+ import type { RepairResult } from "./repair-policy";
4
+
5
+ const reference = Type.String({ minLength: 1 });
6
+
7
+ export const ReportedTaskDetailsSchema = Type.Object({
8
+ outcome: Type.Optional(Type.Union([
9
+ Type.Literal("succeeded"), Type.Literal("blocked"), Type.Literal("failed"), Type.Literal("cancelled"),
10
+ ], { description: "Reported task outcome, separate from clean exit, verification, and lead acceptance. Omit when unspecified." })),
11
+ changedPaths: Type.Optional(Type.Array(reference)),
12
+ inspectedEvidence: Type.Optional(Type.Array(Type.String({ minLength: 1, maxLength: 2048, pattern: "\\S" }), { maxItems: 128 })),
13
+ artifacts: Type.Optional(Type.Array(Type.Object({
14
+ path: reference,
15
+ label: Type.Optional(Type.String()),
16
+ }, { additionalProperties: false }))),
17
+ findings: Type.Optional(Type.Array(Type.Object({
18
+ id: reference,
19
+ text: reference,
20
+ evidence: Type.Array(reference),
21
+ }, { additionalProperties: false }))),
22
+ questions: Type.Optional(Type.Array(reference)),
23
+ }, { additionalProperties: false });
24
+
25
+ export type ReportedTaskDetails = Static<typeof ReportedTaskDetailsSchema>;
26
+ export type TaskOutcome = NonNullable<ReportedTaskDetails["outcome"]>;
27
+ export type VerificationState = "not-requested" | "pending" | "passed" | "failed" | "stale";
28
+
29
+ export interface ReportResult extends ReportedTaskDetails {
30
+ version: 1;
31
+ taskId: string;
32
+ runId: string;
33
+ reportId: string;
34
+ checkpointId?: string;
35
+ verification: { state: VerificationState; checkIds?: string[]; error?: string };
36
+ acceptance: { state: "pending" | "accepted" | "rejected"; decidedAt?: number; reason?: string };
37
+ repair?: RepairResult;
38
+ }
39
+
40
+ export function effectiveTaskOutcome(result: ReportResult): TaskOutcome | undefined {
41
+ return result.repair?.outcome ?? result.outcome;
42
+ }
43
+
44
+ export function normalizeReportedTaskDetails(value: unknown): ReportedTaskDetails {
45
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
46
+ throw new Error("Reported task details must be an object.");
47
+ }
48
+ const input = value as Record<string, unknown>;
49
+ const details = Object.fromEntries(Object.keys(ReportedTaskDetailsSchema.properties)
50
+ .filter(key => input[key] !== undefined)
51
+ .map(key => [key, input[key]]));
52
+ if (!Value.Check(ReportedTaskDetailsSchema, details)) throw new Error("Invalid reported task details.");
53
+ const findingIds = details.findings?.map(finding => finding.id) ?? [];
54
+ if (new Set(findingIds).size !== findingIds.length) throw new Error("Reported finding IDs must be unique.");
55
+ return structuredClone(details);
56
+ }
57
+
58
+ export function createReportResult(
59
+ teamName: string,
60
+ agentName: string,
61
+ runId: string,
62
+ reported: ReportedTaskDetails,
63
+ ): ReportResult {
64
+ if (![teamName, agentName, runId].every(value => typeof value === "string" && value.length > 0)) {
65
+ throw new Error("Report identity requires the bound team, agent and lifecycle run.");
66
+ }
67
+ const identity = [teamName, agentName, runId].map(encodeURIComponent).join(":");
68
+ return {
69
+ ...normalizeReportedTaskDetails(reported),
70
+ version: 1,
71
+ taskId: `task:${identity}`,
72
+ runId,
73
+ reportId: `report:${identity}`,
74
+ verification: { state: "not-requested" },
75
+ acceptance: { state: "pending" },
76
+ };
77
+ }