@howaboua/pi-notes-compaction 0.0.0-stage → 0.0.2

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/src/index.ts ADDED
@@ -0,0 +1,255 @@
1
+ import { join } from "node:path";
2
+ import {
3
+ type ExtensionAPI,
4
+ getAgentDir,
5
+ } from "@earendil-works/pi-coding-agent";
6
+ import {
7
+ discoverBridge,
8
+ type NotesOwner,
9
+ OWNER_CHANNEL,
10
+ type OwnerRequest,
11
+ TOOL_NAMES,
12
+ } from "./bridge.js";
13
+ import { NotesLifecycle } from "./lifecycle.js";
14
+ import { exportNotesSnapshot, sessionRef } from "./notes.js";
15
+ import { type NotesSettings, readSettings, writeSettings } from "./settings.js";
16
+ import { openNotesSettings } from "./settings-screen.js";
17
+ import { NotesStore } from "./store.js";
18
+ import {
19
+ createSharedExecutor,
20
+ createTools,
21
+ HISTORY_USAGE,
22
+ NOTES_USAGE,
23
+ } from "./tools.js";
24
+ import {
25
+ registerWindowRenderer,
26
+ WINDOW_MESSAGE,
27
+ windowDetails,
28
+ } from "./windows.js";
29
+
30
+ export default function notesCompaction(pi: ExtensionAPI): void {
31
+ registerWindowRenderer(pi);
32
+ const dir = join(getAgentDir(), "notes-compaction");
33
+ const settingsPath = join(dir, "settings.json");
34
+ const store = new NotesStore(join(dir, "notes.sqlite"));
35
+ const lifecycle = new NotesLifecycle(pi, store);
36
+ const tools = createTools(pi, store, lifecycle);
37
+ let ownSource: string | undefined;
38
+ let unavailable: string | undefined;
39
+ const owner: NotesOwner = {
40
+ protocol: 1,
41
+ owner: "pi-notes-compaction",
42
+ identity: () =>
43
+ lifecycle.windows.current ? { ...lifecycle.windows.current } : undefined,
44
+ projectMessages: (messages) => lifecycle.windows.projectMessages(messages),
45
+ projectBranch: (entries) => lifecycle.windows.projectBranch(entries),
46
+ canBind: (ctx) =>
47
+ lifecycle.active &&
48
+ ctx.isIdle() &&
49
+ ctx.sessionManager.getEntries().every((entry) => {
50
+ // Herdr activation persists its empty registry before first task delivery.
51
+ if (
52
+ entry.type === "custom" &&
53
+ entry.customType === "herdr-agents-monitor-state"
54
+ ) {
55
+ const data = entry.data as
56
+ | { version?: unknown; agents?: unknown }
57
+ | undefined;
58
+ return Boolean(
59
+ data?.version === 2 &&
60
+ Array.isArray(data.agents) &&
61
+ data.agents.length === 0,
62
+ );
63
+ }
64
+ // Board adoption precedes context binding on a fresh spawn. These
65
+ // metadata records are preparation, not evidence of a first turn.
66
+ if (
67
+ entry.type === "custom" &&
68
+ [
69
+ "shepherdr-board-binding",
70
+ "shepherdr-board-awareness",
71
+ "codex-context-briefing",
72
+ ].includes(entry.customType)
73
+ )
74
+ return true;
75
+ if (
76
+ ["model_change", "thinking_level_change", "session_info"].includes(
77
+ entry.type,
78
+ )
79
+ )
80
+ return true;
81
+ if (
82
+ entry.type !== "custom_message" ||
83
+ entry.customType !== WINDOW_MESSAGE
84
+ )
85
+ return false;
86
+ const details = windowDetails(entry);
87
+ return Boolean(
88
+ details &&
89
+ details.identity.windowNumber === 0 &&
90
+ !details.trim &&
91
+ (details.kind === "window" || details.kind === "identity"),
92
+ );
93
+ }),
94
+ snapshotNotes: exportNotesSnapshot,
95
+ executeShared: createSharedExecutor(pi, store, lifecycle),
96
+ };
97
+ const stopOwner = pi.events.on(OWNER_CHANNEL, (data) => {
98
+ if (!data || typeof data !== "object") return;
99
+ const request = data as Partial<OwnerRequest>;
100
+ if (request.protocol === 1 && typeof request.reply === "function")
101
+ request.reply(owner);
102
+ });
103
+
104
+ pi.on("session_start", (_event, ctx) => {
105
+ lifecycle.reset(ctx);
106
+ lifecycle.active = false;
107
+ unavailable = undefined;
108
+ try {
109
+ Object.assign(lifecycle, readSettings(settingsPath));
110
+ lifecycle.bridge = discoverBridge(pi);
111
+ const claim = lifecycle.bridge?.claim(owner);
112
+ if (claim && !claim.claimed) throw new Error(claim.reason);
113
+ if (claim?.claimed) {
114
+ claim.registerTools(tools, ctx);
115
+ } else {
116
+ const conflicts = pi
117
+ .getAllTools()
118
+ .filter(
119
+ (tool) =>
120
+ TOOL_NAMES.some((name) => name === tool.name) &&
121
+ tool.sourceInfo.path !== ownSource,
122
+ );
123
+ if (conflicts.length)
124
+ throw new Error(
125
+ "Another extension already owns notes continuity. Installed PCC needs the documented takeover bridge before these extensions can run together.",
126
+ );
127
+ for (const tool of tools) pi.registerTool(tool);
128
+ ownSource = pi.getAllTools().find((tool) => tool.name === "notes")
129
+ ?.sourceInfo.path;
130
+ }
131
+ lifecycle.active = true;
132
+ lifecycle.initialize(ctx);
133
+ lifecycle.bridge?.configureTools?.(tools, ctx, {
134
+ notes: NOTES_USAGE,
135
+ history: HISTORY_USAGE,
136
+ });
137
+ store.updateFile(sessionRef(ctx));
138
+ } catch (error) {
139
+ lifecycle.active = false;
140
+ unavailable = error instanceof Error ? error.message : String(error);
141
+ ctx.ui.notify(`Notes continuity inactive: ${unavailable}`, "warning");
142
+ }
143
+ });
144
+ pi.on("before_agent_start", (_event, ctx) => {
145
+ if (lifecycle.active) lifecycle.beforeStart(ctx);
146
+ });
147
+ pi.on("input", (event, ctx) =>
148
+ lifecycle.active ? lifecycle.input(event, ctx) : undefined,
149
+ );
150
+ pi.on("agent_before_settle", (event) => {
151
+ if (lifecycle.active) lifecycle.outcome(event.outcome);
152
+ });
153
+ pi.on("agent_settled", async (event, ctx) => {
154
+ if (!lifecycle.active) return;
155
+ try {
156
+ await lifecycle.settled(event, ctx);
157
+ } catch (error) {
158
+ ctx.ui.notify(
159
+ `Context rollover failed: ${error instanceof Error ? error.message : String(error)}`,
160
+ "error",
161
+ );
162
+ }
163
+ });
164
+ pi.on("turn_end", (_event, ctx) => {
165
+ if (!lifecycle.active || lifecycle.fresh(ctx, lifecycle.runId)) return;
166
+ const reminder = lifecycle.windows.reminder(ctx);
167
+ return reminder ? { entries: [reminder] } : undefined;
168
+ });
169
+ pi.on("context", (event, ctx) => {
170
+ if (!lifecycle.active) return;
171
+ try {
172
+ lifecycle.windows.restore(ctx.sessionManager.getBranch());
173
+ return { messages: lifecycle.windows.projectMessages(event.messages) };
174
+ } catch (error) {
175
+ ctx.abort();
176
+ throw error;
177
+ }
178
+ });
179
+ pi.on("session_before_compact", (event, ctx) =>
180
+ lifecycle.active ? lifecycle.beforeCompact(event, ctx) : undefined,
181
+ );
182
+ pi.on("session_compact_failed", async (event, ctx) => {
183
+ if (lifecycle.active) await lifecycle.compactFailed(event, ctx);
184
+ });
185
+ pi.on("session_compact", (_event, ctx) => {
186
+ if (lifecycle.active)
187
+ lifecycle.windows.restore(ctx.sessionManager.getBranch());
188
+ });
189
+ pi.on("session_tree", (_event, ctx) => {
190
+ lifecycle.reset(ctx);
191
+ if (lifecycle.active) lifecycle.initialize(ctx);
192
+ });
193
+ pi.on("model_select", (_event, ctx) => {
194
+ if (lifecycle.active)
195
+ lifecycle.bridge?.configureTools?.(tools, ctx, {
196
+ notes: NOTES_USAGE,
197
+ history: HISTORY_USAGE,
198
+ });
199
+ });
200
+ pi.on("session_shutdown", () => {
201
+ lifecycle.reset();
202
+ lifecycle.active = false;
203
+ store.close();
204
+ stopOwner();
205
+ });
206
+
207
+ pi.registerCommand("notes", {
208
+ description: "Manage local notes and context rollover",
209
+ handler: async (args, ctx) => {
210
+ if (args.trim()) {
211
+ ctx.ui.notify("Use /notes to open settings.", "info");
212
+ return;
213
+ }
214
+ if (!ctx.hasUI) {
215
+ ctx.ui.notify("Notes settings require interactive Pi.", "warning");
216
+ return;
217
+ }
218
+ const save = (settings: NotesSettings) => {
219
+ writeSettings(settingsPath, settings);
220
+ Object.assign(lifecycle, settings);
221
+ };
222
+ const prune = async () => {
223
+ await ctx.waitForIdle();
224
+ store.updateFile(sessionRef(ctx));
225
+ const result = store.prune(ctx.sessionManager.getSessionId());
226
+ ctx.ui.notify(
227
+ `Pruned ${result.removed} sessions. Retained ${result.unknownPaths} unknown paths and ${result.inaccessible} inaccessible files.`,
228
+ "info",
229
+ );
230
+ };
231
+ try {
232
+ await openNotesSettings(ctx, {
233
+ settings: () => ({
234
+ normalCompaction: lifecycle.normalCompaction,
235
+ idleMinutes: lifecycle.idleMinutes,
236
+ }),
237
+ save,
238
+ prune,
239
+ status: () => {
240
+ const counts = store.status();
241
+ return [
242
+ lifecycle.active
243
+ ? "Continuity active"
244
+ : `Continuity inactive: ${unavailable ?? "not initialized"}`,
245
+ `${counts.sessions} sessions · ${counts.revisions} note revisions · ${counts.cachedResponses} cached responses`,
246
+ `Hybrid lookup: ${lifecycle.bridge?.lookup ? "available" : "unavailable"} · Encrypted delivery: ${lifecycle.bridge?.projectResult ? "available" : "unavailable"}`,
247
+ ];
248
+ },
249
+ });
250
+ } catch (error) {
251
+ ctx.ui.notify(`Notes settings failed: ${String(error)}`, "error");
252
+ }
253
+ },
254
+ });
255
+ }