@howaboua/pi-agent-board 0.0.0-stage → 0.0.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.
@@ -0,0 +1,227 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import {
3
+ buildSessionContext,
4
+ type ExtensionAPI,
5
+ type ExtensionContext,
6
+ } from "@earendil-works/pi-coding-agent";
7
+ import type {
8
+ CODEX_CONTEXT_BRIEFING_TYPE,
9
+ CodexContextBriefing,
10
+ CodexContextBriefingHandler,
11
+ } from "@howaboua/pi-codex-conversion/developer-messages";
12
+ import { registerInferenceBriefing } from "./delivery.js";
13
+
14
+ // Conversion projects this durable protocol in inference, compaction and native replay.
15
+ const BRIEFING: typeof CODEX_CONTEXT_BRIEFING_TYPE = "codex-context-briefing";
16
+
17
+ export function registerContextBriefing(
18
+ pi: ExtensionAPI,
19
+ name: string,
20
+ relevant: (ctx: ExtensionContext) => boolean,
21
+ record: CodexContextBriefingHandler,
22
+ ) {
23
+ const hosted = registerInferenceBriefing(pi, record);
24
+ const warned = new Set<string>();
25
+ pi.on("context_with_system", async (event, ctx) => {
26
+ await hosted.ready;
27
+ if (hosted.hosted()) return;
28
+ if (
29
+ hosted.outdated &&
30
+ relevant(ctx) &&
31
+ !warned.has(ctx.sessionManager.getSessionId())
32
+ ) {
33
+ warned.add(ctx.sessionManager.getSessionId());
34
+ ctx.ui.notify(
35
+ `Update Codex Conversion and reload to preserve ${name} briefings through its context rollover and native replay. Native Pi briefings and other agent tools remain available.`,
36
+ "warning",
37
+ );
38
+ }
39
+ try {
40
+ await record(ctx, undefined, event.messages);
41
+ return { messages: projectNative(ctx, event.messages) };
42
+ } catch (error) {
43
+ ctx.abort();
44
+ throw error;
45
+ }
46
+ });
47
+ }
48
+
49
+ export function contextBriefingWindow(
50
+ ctx: ExtensionContext,
51
+ windowId?: string,
52
+ ) {
53
+ const branch = ctx.sessionManager.getBranch();
54
+ const compaction = branch.findLastIndex(
55
+ (entry) =>
56
+ entry.type === "compaction" &&
57
+ !(
58
+ entry.details &&
59
+ typeof entry.details === "object" &&
60
+ "strategy" in entry.details &&
61
+ entry.details.strategy === "codex-context-window"
62
+ ),
63
+ );
64
+ const boundary = branch.findLastIndex(
65
+ (entry) =>
66
+ entry.type === "custom_message" &&
67
+ entry.customType === "codex-context-window" &&
68
+ entry.details &&
69
+ typeof entry.details === "object" &&
70
+ "contextManagement" in entry.details &&
71
+ entry.details.contextManagement &&
72
+ typeof entry.details.contextManagement === "object" &&
73
+ "kind" in entry.details.contextManagement &&
74
+ entry.details.contextManagement.kind === "window",
75
+ );
76
+ return compaction > boundary
77
+ ? branch[compaction]!.id
78
+ : (windowId ?? ctx.sessionManager.getSessionId());
79
+ }
80
+
81
+ export function hasContextRollover(ctx: ExtensionContext): boolean {
82
+ return ctx.sessionManager.getBranch().some((entry) => {
83
+ if (entry.type === "compaction") return true;
84
+ if (
85
+ entry.type !== "custom_message" ||
86
+ entry.customType !== "codex-context-window" ||
87
+ !entry.details ||
88
+ typeof entry.details !== "object" ||
89
+ !("contextManagement" in entry.details)
90
+ )
91
+ return false;
92
+ const window = entry.details.contextManagement;
93
+ return Boolean(
94
+ window &&
95
+ typeof window === "object" &&
96
+ "kind" in window &&
97
+ window.kind === "window" &&
98
+ "windowNumber" in window &&
99
+ typeof window.windowNumber === "number" &&
100
+ window.windowNumber > 0,
101
+ );
102
+ });
103
+ }
104
+
105
+ export async function recordContextBriefing(
106
+ pi: ExtensionAPI,
107
+ ctx: ExtensionContext,
108
+ owner: string,
109
+ key: string,
110
+ content: () => Promise<string | undefined>,
111
+ ) {
112
+ const saved = ctx.sessionManager
113
+ .getEntries()
114
+ .findLast(
115
+ (entry) =>
116
+ entry.type === "custom" &&
117
+ entry.customType === BRIEFING &&
118
+ readBriefing(entry.data).owner === owner &&
119
+ readBriefing(entry.data).key === key,
120
+ );
121
+ if (saved?.type === "custom") {
122
+ // Tree may select an ancestor before its metadata child. Reuse immutable bytes/ID.
123
+ if (
124
+ !ctx.sessionManager
125
+ .getBranch()
126
+ .some(
127
+ (entry) =>
128
+ entry.type === "custom" &&
129
+ entry.customType === BRIEFING &&
130
+ readBriefing(entry.data).owner === owner &&
131
+ readBriefing(entry.data).key === key,
132
+ )
133
+ )
134
+ pi.appendEntry(BRIEFING, readBriefing(saved.data));
135
+ return;
136
+ }
137
+ const value = await content();
138
+ if (value === undefined) return;
139
+ pi.appendEntry(BRIEFING, {
140
+ protocol: 1,
141
+ id: randomUUID(),
142
+ owner,
143
+ key,
144
+ content: value,
145
+ } satisfies CodexContextBriefing);
146
+ }
147
+
148
+ function projectNative(
149
+ ctx: ExtensionContext,
150
+ messages: NativeMessage[],
151
+ ): NativeMessage[] {
152
+ const entries = ctx.sessionManager.getBranch().map((entry) => {
153
+ if (entry.type !== "custom" || entry.customType !== BRIEFING) return entry;
154
+ const briefing = readBriefing(entry.data);
155
+ return {
156
+ ...entry,
157
+ type: "custom_message" as const,
158
+ content: briefing.content,
159
+ display: false,
160
+ details: briefing,
161
+ };
162
+ });
163
+ const reconstructed = buildSessionContext(entries).messages;
164
+ const positions = new Map<string, number[]>();
165
+ messages.forEach((message, index) => {
166
+ const key = messageKey(message);
167
+ const indices = positions.get(key) ?? [];
168
+ indices.push(index);
169
+ positions.set(key, indices);
170
+ });
171
+ const insertions = new Map<number, NativeMessage[]>();
172
+ let pending: NativeMessage[] = [];
173
+ let last = messages[0]?.role === "system" ? 0 : -1;
174
+ const insert = (index: number) => {
175
+ if (!pending.length) return;
176
+ const position =
177
+ messages[0]?.role === "system" ? Math.max(1, index) : index;
178
+ insertions.set(position, [...(insertions.get(position) ?? []), ...pending]);
179
+ pending = [];
180
+ };
181
+ for (const message of reconstructed) {
182
+ const index = positions.get(messageKey(message))?.shift();
183
+ if (index !== undefined) {
184
+ insert(index);
185
+ last = index;
186
+ } else if (message.role === "custom" && message.customType === BRIEFING) {
187
+ pending.push(message);
188
+ }
189
+ }
190
+ insert(last + 1);
191
+ return messages
192
+ .flatMap((message, index) => [...(insertions.get(index) ?? []), message])
193
+ .concat(insertions.get(messages.length) ?? []);
194
+ }
195
+
196
+ type NativeMessage = ReturnType<typeof buildSessionContext>["messages"][number];
197
+ function messageKey(message: NativeMessage) {
198
+ return JSON.stringify([
199
+ message.role,
200
+ message.timestamp,
201
+ message.role === "custom"
202
+ ? message.customType === BRIEFING
203
+ ? readBriefing(message.details).id
204
+ : message.customType
205
+ : message.role === "toolResult"
206
+ ? message.toolCallId
207
+ : undefined,
208
+ ]);
209
+ }
210
+ function readBriefing(value: unknown): CodexContextBriefing {
211
+ if (
212
+ !value ||
213
+ typeof value !== "object" ||
214
+ !("protocol" in value) ||
215
+ value.protocol !== 1 ||
216
+ !("id" in value) ||
217
+ typeof value.id !== "string" ||
218
+ !("owner" in value) ||
219
+ typeof value.owner !== "string" ||
220
+ !("key" in value) ||
221
+ typeof value.key !== "string" ||
222
+ !("content" in value) ||
223
+ typeof value.content !== "string"
224
+ )
225
+ throw new Error("Invalid saved context briefing");
226
+ return value as CodexContextBriefing;
227
+ }
@@ -0,0 +1,189 @@
1
+ import { StringEnum, Type } from "@earendil-works/pi-ai";
2
+ import type { Static } from "typebox";
3
+ import { Check } from "typebox/value";
4
+
5
+ const BOARD_ACTIONS = [
6
+ "help",
7
+ "list_boards",
8
+ "create_channel",
9
+ "get_channels",
10
+ "post",
11
+ "list_threads",
12
+ "read_thread",
13
+ "read_post",
14
+ "search_posts",
15
+ "subscribe",
16
+ "unsubscribe",
17
+ ] as const;
18
+ export const BoardParameters = Type.Object({
19
+ action: Type.Optional(StringEnum(BOARD_ACTIONS)),
20
+ });
21
+ const positive = Type.Optional(
22
+ Type.Integer({ minimum: 1, maximum: 0xffffffff }),
23
+ );
24
+ const string = Type.Optional(Type.String());
25
+ const Request = Type.Object(
26
+ {
27
+ action: StringEnum(BOARD_ACTIONS),
28
+ channel_name: string,
29
+ new_channel_name: string,
30
+ thread_id: string,
31
+ message_id: string,
32
+ text: string,
33
+ query: string,
34
+ author: string,
35
+ target_agent: string,
36
+ after_message_id: string,
37
+ board_id: string,
38
+ cursor: string,
39
+ subscribe: Type.Optional(Type.Boolean()),
40
+ recent_first: Type.Optional(Type.Boolean()),
41
+ sort: Type.Optional(StringEnum(["created", "activity"])),
42
+ limit: positive,
43
+ max_chars_per_post: positive,
44
+ limit_chars: positive,
45
+ offset_chars: Type.Optional(
46
+ Type.Integer({ minimum: 0, maximum: 0xffffffff }),
47
+ ),
48
+ agents_to_notify: Type.Optional(
49
+ Type.Array(Type.String(), { maxItems: 256 }),
50
+ ),
51
+ },
52
+ { additionalProperties: false },
53
+ );
54
+ export type BoardParams = Static<typeof Request>;
55
+ export type BoardAction = BoardParams["action"];
56
+ export const MUTATIONS = new Set<BoardAction>([
57
+ "create_channel",
58
+ "post",
59
+ "subscribe",
60
+ "unsubscribe",
61
+ ]);
62
+ const paging = ["limit", "cursor"];
63
+ const reads = ["board_id"];
64
+ const fields: Record<BoardAction, string[]> = {
65
+ help: [],
66
+ list_boards: paging,
67
+ create_channel: ["channel_name", "subscribe"],
68
+ get_channels: ["query", "recent_first", ...paging, ...reads],
69
+ post: [
70
+ "text",
71
+ "channel_name",
72
+ "new_channel_name",
73
+ "thread_id",
74
+ "agents_to_notify",
75
+ ],
76
+ list_threads: [
77
+ "channel_name",
78
+ "sort",
79
+ "recent_first",
80
+ "max_chars_per_post",
81
+ ...paging,
82
+ ...reads,
83
+ ],
84
+ read_thread: ["thread_id", "max_chars_per_post", ...paging, ...reads],
85
+ read_post: ["message_id", "offset_chars", "limit_chars", ...reads],
86
+ search_posts: [
87
+ "channel_name",
88
+ "query",
89
+ "author",
90
+ "after_message_id",
91
+ "max_chars_per_post",
92
+ ...paging,
93
+ ...reads,
94
+ ],
95
+ subscribe: ["channel_name", "thread_id", "target_agent"],
96
+ unsubscribe: ["channel_name", "thread_id", "target_agent"],
97
+ };
98
+ const requiredFields: Partial<Record<BoardAction, string[]>> = {
99
+ create_channel: ["channel_name"],
100
+ post: ["text"],
101
+ list_threads: ["channel_name"],
102
+ read_thread: ["thread_id"],
103
+ read_post: ["message_id"],
104
+ };
105
+ export function parseBoardRequest(value: unknown): BoardParams {
106
+ if (
107
+ value === undefined ||
108
+ (typeof value === "object" &&
109
+ value !== null &&
110
+ !Array.isArray(value) &&
111
+ Object.keys(value).length === 0)
112
+ )
113
+ return { action: "help" };
114
+ if (!Check(Request, value))
115
+ throw new Error("Invalid board request; call board help");
116
+ const request = { ...(value as BoardParams) };
117
+ const allowed = new Set(["action", ...fields[request.action]]);
118
+ if (Object.keys(request).some((field) => !allowed.has(field)))
119
+ throw new Error(`Unknown ${request.action} field; board_id is read-only`);
120
+ for (const field of requiredFields[request.action] ?? []) {
121
+ if (!(field in request)) throw new Error(`${field} is required`);
122
+ }
123
+ for (const field of [
124
+ "thread_id",
125
+ "message_id",
126
+ "after_message_id",
127
+ "board_id",
128
+ ] as const) {
129
+ const id = request[field];
130
+ if (
131
+ id !== undefined &&
132
+ !/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(
133
+ id,
134
+ )
135
+ )
136
+ throw new Error(`${field} must be a UUID`);
137
+ if (id !== undefined) request[field] = id.toLowerCase();
138
+ }
139
+ if (request.action === "post")
140
+ exactlyOne(request, ["channel_name", "new_channel_name", "thread_id"]);
141
+ if (request.action === "subscribe" || request.action === "unsubscribe")
142
+ exactlyOne(request, ["channel_name", "thread_id"]);
143
+ return request;
144
+ }
145
+ function exactlyOne(value: BoardParams, keys: (keyof BoardParams)[]) {
146
+ if (keys.filter((key) => value[key] !== undefined).length !== 1)
147
+ throw new Error(`Provide exactly one of ${keys.join(", ")}`);
148
+ }
149
+ export function required(value: string | undefined, field: string): string {
150
+ if (value === undefined) throw new Error(`${field} is required`);
151
+ return value;
152
+ }
153
+ export function agentPath(value: string, caller: string): string {
154
+ const path = value.startsWith("/") ? value : `${caller}/${value}`;
155
+ if (!/^\/root(?:\/[a-zA-Z0-9_-]+)*$/.test(path))
156
+ throw new Error("Invalid agent path");
157
+ return path;
158
+ }
159
+ export const boardHelp = {
160
+ actions: Object.fromEntries(
161
+ Object.entries(fields).map(([action, values]) => [
162
+ action,
163
+ values
164
+ .map(
165
+ (field) =>
166
+ `${field}${requiredFields[action as BoardAction]?.includes(field) ? "" : "?"}`,
167
+ )
168
+ .join(" "),
169
+ ]),
170
+ ),
171
+ rules: {
172
+ scope:
173
+ "Defaults to your tree; board_id browses history, never writes. list_boards discovers saved boards",
174
+ post: "text required; exactly one of channel_name, new_channel_name, thread_id. Thread ID is the first post ID. New channel subscribes author to first posts",
175
+ subscriptions:
176
+ "Exactly one of channel_name/thread_id; channel covers first posts, thread covers replies. target_agent defaults to you. Posting subscribes author to thread unless explicitly unsubscribed",
177
+ notify:
178
+ "agents_to_notify is one-time, excludes author; only running turns, no waking or offline notices",
179
+ agents:
180
+ "Use spawn's boardAgent; absolute /root/... or child path relative to you, not a pane or contextAgent",
181
+ paging:
182
+ "limit default20, cap50; cursor opaque, keep query/sort unchanged. Concurrent posts may shift pages",
183
+ order:
184
+ "Newest first; get_channels by activity, list_threads sort=created|activity. read_thread pages newest replies with root on every page",
185
+ text: "Case-insensitive substring search; max_chars_per_post default1000. read_post offsets count Unicode characters, limit_chars default20000. Continue at next_offset_chars<n_chars",
186
+ budget:
187
+ "Post cap64KiB, channel1-128 UTF-8 bytes without edge whitespace/control characters. Results cap8000 serialized UTF-8 bytes; reads may reduce limits, continue with returned cursor/offset",
188
+ },
189
+ };
@@ -0,0 +1,76 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import type {
3
+ CodexContextBriefingHandler,
4
+ CodexDeveloperCustomMessage,
5
+ CodexDeveloperMessageOptions,
6
+ } from "@howaboua/pi-codex-conversion/developer-messages";
7
+
8
+ type Delivery =
9
+ typeof import("@howaboua/pi-codex-conversion/developer-messages");
10
+ let optionalDelivery: Promise<Delivery | undefined> | undefined;
11
+
12
+ function loadDelivery() {
13
+ return (optionalDelivery ??= import(
14
+ "@howaboua/pi-codex-conversion/developer-messages"
15
+ ).catch((error) => {
16
+ if (isMissingOptionalModule(error, "developer-messages")) return undefined;
17
+ throw error;
18
+ }));
19
+ }
20
+
21
+ export function isMissingOptionalModule(error: unknown, subpath: string) {
22
+ if (!(error instanceof Error) || !("code" in error)) return false;
23
+ const name = "@howaboua/pi-codex-conversion";
24
+ if (error.code === "ERR_PACKAGE_PATH_NOT_EXPORTED")
25
+ return (
26
+ error.message.includes(name) && error.message.includes(`./${subpath}`)
27
+ );
28
+ if (
29
+ error.code !== "ERR_MODULE_NOT_FOUND" &&
30
+ error.code !== "MODULE_NOT_FOUND"
31
+ )
32
+ return false;
33
+ const missing = error.message.match(
34
+ /Cannot find (?:package|module) ['"]([^'"]+)['"]/,
35
+ )?.[1];
36
+ return missing === name || missing === `${name}/${subpath}`;
37
+ }
38
+
39
+ let delivery: Delivery | undefined;
40
+ export async function initializeDelivery() {
41
+ delivery = await loadDelivery();
42
+ }
43
+
44
+ export function registerInferenceBriefing(
45
+ pi: ExtensionAPI,
46
+ handler: CodexContextBriefingHandler,
47
+ ) {
48
+ let unregister: (() => void) | undefined;
49
+ const ready = loadDelivery().then((api) => {
50
+ delivery = api;
51
+ unregister = api?.registerCodexContextBriefing?.(pi, handler);
52
+ });
53
+ pi.on("session_shutdown", () => unregister?.());
54
+ return {
55
+ ready,
56
+ hosted: () => delivery?.hasCodexContextBriefingHost?.(pi) ?? false,
57
+ get outdated() {
58
+ return Boolean(delivery && !delivery.registerCodexContextBriefing);
59
+ },
60
+ };
61
+ }
62
+
63
+ export function sendPolicyMessage(
64
+ pi: ExtensionAPI,
65
+ message: CodexDeveloperCustomMessage,
66
+ options: CodexDeveloperMessageOptions,
67
+ ) {
68
+ if (delivery?.trySendCodexDeveloperCustomMessage?.(pi, message, options))
69
+ return;
70
+ pi.sendMessage(
71
+ message,
72
+ options.deliverAs === "nextTurn"
73
+ ? { ...options, deliverAs: "steer" }
74
+ : options,
75
+ );
76
+ }
@@ -0,0 +1,174 @@
1
+ import { resolve } from "node:path";
2
+ import { Type } from "@earendil-works/pi-ai";
3
+ import type {
4
+ ExtensionAPI,
5
+ ExtensionContext,
6
+ } from "@earendil-works/pi-coding-agent";
7
+ import type { Static } from "typebox";
8
+ import { Check } from "typebox/value";
9
+ import { readBoardConfig } from "./config.js";
10
+
11
+ const uuid = Type.String({ pattern: "^[a-f0-9-]{36}$" });
12
+ export const BindingSchema = Type.Object(
13
+ {
14
+ protocol: Type.Literal(1),
15
+ boardId: uuid,
16
+ rootSessionId: uuid,
17
+ sessionId: uuid,
18
+ agentName: Type.String({ pattern: "^/root(?:/[a-zA-Z0-9_-]+)*$" }),
19
+ ownerFolder: Type.String({ minLength: 1 }),
20
+ databasePath: Type.String({ minLength: 1 }),
21
+ enabled: Type.Boolean(),
22
+ upstream: Type.Optional(Type.String({ minLength: 1 })),
23
+ },
24
+ { additionalProperties: false },
25
+ );
26
+ export type BoardBinding = Static<typeof BindingSchema>;
27
+ const ChildSchema = Type.Object({
28
+ parentSessionId: uuid,
29
+ binding: BindingSchema,
30
+ machine: Type.String(),
31
+ sessionFile: Type.String({ minLength: 1 }),
32
+ });
33
+ export type BoardChild = Static<typeof ChildSchema>;
34
+ const BINDING = "shepherdr-board-binding";
35
+ const MEMBER = "shepherdr-board-member";
36
+ const CHILD = "shepherdr-board-child";
37
+ const SETTING = "shepherdr-board-setting";
38
+ const defaults = new WeakMap<ExtensionContext["sessionManager"], boolean>();
39
+ export function setBoardDefault(ctx: ExtensionContext, enabled: boolean) {
40
+ defaults.set(ctx.sessionManager, enabled);
41
+ }
42
+ const REMOVED = "shepherdr-board-removed";
43
+ const SettingSchema = Type.Object({
44
+ sessionId: uuid,
45
+ enabled: Type.Union([Type.Boolean(), Type.Null()]),
46
+ });
47
+
48
+ function sessionBoardSetting(ctx: ExtensionContext): boolean | undefined {
49
+ let enabled: boolean | undefined;
50
+ for (const entry of ctx.sessionManager.getEntries()) {
51
+ if (entry.type !== "custom" || entry.customType !== SETTING) continue;
52
+ if (!Check(SettingSchema, entry.data))
53
+ throw new Error("Invalid saved board setting");
54
+ const value = entry.data as Static<typeof SettingSchema>;
55
+ if (value.sessionId === ctx.sessionManager.getSessionId())
56
+ enabled = value.enabled ?? undefined;
57
+ }
58
+ return enabled;
59
+ }
60
+
61
+ export function saveBoardSetting(
62
+ pi: ExtensionAPI,
63
+ ctx: ExtensionContext,
64
+ enabled: boolean | undefined,
65
+ ) {
66
+ pi.appendEntry(SETTING, {
67
+ sessionId: ctx.sessionManager.getSessionId(),
68
+ enabled: enabled ?? null,
69
+ });
70
+ }
71
+
72
+ export function rootBoardSetting(ctx: ExtensionContext) {
73
+ try {
74
+ const config = readBoardConfig(
75
+ resolve(ctx.sessionManager.getCwd()),
76
+ defaults.get(ctx.sessionManager),
77
+ );
78
+ const session = sessionBoardSetting(ctx);
79
+ return {
80
+ enabled: session ?? config.folder ?? config.global,
81
+ source:
82
+ session !== undefined
83
+ ? "session"
84
+ : config.folder !== undefined
85
+ ? "folder"
86
+ : "global default",
87
+ };
88
+ } catch (error) {
89
+ return {
90
+ enabled: false,
91
+ source: "invalid configuration",
92
+ error: String(error),
93
+ };
94
+ }
95
+ }
96
+
97
+ export function parseBinding(value: unknown): BoardBinding {
98
+ if (!Check(BindingSchema, value))
99
+ throw new Error("Invalid Shepherdr board binding");
100
+ return value as BoardBinding;
101
+ }
102
+ export function binding(ctx: ExtensionContext): BoardBinding {
103
+ const sessionId = ctx.sessionManager.getSessionId();
104
+ let saved: BoardBinding | undefined;
105
+ for (const entry of ctx.sessionManager.getEntries()) {
106
+ if (entry.type !== "custom" || entry.customType !== BINDING) continue;
107
+ const candidate = parseBinding(entry.data);
108
+ if (candidate.sessionId === sessionId) saved = candidate;
109
+ }
110
+ const ownerFolder = resolve(ctx.sessionManager.getCwd());
111
+ if (saved?.upstream) return saved;
112
+ return {
113
+ ...(saved ?? {
114
+ protocol: 1,
115
+ sessionId,
116
+ rootSessionId: sessionId,
117
+ boardId: sessionId,
118
+ agentName: "/root",
119
+ ownerFolder,
120
+ databasePath: resolve(ownerFolder, ".pi", "agent-message-board.sqlite"),
121
+ enabled: false,
122
+ }),
123
+ enabled: rootBoardSetting(ctx).enabled,
124
+ };
125
+ }
126
+ export function saveBinding(pi: ExtensionAPI, value: BoardBinding) {
127
+ pi.appendEntry(BINDING, value);
128
+ }
129
+ export function members(ctx: ExtensionContext): BoardBinding[] {
130
+ const own = binding(ctx);
131
+ const found = new Map([[own.agentName, own]]);
132
+ for (const entry of ctx.sessionManager.getEntries()) {
133
+ if (entry.type === "custom" && entry.customType === REMOVED) {
134
+ const value = parseBinding(entry.data);
135
+ if (value.boardId === own.boardId) found.delete(value.agentName);
136
+ continue;
137
+ }
138
+ if (entry.type !== "custom" || entry.customType !== MEMBER) continue;
139
+ const value = parseBinding(entry.data);
140
+ if (value.rootSessionId === own.sessionId && value.boardId === own.boardId)
141
+ found.set(value.agentName, value);
142
+ }
143
+ return [...found.values()];
144
+ }
145
+ export function saveMember(pi: ExtensionAPI, value: BoardBinding) {
146
+ pi.appendEntry(MEMBER, value);
147
+ }
148
+ export function children(ctx: ExtensionContext): BoardChild[] {
149
+ const own = binding(ctx);
150
+ const found = new Map<string, BoardChild>();
151
+ for (const entry of ctx.sessionManager.getEntries()) {
152
+ if (entry.type === "custom" && entry.customType === REMOVED) {
153
+ const value = parseBinding(entry.data);
154
+ if (value.boardId === own.boardId) found.delete(value.agentName);
155
+ continue;
156
+ }
157
+ if (entry.type !== "custom" || entry.customType !== CHILD) continue;
158
+ if (!Check(ChildSchema, entry.data))
159
+ throw new Error("Invalid saved board child route");
160
+ const value = entry.data as BoardChild;
161
+ if (
162
+ value.parentSessionId === own.sessionId &&
163
+ value.binding.boardId === own.boardId
164
+ )
165
+ found.set(value.binding.agentName, value);
166
+ }
167
+ return [...found.values()];
168
+ }
169
+ export function saveChild(pi: ExtensionAPI, value: BoardChild) {
170
+ pi.appendEntry(CHILD, value);
171
+ }
172
+ export function removeMember(pi: ExtensionAPI, value: BoardBinding) {
173
+ pi.appendEntry(REMOVED, value);
174
+ }