@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/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # @howaboua/pi-notes-compaction
2
+
3
+ ## 0.0.2
4
+
5
+ - Added a `/notes` settings panel for normal compaction, idle rollover, continuity status and confirmed pruning, replacing the subcommands and JSON status output.
6
+
7
+ ## 0.0.1
8
+
9
+ - Added Notes Compaction: local checkpoint notes, searchable session history and notes-guided context windows for any Pi model, with optional Codex Conversion integration and cached remote lookup.
@@ -0,0 +1,100 @@
1
+ # PCC takeover contract, protocol 1
2
+
3
+ This contract is implemented by Pi Codex Conversion in this checkout and by this extension's `src/bridge.ts`. The extension stays inactive when legacy context tools exist without an acknowledged bridge.
4
+
5
+ Use Pi's public `pi.events` bus. Do not import PCC internals into this package or create a dependency cycle. `src/bridge.ts` defines the exact TypeScript signatures. Bus objects and callbacks are process-local, not serialized session data.
6
+
7
+ ## Discovery and one registration owner
8
+
9
+ Two request channels use synchronous callbacks:
10
+
11
+ ```ts
12
+ // PCC asks whether an external continuity owner is installed.
13
+ pi.events.emit("pi-notes-compaction:owner:v1", {
14
+ protocol: 1,
15
+ reply(owner) { /* retain the NotesOwner for this runtime */ }
16
+ })
17
+
18
+ // PCC publishes its optional adapter before session_start.
19
+ pi.events.on("pi-notes-compaction:bridge:v1", request => {
20
+ if (request.protocol === 1) request.reply(bridge)
21
+ })
22
+ ```
23
+
24
+ Every extension finishes loading before `session_start`. Both sides must discover again at that boundary. PCC must check for the external owner before its first session initialization and before every activation-plan or tool rewrite. This works in either load order. No prefix edit, timer or module-global import is discovery.
25
+
26
+ `bridge.claim(owner)` returns either `{ claimed: false, reason }` or `{ claimed: true, registerTools }`. A success synchronously transfers continuity policy ownership. The callback `registerTools(tools, ctx)` registers the supplied four definitions **through PCC's own `pi.registerTool` API**, replacing PCC's existing definitions in place. The new extension does not register duplicate names when takeover succeeds. Pi cannot unregister tools. Hiding a legacy definition in another extension is not sufficient because registry precedence depends on extension order.
27
+
28
+ PCC may defer its legacy registrations until discovery, but it must still use the same PCC-owned registration callback on takeover. With no external owner, PCC retains all existing behavior. A failed claim must leave its legacy owner intact. Do not acknowledge a partial takeover.
29
+
30
+ ## PCC owners that must yield
31
+
32
+ Add an explicit external-owner branch to `adapter/activation/runtime-plan.ts` and every continuity consumer. The external owner takes notes, history routing, context identities, reminders, manual checkpoint policy, idle input admission and window rollover. Disable PCC's corresponding Local, Tree and Remote policy owners, not its provider services.
33
+
34
+ Required boundaries in the current source:
35
+
36
+ - `context-management/tools.ts` and `extension/tools.ts`: replace the four tools through the registration callback. Active-tool and schema rewrites must preserve delegated definitions. Do not reinstall legacy routers on model changes or settings changes.
37
+ - `extension/session-lifecycle.ts` and `extension/turn-lifecycle.ts`: skip legacy notes/window initialization, input holding, checkpoint admission, note freshness, reminders, settlement records and continuation scheduling. Leave PCC's prompt preparation, runtime synchronization, Notebook status, voice, quota and provider lifecycle active.
38
+ - `extension/compaction-lifecycle.ts`: skip only legacy `contextWindows.prepareCompaction` and notes checkpoint overrides. When this extension allows normal compaction, run the unchanged installed PCC normal-compaction path. Overflow stays in the same window. Notebook checkpointing, native compaction, replay, transport reset and presentation remain PCC-owned.
39
+ - `context-management/window-manager.ts`, `window-request.ts` and native replay serializers: use `owner.identity(ctx)` and `owner.projectBranch(entries)` for the active window and retirement boundary. Preserve PCC request metadata and zero-based generation semantics. Do not recreate remote history-ingest requirements for local notes.
40
+ - PCC message preparation and `adapter/developer-messages.ts`: preserve `notes-compaction:window:v1` and `notes-compaction:checkpoint:v1` custom messages on every model. Compatible Responses routes may promote them to developer messages. Ordinary Pi conversion must retain them on other routes. Use `owner.projectMessages(messages)` before native replay/prewarm, without overwriting another extension's preparation.
41
+ - Settings UI: show notes continuity as externally managed. Preserve old PCC values for sessions without the external extension. Do not add another compaction setting to this extension.
42
+
43
+ Normal compaction output belongs exclusively to Pi and PCC. This package does not port V2, copy encrypted compaction checkpoints or persist its own compaction records.
44
+
45
+ `beforeWindow(ctx, signal?)` is optional. PCC can checkpoint Notebook and reset owned transport/window state there. It must fail before a new marker is appended if preparation fails. A rollover continuation is a settled **user** kickoff. Both the checkpoint and successor must run the complete `input` and `before_agent_start` chain, not a captured prompt or developer-message turn.
46
+
47
+ ## Code and Notebook
48
+
49
+ `configureTools(tools, ctx, contracts)` receives the delegated definitions and compact `notes` and `history` usage strings. Reuse PCC's existing `toNestedTool` and `registerCodeModeExtensionTools` machinery inside PCC:
50
+
51
+ - Defer notes/history schemas and full contracts through discovery. Keep them callable and discoverable in `ALL_TOOLS` without per-tool provider schemas or standing usage.
52
+ - Calls use ordinary query and text arguments, not encrypted model arguments.
53
+ - Plain local results remain ordinary JavaScript values. For results containing opaque remote responses, JavaScript gets a receipt and the model gets protected delivery.
54
+ - Preserve current-call and original-exec ancestry through `exec` and `wait`. Propagate the successful local note-write receipt to PCC's existing saved-note presentation if needed.
55
+ - Preserve `details.notesCompaction` on direct write results. Nested exec/wait must keep PCC's `contextNotesSaved` success or failure outcome, including a failed write after an earlier successful write. Checkpoint freshness requires completed write results, not receipt existence alone.
56
+ - `new_context` is `model-only`, sequential and **never nested**. Keep its native schema in Code and Notebook. Do not expose it through `tools` or execute a rollover under an exec cell.
57
+
58
+ Configuration is repeated on session start and model selection. It must be idempotent. Do not rewrite earlier tool calls or results.
59
+
60
+ ## Remote response cache and provider projection
61
+
62
+ Optional `lookup(query, ctx, signal?)` handles read-only notes/history operations through PCC's existing authenticated backend. The query is `{ namespace: "notes" | "history", params }`. Paths sent to lookup are fully qualified virtual paths. Writes never go to the remote backend.
63
+
64
+ Return `CachedResponse[]`, with each element containing:
65
+
66
+ - `query`: the exact query whose result these bytes represent.
67
+ - `bytes`: the unchanged backend response bytes as `Uint8Array`.
68
+ - `source`: the existing backend/source identity needed to project the response.
69
+ - `encoding`: the response encoding, normally `application/json`.
70
+ - `fetchedAt`: the access timestamp in milliseconds.
71
+ - `coverage`: `complete`, `partial` or `unknown`. This describes that response, not the entire remote archive.
72
+
73
+ Return the requested response and any additional complete query responses already present on access. Do not fabricate plaintext file indexes from encrypted lists or searches. The new extension caches every returned response, then projects responses matching the requested query. A network failure reuses the newest cached exact-query response per source and labels it offline. Database or delivery failure remains a failure.
74
+
75
+ `canProject(ctx)` must return false when the active model cannot consume encrypted results. Without compatible projection, ciphertext stays cached and local data remains available.
76
+
77
+ `projectResult(result, responses, callId, ctx, signal?)` returns a fresh `AgentToolResult`. Decode only the response envelope to recover its original `encrypted_output` and useful plain attachment hint. Do not decrypt, concatenate ciphertext strings, invent an old call ID or reuse an old protected-delivery record. The provider boundary must emit the actual current tool call's function output as native blocks:
78
+
79
+ ```ts
80
+ {
81
+ type: "function_call_output",
82
+ call_id: actualCurrentCallId,
83
+ output: [
84
+ { type: "input_text", text: localResultAndCoverage },
85
+ { type: "encrypted_content", encrypted_content: unchangedEncryptedOutput }
86
+ ]
87
+ }
88
+ ```
89
+
90
+ Multiple responses stay distinct blocks. Local append overlays stay labelled as overlays, not a decrypted merge. Local replacements supersede remote path reads. Opaque lists/searches can still describe old paths, so preserve the local-authority labels.
91
+
92
+ For direct calls, extend PCC's `codexHistoryNotes` extraction and Responses conversion to accept the fresh cache-backed result. For nested calls, use PCC's existing authenticated opaque capture and protocol-2 original-exec relay. Cache-backed responses need a new honest delivery envelope for the current call. Retain necessary existing source metadata, but do not add an account-binding, portability, export or key-management framework.
93
+
94
+ **Persistence boundary:** `Uint8Array` is only the in-process adapter type. Any bytes placed in persisted result details must be encoded explicitly, for example base64 with the declared encoding. JSON serialization of a typed array is not a supported cache or replay format. SQLite stores the original BLOB. PCC retains its existing serialized opaque-delivery format where possible.
95
+
96
+ Optional `agentName(ctx)` and `route(query, callId, ctx, signal?)` adapt existing PCC family identity and sharing routes. `route` returns `undefined` for current-agent requests. For another agent it returns the normal routed result, preserving that owner's freshness and delivery semantics. Without this capability, cross-agent targets fail explicitly rather than reading another session by guesswork.
97
+
98
+ ## Validation required for the PCC patch
99
+
100
+ Use the real Pi registry in both extension orders. Check that only PCC owns the four delegated registrations and that its remaining prompt/tool rewrites retain them. Capture final provider schemas and instructions separately in normal, Code and Notebook modes. Exercise direct and original-exec cached opaque delivery, including `wait`, offline lookup, incompatible model selection and persisted replay. Run abort/error checkpoint paths and a later extension's preparation hook. Existing PCC alone must remain unchanged.
package/README.md CHANGED
@@ -1,3 +1,43 @@
1
- # Temporary Holding Version
1
+ # Pi Notes Compaction
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Keep working across fresh context windows with local checkpoint notes, searchable session history and optional remote lookup. Works with any Pi model.
4
+
5
+ ## Install
6
+
7
+ Requires Pi 1.1.0 or newer and Node.js 22.18 or newer.
8
+
9
+ ```sh
10
+ pi install npm:@howaboua/pi-notes-compaction
11
+ ```
12
+
13
+ Reload Pi, then ask the agent to save a checkpoint and start a new context window. It can read those notes and search earlier session history to continue.
14
+
15
+ With Pi Codex Conversion 3.0.48 or newer, local notes work alongside Codex tools, Code Mode, Notebook and normal compaction. Older PCC versions without this integration leave the extension inactive. Run `/notes` to check activation and available capabilities. Extension authors can read the [integration contract](PCC-INTEGRATION.md).
16
+
17
+ ## Notes and windows
18
+
19
+ The agent keeps the familiar `notes`, `history`, `new_context` and `get_context_remaining` tools. Notes use virtual paths. Relative paths belong to the current agent. History searches the selected Pi session branch, including earlier windows. It does not create a second transcript archive.
20
+
21
+ By default, `new_context` starts a fresh window without generating a summary. The agent receives recent note paths, and Pi's saved history remains available for lookup.
22
+
23
+ Reminders arrive at 85% and 90% context use. `/compact` requests a notes checkpoint and opens a new window only after a completed run with fresh saved notes. Abort, errors and failed writes retain the old context. Overflow recovery still uses Pi's normal compaction.
24
+
25
+ Idle rollover is off by default. Choose 5, 15, 25 or 55 minutes in `/notes`. After that interval, the next input waits for rollover only if the latest completed turn saved fresh notes. Otherwise it proceeds in the current window without requesting a checkpoint. These intervals are user preferences, not provider cache-expiry guarantees. Held input retains its original text, attachments and SDK options. A failed rollover keeps input pending. Submit another prompt to retry, or reload to cancel it.
26
+
27
+ ## Management
28
+
29
+ `/notes` opens the settings panel in interactive Pi. It shows continuity status, storage counts and remote capabilities. Changes save immediately.
30
+
31
+ - **Normal compaction** includes the installed Pi or PCC compaction flow on `new_context`. Off keeps notes-only rollover, the default.
32
+ - **Idle rollover** selects the interval or disables it.
33
+ - **Prune missing sessions** removes records for missing session files after confirmation. Unknown paths, permission errors and the active session are retained.
34
+
35
+ Notes are stored as plaintext in `<Pi agent directory>/notes-compaction/notes.sqlite`, honoring `PI_CODING_AGENT_DIR`. Saved session snapshots preserve notes across forks and branch navigation, including after database pruning. Existing plaintext PCC notes remain readable without an import.
36
+
37
+ ## Optional remote lookup
38
+
39
+ With the documented provider bridge, reads and searches automatically combine local notes or JSONL history with remote results. Remote response bytes are cached verbatim in SQLite. Cached lookups can be reused offline, with their original coverage and an explicit exact-query limit. Encrypted listings cannot be searched or expanded locally into queries that were never cached.
40
+
41
+ New writes always stay local and work with any model. `write_file` replaces the local path and takes precedence over a remote read of that path. `append_to_file` adds a local overlay when no local replacement exists. Remote base content and the overlay remain separate. List and search results label local replacements as authoritative because opaque remote results can contain older versions.
42
+
43
+ Encrypted remote content needs a compatible provider bridge and the user's compatible OpenAI setup. The cache does not decrypt content or make it portable across incompatible setups. Without compatible delivery, local notes remain usable and the result reports that cached remote content is unavailable.
package/changelog.ts ADDED
@@ -0,0 +1,337 @@
1
+ // Generated extension packages copy this file during changelog:extensions.
2
+
3
+ import { randomUUID } from "node:crypto";
4
+ import { readFileSync } from "node:fs";
5
+ import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
6
+ import { dirname, join } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import {
9
+ DynamicBorder,
10
+ type ExtensionAPI,
11
+ getAgentDir,
12
+ getMarkdownTheme,
13
+ } from "@earendil-works/pi-coding-agent";
14
+ import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
15
+
16
+ const ENTRY_TYPE = "@howaboua/pi-stuff/changelog";
17
+ const REGISTRATION_CHANNEL = "@howaboua/pi-stuff/changelog/register/v1";
18
+ const STATE_FILENAME = "howaboua-pi-stuff-changelog.json";
19
+ const SUPPRESS_KEY = "suppress";
20
+ const LOCK_STALE_MS = 30_000;
21
+ const LOCK_TIMEOUT_MS = 2_000;
22
+
23
+ interface PackageMetadata {
24
+ name: string;
25
+ version: string;
26
+ }
27
+
28
+ interface PackageRegistration extends PackageMetadata {
29
+ changelogPath: string;
30
+ }
31
+
32
+ interface RegistrationEvent {
33
+ accept: () => void;
34
+ registration: PackageRegistration;
35
+ }
36
+
37
+ interface ChangelogEntry {
38
+ content: string;
39
+ version: string;
40
+ }
41
+
42
+ interface ChangelogEntryData {
43
+ markdown: string;
44
+ }
45
+
46
+ type ChangelogState = Record<string, boolean | string>;
47
+
48
+ function packageRegistration(): PackageRegistration {
49
+ const packageDirectory = fileURLToPath(new URL(".", import.meta.url));
50
+ const metadata = JSON.parse(
51
+ readFileSync(join(packageDirectory, "package.json"), "utf8"),
52
+ ) as Partial<PackageMetadata>;
53
+ if (typeof metadata.name !== "string" || typeof metadata.version !== "string")
54
+ throw new Error(`Invalid package metadata in ${packageDirectory}`);
55
+ return {
56
+ name: metadata.name,
57
+ version: metadata.version,
58
+ changelogPath: join(packageDirectory, "CHANGELOG.md"),
59
+ };
60
+ }
61
+
62
+ function isRegistrationEvent(value: unknown): value is RegistrationEvent {
63
+ if (!value || typeof value !== "object") return false;
64
+ const event = value as Partial<RegistrationEvent>;
65
+ return (
66
+ typeof event.accept === "function" &&
67
+ typeof event.registration?.name === "string" &&
68
+ typeof event.registration.version === "string" &&
69
+ typeof event.registration.changelogPath === "string"
70
+ );
71
+ }
72
+
73
+ function parseChangelog(markdown: string): ChangelogEntry[] {
74
+ const entries: ChangelogEntry[] = [];
75
+ let currentLines: string[] = [];
76
+ let currentVersion: string | undefined;
77
+
78
+ for (const line of markdown.split("\n")) {
79
+ const version = line.match(/^##\s+\[?(\d+\.\d+\.\d+)\]?(?:\s.*)?$/)?.[1];
80
+ if (version) {
81
+ if (currentVersion)
82
+ entries.push({
83
+ content: currentLines.join("\n").trim(),
84
+ version: currentVersion,
85
+ });
86
+ currentVersion = version;
87
+ currentLines = [line];
88
+ } else if (currentVersion) {
89
+ currentLines.push(line);
90
+ }
91
+ }
92
+
93
+ if (currentVersion)
94
+ entries.push({
95
+ content: currentLines.join("\n").trim(),
96
+ version: currentVersion,
97
+ });
98
+ return entries;
99
+ }
100
+
101
+ function compareVersions(left: string, right: string): number {
102
+ const leftParts = left.split(".").map(Number);
103
+ const rightParts = right.split(".").map(Number);
104
+ for (let index = 0; index < 3; index++) {
105
+ const difference = (leftParts[index] ?? 0) - (rightParts[index] ?? 0);
106
+ if (difference !== 0) return difference;
107
+ }
108
+ return 0;
109
+ }
110
+
111
+ function isStableVersion(version: string): boolean {
112
+ return /^\d+\.\d+\.\d+$/.test(version);
113
+ }
114
+
115
+ function statePath(): string {
116
+ return join(getAgentDir(), STATE_FILENAME);
117
+ }
118
+
119
+ async function readState(path: string): Promise<ChangelogState> {
120
+ let text: string;
121
+ try {
122
+ text = await readFile(path, "utf8");
123
+ } catch (error) {
124
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return {};
125
+ throw error;
126
+ }
127
+ const parsed = JSON.parse(text) as unknown;
128
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
129
+ throw new Error(`${path} must contain a JSON object`);
130
+ for (const [name, value] of Object.entries(parsed)) {
131
+ if (name === SUPPRESS_KEY) {
132
+ if (typeof value !== "boolean")
133
+ throw new Error(`${path} ${SUPPRESS_KEY} must be a boolean`);
134
+ } else if (typeof value !== "string") {
135
+ throw new Error(`${path} version for ${name} must be a string`);
136
+ }
137
+ }
138
+ return parsed as ChangelogState;
139
+ }
140
+
141
+ async function acquireLock(path: string): Promise<() => Promise<void>> {
142
+ const deadline = Date.now() + LOCK_TIMEOUT_MS;
143
+ while (true) {
144
+ try {
145
+ await mkdir(path);
146
+ return () => rm(path, { force: true, recursive: true });
147
+ } catch (error) {
148
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
149
+ const info = await stat(path).catch(() => undefined);
150
+ if (info && Date.now() - info.mtimeMs > LOCK_STALE_MS) {
151
+ await rm(path, { force: true, recursive: true });
152
+ continue;
153
+ }
154
+ if (Date.now() >= deadline)
155
+ throw new Error(`Timed out waiting for ${path}`);
156
+ await new Promise((resolve) => setTimeout(resolve, 20));
157
+ }
158
+ }
159
+ }
160
+
161
+ async function writeState(path: string, state: ChangelogState): Promise<void> {
162
+ await mkdir(dirname(path), { mode: 0o700, recursive: true });
163
+ const temporaryPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
164
+ try {
165
+ const ordered = Object.fromEntries(
166
+ Object.entries(state).sort(([left], [right]) =>
167
+ left.localeCompare(right),
168
+ ),
169
+ );
170
+ await writeFile(temporaryPath, `${JSON.stringify(ordered, null, 2)}\n`, {
171
+ encoding: "utf8",
172
+ mode: 0o600,
173
+ });
174
+ await rename(temporaryPath, path);
175
+ } catch (error) {
176
+ await rm(temporaryPath, { force: true });
177
+ throw error;
178
+ }
179
+ }
180
+
181
+ function packageMarkdown(
182
+ registration: PackageRegistration,
183
+ entries: ChangelogEntry[],
184
+ ): string {
185
+ const body = (entry: ChangelogEntry) =>
186
+ entry.content
187
+ .replace(/^##[^\n]*\n*/, "")
188
+ .replace(/^###\s+(?:Changes|Minor Changes|Patch Changes)\s*\n*/gim, "")
189
+ .replace(/^###\s+(.+)$/gm, "**$1**");
190
+ if (entries.length === 1) {
191
+ const [entry] = entries;
192
+ if (!entry) return "";
193
+ return `## ${registration.name} ${entry.version}\n\n${body(entry)}`;
194
+ }
195
+ return [
196
+ `## ${registration.name}`,
197
+ ...entries.map((entry) => `**${entry.version}**\n\n${body(entry)}`),
198
+ ].join("\n\n");
199
+ }
200
+
201
+ async function claimUpdates(
202
+ registrations: Iterable<PackageRegistration>,
203
+ ): Promise<{ errors: string[]; markdown?: string }> {
204
+ const stableRegistrations = [...registrations]
205
+ .filter((registration) => isStableVersion(registration.version))
206
+ .sort((left, right) => left.name.localeCompare(right.name));
207
+ if (stableRegistrations.length === 0) return { errors: [] };
208
+ const path = statePath();
209
+ await mkdir(dirname(path), { mode: 0o700, recursive: true });
210
+ const release = await acquireLock(`${path}.lock`);
211
+ try {
212
+ const state = await readState(path);
213
+ const errors: string[] = [];
214
+ const sections: string[] = [];
215
+ let changed = false;
216
+ if (state[SUPPRESS_KEY] === true) {
217
+ for (const registration of stableRegistrations) {
218
+ const seenVersion = state[registration.name];
219
+ if (
220
+ typeof seenVersion !== "string" ||
221
+ compareVersions(registration.version, seenVersion) > 0
222
+ ) {
223
+ state[registration.name] = registration.version;
224
+ changed = true;
225
+ }
226
+ }
227
+ if (changed) await writeState(path, state);
228
+ return { errors };
229
+ }
230
+ for (const registration of stableRegistrations) {
231
+ const seenVersion = state[registration.name];
232
+ if (
233
+ typeof seenVersion === "string" &&
234
+ compareVersions(registration.version, seenVersion) <= 0
235
+ )
236
+ continue;
237
+ try {
238
+ const entries = parseChangelog(
239
+ await readFile(registration.changelogPath, "utf8"),
240
+ ).filter(
241
+ (entry) =>
242
+ compareVersions(entry.version, registration.version) <= 0 &&
243
+ (typeof seenVersion === "string"
244
+ ? compareVersions(entry.version, seenVersion) > 0
245
+ : entry.version === registration.version),
246
+ );
247
+ if (entries.length === 0) {
248
+ errors.push(
249
+ `No ${registration.version} changelog entry found for ${registration.name}`,
250
+ );
251
+ continue;
252
+ }
253
+ sections.push(packageMarkdown(registration, entries));
254
+ state[registration.name] = registration.version;
255
+ changed = true;
256
+ } catch (error) {
257
+ errors.push(
258
+ `Could not read the ${registration.name} changelog: ${error instanceof Error ? error.message : String(error)}`,
259
+ );
260
+ }
261
+ }
262
+ if (changed) await writeState(path, state);
263
+ return {
264
+ errors,
265
+ ...(sections.length > 0 ? { markdown: sections.join("\n\n") } : {}),
266
+ };
267
+ } finally {
268
+ await release();
269
+ }
270
+ }
271
+
272
+ function registerCoordinator(
273
+ pi: ExtensionAPI,
274
+ registration: PackageRegistration,
275
+ ): void {
276
+ let accepted = false;
277
+ pi.events.emit(REGISTRATION_CHANNEL, {
278
+ registration,
279
+ accept: () => {
280
+ accepted = true;
281
+ },
282
+ } satisfies RegistrationEvent);
283
+ if (accepted) return;
284
+
285
+ const registrations = new Map([[registration.name, registration]]);
286
+ pi.events.on(REGISTRATION_CHANNEL, (value) => {
287
+ if (!isRegistrationEvent(value)) return;
288
+ registrations.set(value.registration.name, value.registration);
289
+ value.accept();
290
+ });
291
+ pi.registerEntryRenderer<ChangelogEntryData>(
292
+ ENTRY_TYPE,
293
+ (entry, _options, theme) => {
294
+ if (!entry.data) return undefined;
295
+ const container = new Container();
296
+ container.addChild(new DynamicBorder());
297
+ container.addChild(
298
+ new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0),
299
+ );
300
+ container.addChild(new Spacer(1));
301
+ container.addChild(
302
+ new Markdown(entry.data.markdown.trim(), 1, 0, getMarkdownTheme()),
303
+ );
304
+ container.addChild(new Spacer(1));
305
+ container.addChild(new DynamicBorder());
306
+ return container;
307
+ },
308
+ );
309
+ pi.on("session_start", async (event, ctx) => {
310
+ if (event.reason === "reload" || ctx.mode !== "tui") return;
311
+ if (
312
+ ctx.sessionManager
313
+ .getEntries()
314
+ .some((entry) =>
315
+ ["branch_summary", "compaction", "message"].includes(entry.type),
316
+ )
317
+ )
318
+ return;
319
+ try {
320
+ const update = await claimUpdates(registrations.values());
321
+ if (update.markdown)
322
+ pi.appendEntry<ChangelogEntryData>(ENTRY_TYPE, {
323
+ markdown: update.markdown,
324
+ });
325
+ for (const error of update.errors) ctx.ui.notify(error, "warning");
326
+ } catch (error) {
327
+ ctx.ui.notify(
328
+ `Could not update Howaboua changelog state: ${error instanceof Error ? error.message : String(error)}`,
329
+ "warning",
330
+ );
331
+ }
332
+ });
333
+ }
334
+
335
+ export default function howabouaPackageChangelog(pi: ExtensionAPI): void {
336
+ registerCoordinator(pi, packageRegistration());
337
+ }
package/package.json CHANGED
@@ -1,6 +1,51 @@
1
1
  {
2
- "name": "@howaboua/pi-notes-compaction",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@howaboua/pi-notes-compaction",
3
+ "version": "0.0.2",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/IgorWarzocha/howaboua-pi-stuff.git",
10
+ "directory": "packages/pi-notes-compaction"
11
+ },
12
+ "files": [
13
+ "src",
14
+ "README.md",
15
+ "PCC-INTEGRATION.md",
16
+ "changelog.ts",
17
+ "CHANGELOG.md"
18
+ ],
19
+ "engines": {
20
+ "node": ">=22.18.0"
21
+ },
22
+ "type": "module",
23
+ "description": "Provider-neutral session notes and notes-guided context windows for Pi",
24
+ "license": "MIT",
25
+ "pi": {
26
+ "extensions": [
27
+ "./changelog.ts",
28
+ "./src/index.ts"
29
+ ]
30
+ },
31
+ "main": "src/index.ts",
32
+ "scripts": {
33
+ "check": "bun run lint && bun run typecheck",
34
+ "lint": "biome check .",
35
+ "format": "biome check --write .",
36
+ "typecheck": "tsgo --noEmit"
37
+ },
38
+ "dependencies": {
39
+ "better-sqlite3": "13.0.3"
40
+ },
41
+ "devDependencies": {
42
+ "@types/better-sqlite3": "9.6.0"
43
+ },
44
+ "peerDependencies": {
45
+ "@earendil-works/pi-coding-agent": ">=1.1.0",
46
+ "@earendil-works/pi-agent-core": ">=1.0.4",
47
+ "@earendil-works/pi-ai": ">=1.0.4",
48
+ "@earendil-works/pi-tui": ">=1.0.4",
49
+ "typebox": "*"
50
+ }
51
+ }