@astrosheep/pi-context 0.3.0 → 0.4.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.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/src/index.ts +33 -30
package/README.md CHANGED
@@ -19,8 +19,8 @@ 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. 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.
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 appended after the request messages on every call (transient single-shot would vanish from the next request; Codex's persisted reminder stays visible, so continuous low-budget injection is the effective parity). The text is static and appended (not prepended), so the provider's prefix cache survives: crossing the threshold adds one stable suffix segment instead of rewriting the prefix. The exact remaining figure is one `get_context_remaining` call away. 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
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.
25
25
  - **History tools** — the model searches pre-reset conversation with case-sensitive literal substring search, exactly like Codex's `history.*` namespace.
26
26
  - **Notes tools** — persistent, session-scoped virtual files that survive window resets.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/pi-context",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
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;
@@ -234,14 +235,13 @@ export function contextWindowHint(ctx: ExtensionContext): string {
234
235
  return `${CONTEXT_WINDOW_OPEN_TAG}\n${lines.join("\n")}\n${CONTEXT_WINDOW_CLOSE_TAG}`;
235
236
  }
236
237
 
237
- /** Codex-equivalent low-budget reminder: threshold-gated, claimed once per context window. */
238
- function tokenBudgetGuidance(remaining: number): string {
239
- 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
-
242
- function currentWindowId(ctx: ExtensionContext): string | undefined {
243
- const windows = historyFromSession(ctx);
244
- return windows[windows.length - 1]?.windowId;
238
+ /**
239
+ * Codex-equivalent low-budget reminder. Static by design: this text is injected on
240
+ * every request while below the threshold, so a varying token count would invalidate
241
+ * the provider's prefix cache on each call. The exact figure is one tool call away.
242
+ */
243
+ function tokenBudgetGuidance(): string {
244
+ return `${GUIDANCE_OPEN_TAG}\nContext budget is at or below ${REMINDER_THRESHOLD_TOKENS} tokens remaining. Persist durable state with notes_write_file, then call new_context before the window closes. get_context_remaining reports the exact figure.\n${GUIDANCE_CLOSE_TAG}`;
245
245
  }
246
246
 
247
247
  function lineRange(text: string, startValue: unknown, stopValue: unknown) {
@@ -264,8 +264,17 @@ const role = Type.Union([Type.Literal("user"), Type.Literal("assistant"), Type.L
264
264
 
265
265
  export default function piContext(pi: ExtensionAPI) {
266
266
  let rollover: "idle" | "requested" | "compacting" | "continued" = "idle";
267
- let reminderClaimedInWindow: string | undefined;
268
267
  let enabled = true;
268
+
269
+ /** Persist the context_window hint as a visible message (lands in history and the TUI), Codex-style. */
270
+ const persistHint = (ctx: ExtensionContext) => {
271
+ pi.sendMessage({ customType: HINT_TYPE, content: contextWindowHint(ctx), display: true }, { triggerTurn: false });
272
+ };
273
+
274
+ pi.on("session_start", (_event, ctx) => {
275
+ if (!enabled) return;
276
+ persistHint(ctx);
277
+ });
269
278
  const saveNote = (op: NoteOperation) => {
270
279
  // pi.appendEntry writes a custom SessionManager entry. Custom entries are persistent but excluded from LLM context.
271
280
  // ExtensionContext deliberately exposes only a readonly SessionManager, so this is the public extension write path.
@@ -409,27 +418,20 @@ export default function piContext(pi: ExtensionAPI) {
409
418
 
410
419
  pi.on("context", (event, ctx) => {
411
420
  if (!enabled) return undefined;
412
- // Rebuilt per request, so no state diffing is needed; identical to Codex's
413
- // context_window developer fragment rendered into each model call.
414
- const userText = (text: string) => ({
421
+ // Transient while below the threshold: Codex's reminder persists in history and
422
+ // stays visible, so continuous low-budget visibility is the effective parity.
423
+ // Appended (not prepended) with static text: the cached prefix stays untouched,
424
+ // crossing the threshold only adds one stable suffix segment.
425
+ const usage = ctx.getContextUsage();
426
+ if (!usage || usage.tokens === null) return undefined;
427
+ const remaining = Math.max(0, usage.contextWindow - usage.tokens);
428
+ if (remaining > REMINDER_THRESHOLD_TOKENS) return undefined;
429
+ const guidance = {
415
430
  role: "user" as const,
416
- content: [{ type: "text" as const, text }],
431
+ content: [{ type: "text" as const, text: tokenBudgetGuidance() }],
417
432
  timestamp: Date.now(),
418
- });
419
- const injected = [userText(contextWindowHint(ctx))];
420
-
421
- // Codex token_budget.maybe_record parity: below the threshold, claim the
422
- // reminder once per context window; a new window makes it eligible again.
423
- const usage = ctx.getContextUsage();
424
- if (usage && usage.tokens !== null) {
425
- const remaining = Math.max(0, usage.contextWindow - usage.tokens);
426
- const windowId = currentWindowId(ctx);
427
- if (remaining <= REMINDER_THRESHOLD_TOKENS && reminderClaimedInWindow !== windowId) {
428
- reminderClaimedInWindow = windowId;
429
- injected.push(userText(tokenBudgetGuidance(remaining)));
430
- }
431
- }
432
- return { messages: [...injected, ...event.messages] };
433
+ };
434
+ return { messages: [...event.messages, guidance] };
433
435
  });
434
436
 
435
437
  pi.registerTool(defineTool({
@@ -480,13 +482,14 @@ export default function piContext(pi: ExtensionAPI) {
480
482
  }
481
483
  });
482
484
 
483
- pi.on("session_compact", (event) => {
485
+ pi.on("session_compact", (event, ctx) => {
484
486
  if (!enabled) return;
485
487
  // Overflow retry is already continued once by Pi core. Sending another turn would duplicate it.
486
488
  if (event.willRetry) return;
487
489
  if (rollover !== "compacting") return;
488
490
  rollover = "continued";
489
491
  pi.appendEntry(STATE_TYPE, { version: 1, lastResetEntryId: event.compactionEntry.id });
492
+ persistHint(ctx);
490
493
  pi.sendMessage({ customType: CONTINUATION_TYPE, content: CONTINUATION, display: false }, { triggerTurn: true });
491
494
  });
492
495
 
@@ -495,4 +498,4 @@ export default function piContext(pi: ExtensionAPI) {
495
498
  });
496
499
  }
497
500
 
498
- 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 };
501
+ 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 };