@astrosheep/pi-context 0.2.0 → 0.4.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 (3) hide show
  1. package/README.md +4 -3
  2. package/package.json +1 -1
  3. package/src/index.ts +49 -27
package/README.md CHANGED
@@ -19,8 +19,9 @@ pi -e npm:@astrosheep/pi-context
19
19
  The extension composes Pi's public `session_before_compact` / `session_compact` hooks, custom session entries, and the `context` hook to approximate Codex's experimental context management:
20
20
 
21
21
  - **`new_context` tool** — the model requests a fresh context window. The extension waits for the current tool turn to end, compacts with a short deterministic reset message (old conversation is excluded from the new provider context but stays in the session), then sends exactly one hidden continuation turn.
22
- - **`<context_window>` hint** — every model request carries a Codex-equivalent fragment with the agent name, first/current/previous window IDs, and the 5 most recently updated notes. The model gets recovery entry points, not a bare "go search" message.
23
- - **Low-budget guidance** — when remaining context drops to 16,000 tokens or below, a `<context_window_guidance>` reminder is injected exactly once per window (re-armed after each reset), matching Codex's `token_budget` reminder semantics: it tells the model to persist state with `notes_write_file` and call `new_context` before the window closes.
22
+ - **`<context_window>` hint** — persisted as a visible custom message at session start and after each window reset (Codex-style: written once into history instead of re-injected per request). It carries the agent name, first/current/previous window IDs, and the 5 most recently updated notes. Within a window the note list goes stale, exactly like Codex's steady-state world-state diffing.
23
+ - **Low-budget guidance** — while remaining context is at or below 16,000 tokens, a `<context_window_guidance>` reminder is injected transiently on every request (transient single-shot would vanish from the next request; Codex's persisted reminder stays visible, so continuous low-budget injection is the effective parity). It tells the model to persist state with `notes_write_file` and call `new_context` before the window closes. Note: Pi's built-in auto-compaction fires when remaining context falls below `reserveTokens` (default 16,384), so raise the reminder threshold above your `reserveTokens` or the reminder never precedes compaction.
24
+ - **Runtime toggle** — `/pi-context off` disables hint injection, guidance, and reset-style compaction (Pi's default compaction, including `keepRecentTokens`, applies again). `/pi-context on` re-enables; a bare `/pi-context` reports the current state.
24
25
  - **History tools** — the model searches pre-reset conversation with case-sensitive literal substring search, exactly like Codex's `history.*` namespace.
25
26
  - **Notes tools** — persistent, session-scoped virtual files that survive window resets.
26
27
 
@@ -55,7 +56,7 @@ Two extra controls compose Pi public APIs:
55
56
 
56
57
  On `session_before_compact`, the extension appends a persistent custom reset marker through public `pi.appendEntry`, reads that real marker ID from the readonly session manager, and returns it as `firstKeptEntryId`. Pi's `buildContextEntries()` then keeps the compaction envelope plus that custom marker; custom markers are excluded from LLM context. Thus the subsequent provider context contains the short reset result and hidden continuation, not old conversation messages. The old entries remain only in the session tree for `history_*`.
57
58
 
58
- The same handler is used for native automatic compaction. When Pi marks an overflow compaction `willRetry`, Pi core performs its single retry itself and this extension deliberately sends no second continuation.
59
+ The same handler is used for native automatic compaction. When Pi marks an overflow compaction `willRetry`, Pi core performs its single retry itself and this extension deliberately sends no second continuation. While the extension is enabled, its custom reset keeps nothing after the boundary marker, so Pi's `keepRecentTokens` setting has no effect; with `/pi-context off`, Pi's default compaction (and `keepRecentTokens`) applies again.
59
60
 
60
61
  This is a composition of public `session_before_compact`, `session_compact`, `pi.appendEntry`, `ctx.compact`, and `pi.sendMessage`; it is not a Pi-core `newSession` call. A manual Pi compaction is only eligible when Pi's own `prepareCompaction()` accepts the session. Therefore a `new_context` request in a too-small/uncompactable session fails cleanly without default-summary fallback or continuation. A core change would be needed only to guarantee a force-reset at arbitrary small context sizes or to atomically interrupt a mixed parallel tool batch.
61
62
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/pi-context",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "Codex-style context windows for Pi: reset-style compaction, durable session history tools, and persistent notes.",
6
6
  "license": "MIT",
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@ import { defineTool, type ExtensionAPI, type ExtensionContext } from "@earendil-
4
4
 
5
5
  const STATE_TYPE = "pi-context/state";
6
6
  const NOTE_TYPE = "pi-context/note";
7
+ const HINT_TYPE = "pi-context/hint";
7
8
  const RESET_MARKER_TYPE = "pi-context/reset-marker";
8
9
  const CONTINUATION_TYPE = "pi-context/continuation";
9
10
  const MAX_NOTE_BYTES = 1_000_000;
@@ -239,11 +240,6 @@ function tokenBudgetGuidance(remaining: number): string {
239
240
  return `${GUIDANCE_OPEN_TAG}\nYou have ${remaining} tokens left in this context window. Write durable state with notes_write_file and call new_context before the window closes.\n${GUIDANCE_CLOSE_TAG}`;
240
241
  }
241
242
 
242
- function currentWindowId(ctx: ExtensionContext): string | undefined {
243
- const windows = historyFromSession(ctx);
244
- return windows[windows.length - 1]?.windowId;
245
- }
246
-
247
243
  function lineRange(text: string, startValue: unknown, stopValue: unknown) {
248
244
  const lines = text.split("\n");
249
245
  const resolve = (value: unknown, fallback: number) => {
@@ -264,13 +260,39 @@ const role = Type.Union([Type.Literal("user"), Type.Literal("assistant"), Type.L
264
260
 
265
261
  export default function piContext(pi: ExtensionAPI) {
266
262
  let rollover: "idle" | "requested" | "compacting" | "continued" = "idle";
267
- let reminderClaimedInWindow: string | undefined;
263
+ let enabled = true;
264
+
265
+ /** Persist the context_window hint as a visible message (lands in history and the TUI), Codex-style. */
266
+ const persistHint = (ctx: ExtensionContext) => {
267
+ pi.sendMessage({ customType: HINT_TYPE, content: contextWindowHint(ctx), display: true }, { triggerTurn: false });
268
+ };
269
+
270
+ pi.on("session_start", (_event, ctx) => {
271
+ if (!enabled) return;
272
+ persistHint(ctx);
273
+ });
268
274
  const saveNote = (op: NoteOperation) => {
269
275
  // pi.appendEntry writes a custom SessionManager entry. Custom entries are persistent but excluded from LLM context.
270
276
  // ExtensionContext deliberately exposes only a readonly SessionManager, so this is the public extension write path.
271
277
  pi.appendEntry(NOTE_TYPE, op);
272
278
  };
273
279
 
280
+ pi.registerCommand("pi-context", {
281
+ description: "Toggle pi-context: context_window hint, low-budget guidance, and reset-style compaction",
282
+ getArgumentCompletions: (prefix) =>
283
+ ["on", "off"].filter((a) => a.startsWith(prefix)).map((a) => ({ value: a, label: a })),
284
+ handler: async (args, cmdCtx) => {
285
+ const arg = args.trim().toLowerCase();
286
+ if (arg === "on") enabled = true;
287
+ else if (arg === "off") enabled = false;
288
+ else if (arg !== "") {
289
+ cmdCtx.ui.notify("Usage: /pi-context [on|off]", "error");
290
+ return;
291
+ }
292
+ cmdCtx.ui.notify(`pi-context: ${enabled ? "on" : "off"}`, "info");
293
+ },
294
+ });
295
+
274
296
  pi.registerTool(defineTool({
275
297
  name: "history_list_windows",
276
298
  label: "History list windows",
@@ -391,27 +413,19 @@ export default function piContext(pi: ExtensionAPI) {
391
413
  }
392
414
 
393
415
  pi.on("context", (event, ctx) => {
394
- // Rebuilt per request, so no state diffing is needed; identical to Codex's
395
- // context_window developer fragment rendered into each model call.
396
- const userText = (text: string) => ({
416
+ if (!enabled) return undefined;
417
+ // Transient per-request while below the threshold. Codex's reminder persists in
418
+ // history and therefore stays visible; re-injecting while low is the effective parity.
419
+ const usage = ctx.getContextUsage();
420
+ if (!usage || usage.tokens === null) return undefined;
421
+ const remaining = Math.max(0, usage.contextWindow - usage.tokens);
422
+ if (remaining > REMINDER_THRESHOLD_TOKENS) return undefined;
423
+ const guidance = {
397
424
  role: "user" as const,
398
- content: [{ type: "text" as const, text }],
425
+ content: [{ type: "text" as const, text: tokenBudgetGuidance(remaining) }],
399
426
  timestamp: Date.now(),
400
- });
401
- const injected = [userText(contextWindowHint(ctx))];
402
-
403
- // Codex token_budget.maybe_record parity: below the threshold, claim the
404
- // reminder once per context window; a new window makes it eligible again.
405
- const usage = ctx.getContextUsage();
406
- if (usage && usage.tokens !== null) {
407
- const remaining = Math.max(0, usage.contextWindow - usage.tokens);
408
- const windowId = currentWindowId(ctx);
409
- if (remaining <= REMINDER_THRESHOLD_TOKENS && reminderClaimedInWindow !== windowId) {
410
- reminderClaimedInWindow = windowId;
411
- injected.push(userText(tokenBudgetGuidance(remaining)));
412
- }
413
- }
414
- return { messages: [...injected, ...event.messages] };
427
+ };
428
+ return { messages: [guidance, ...event.messages] };
415
429
  });
416
430
 
417
431
  pi.registerTool(defineTool({
@@ -432,18 +446,24 @@ export default function piContext(pi: ExtensionAPI) {
432
446
  description: "Request a reset-style context rollover after this tool result is safely recorded. Call alone in a tool batch.",
433
447
  parameters: Type.Object({}, { additionalProperties: false }),
434
448
  async execute() {
449
+ if (!enabled) return output({ error: "pi-context is off (/pi-context on to enable)" });
435
450
  if (rollover === "idle") rollover = "requested";
436
451
  return output({ status: rollover === "requested" ? "rollover_requested" : "rollover_already_pending" }, undefined, true);
437
452
  },
438
453
  }));
439
454
 
440
455
  pi.on("agent_end", (_event, ctx) => {
456
+ if (!enabled) {
457
+ if (rollover === "requested") rollover = "idle";
458
+ return;
459
+ }
441
460
  if (rollover !== "requested") return;
442
461
  rollover = "compacting";
443
462
  ctx.compact({ onError: () => { if (rollover === "compacting") rollover = "idle"; } });
444
463
  });
445
464
 
446
465
  pi.on("session_before_compact", async (event, ctx) => {
466
+ if (!enabled) return undefined; // Default Pi compaction applies; keepRecentTokens is honored again.
447
467
  // Never let an aborted or failed custom reset fall through to Pi's default summary.
448
468
  if (event.signal.aborted) return { cancel: true };
449
469
  try {
@@ -456,12 +476,14 @@ export default function piContext(pi: ExtensionAPI) {
456
476
  }
457
477
  });
458
478
 
459
- pi.on("session_compact", (event) => {
479
+ pi.on("session_compact", (event, ctx) => {
480
+ if (!enabled) return;
460
481
  // Overflow retry is already continued once by Pi core. Sending another turn would duplicate it.
461
482
  if (event.willRetry) return;
462
483
  if (rollover !== "compacting") return;
463
484
  rollover = "continued";
464
485
  pi.appendEntry(STATE_TYPE, { version: 1, lastResetEntryId: event.compactionEntry.id });
486
+ persistHint(ctx);
465
487
  pi.sendMessage({ customType: CONTINUATION_TYPE, content: CONTINUATION, display: false }, { triggerTurn: true });
466
488
  });
467
489
 
@@ -470,4 +492,4 @@ export default function piContext(pi: ExtensionAPI) {
470
492
  });
471
493
  }
472
494
 
473
- export const internal = { MAX_NOTE_BYTES, NOTE_TYPE, RESET_MARKER_TYPE, RESET_SUMMARY, CONTINUATION, CONTEXT_WINDOW_OPEN_TAG, GUIDANCE_OPEN_TAG, REMINDER_THRESHOLD_TOKENS, lineRange, assertVirtualPath };
495
+ export const internal = { MAX_NOTE_BYTES, NOTE_TYPE, HINT_TYPE, RESET_MARKER_TYPE, RESET_SUMMARY, CONTINUATION, CONTEXT_WINDOW_OPEN_TAG, GUIDANCE_OPEN_TAG, REMINDER_THRESHOLD_TOKENS, lineRange, assertVirtualPath };