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