@astrosheep/pi-context 0.4.1 → 0.6.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.
- package/README.md +3 -2
- package/package.json +1 -1
- package/src/index.ts +60 -12
package/README.md
CHANGED
|
@@ -20,8 +20,9 @@ The extension composes Pi's public `session_before_compact` / `session_compact`
|
|
|
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
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** —
|
|
24
|
-
- **
|
|
23
|
+
- **Low-budget guidance** — when remaining context first drops to 16,000 tokens or below, a `<context_window_guidance>` reminder is **persisted once per window** into history (TUI-visible, no extra turn; `sendMessage` safely defers to end of turn mid-stream). The in-flight request additionally gets one transient tail copy so the model sees it immediately. Text is static and appended, so the provider prefix cache survives. 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
|
+
- **Auto-compact fallback** — Codex `auto_compact_fallback_prompt` parity, adapted to Pi's trigger points. On the first proactive `threshold` compaction in a window (post-run, agent still streaming), the extension cancels compaction once and steers in a note-taking turn ("write durable state with `notes_write_file` now"); the next threshold trigger performs the real reset and auto-continues, like Codex's mid-turn rollover. The pre-prompt threshold path (idle — cancelling would race the user prompt) and `overflow` recovery (cancelling would abandon Pi's one-shot retry) reset immediately without a fallback turn.
|
|
25
|
+
- **Runtime toggle** — `/pi-context off` disables hint injection, guidance, fallback turns, 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
26
|
- **History tools** — the model searches pre-reset conversation with case-sensitive literal substring search, exactly like Codex's `history.*` namespace.
|
|
26
27
|
- **Notes tools** — persistent, session-scoped virtual files that survive window resets.
|
|
27
28
|
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -5,6 +5,8 @@ import { defineTool, type ExtensionAPI, type ExtensionContext } from "@earendil-
|
|
|
5
5
|
const STATE_TYPE = "pi-context/state";
|
|
6
6
|
const NOTE_TYPE = "pi-context/note";
|
|
7
7
|
const HINT_TYPE = "pi-context/hint";
|
|
8
|
+
const GUIDANCE_TYPE = "pi-context/guidance";
|
|
9
|
+
const FALLBACK_TYPE = "pi-context/fallback";
|
|
8
10
|
const RESET_MARKER_TYPE = "pi-context/reset-marker";
|
|
9
11
|
const CONTINUATION_TYPE = "pi-context/continuation";
|
|
10
12
|
const MAX_NOTE_BYTES = 1_000_000;
|
|
@@ -16,6 +18,10 @@ const REMINDER_THRESHOLD_TOKENS = 16_000;
|
|
|
16
18
|
const RESET_SUMMARY = "Context window reset. Prior session entries remain available only through the pi-context history tools.";
|
|
17
19
|
const CONTINUATION = "This is a fresh context window. Recover only the details needed to continue with history_* and notes_*; then continue the task.";
|
|
18
20
|
|
|
21
|
+
/** Codex auto_compact_fallback_prompt parity: one note-taking chance before an automatic reset. */
|
|
22
|
+
const FALLBACK_PROMPT =
|
|
23
|
+
"Context limit reached. This window is about to be reset. Write durable state with notes_write_file now: task state, decisions, open issues, next steps. Do not start new work. After this turn the window resets automatically; old conversation stays searchable through the history_* tools.";
|
|
24
|
+
|
|
19
25
|
type NoteFile = { text: string; createdAt: number; updatedAt: number };
|
|
20
26
|
type NoteOperation = {
|
|
21
27
|
op: "write" | "append";
|
|
@@ -236,14 +242,24 @@ export function contextWindowHint(ctx: ExtensionContext): string {
|
|
|
236
242
|
}
|
|
237
243
|
|
|
238
244
|
/**
|
|
239
|
-
* Codex-equivalent low-budget reminder. Static by design:
|
|
240
|
-
*
|
|
241
|
-
* the provider's prefix cache on each call. The exact figure is one tool call away.
|
|
245
|
+
* Codex-equivalent low-budget reminder. Static by design: a stale token count in a
|
|
246
|
+
* persisted message would mislead later turns; the exact figure is one tool call away.
|
|
242
247
|
*/
|
|
243
248
|
function tokenBudgetGuidance(): string {
|
|
244
249
|
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
250
|
}
|
|
246
251
|
|
|
252
|
+
/** Cheap current-window lookup: scan the branch tail for the latest compaction entry. */
|
|
253
|
+
function currentWindowId(ctx: ExtensionContext): string {
|
|
254
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
255
|
+
const branch = ctx.sessionManager.getBranch();
|
|
256
|
+
for (let i = branch.length - 1; i >= 0; i--) {
|
|
257
|
+
const entry = branch[i];
|
|
258
|
+
if (entry?.type === "compaction") return `pcw:${sessionId}:${entry.id}`;
|
|
259
|
+
}
|
|
260
|
+
return `pcw:${sessionId}:root`;
|
|
261
|
+
}
|
|
262
|
+
|
|
247
263
|
function lineRange(text: string, startValue: unknown, stopValue: unknown) {
|
|
248
264
|
const lines = text.split("\n");
|
|
249
265
|
const resolve = (value: unknown, fallback: number) => {
|
|
@@ -265,6 +281,9 @@ const role = Type.Union([Type.Literal("user"), Type.Literal("assistant"), Type.L
|
|
|
265
281
|
export default function piContext(pi: ExtensionAPI) {
|
|
266
282
|
let rollover: "idle" | "requested" | "compacting" | "continued" = "idle";
|
|
267
283
|
let enabled = true;
|
|
284
|
+
let guidancePersistedInWindow: string | undefined;
|
|
285
|
+
let fallbackSentInWindow: string | undefined;
|
|
286
|
+
let continueAfterFallback = false;
|
|
268
287
|
|
|
269
288
|
/** Persist the context_window hint as a visible message (lands in history and the TUI), Codex-style. */
|
|
270
289
|
const persistHint = (ctx: ExtensionContext) => {
|
|
@@ -418,17 +437,23 @@ export default function piContext(pi: ExtensionAPI) {
|
|
|
418
437
|
|
|
419
438
|
pi.on("context", (event, ctx) => {
|
|
420
439
|
if (!enabled) return undefined;
|
|
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
440
|
const usage = ctx.getContextUsage();
|
|
426
441
|
if (!usage || usage.tokens === null) return undefined;
|
|
427
442
|
const remaining = Math.max(0, usage.contextWindow - usage.tokens);
|
|
428
443
|
if (remaining > REMINDER_THRESHOLD_TOKENS) return undefined;
|
|
444
|
+
const windowId = currentWindowId(ctx);
|
|
445
|
+
if (guidancePersistedInWindow === windowId) return undefined;
|
|
446
|
+
guidancePersistedInWindow = windowId;
|
|
447
|
+
// Persist once per window, like the hint. sendMessage defers safely to end of
|
|
448
|
+
// turn while streaming (sendCustomMessage queues instead of splitting a tool
|
|
449
|
+
// call/result pair), so from the next turn on the guidance lives in history
|
|
450
|
+
// and the TUI. A transient tail copy covers the in-flight request; it is
|
|
451
|
+
// appended, static, and one-shot, so the cached prefix survives.
|
|
452
|
+
const text = tokenBudgetGuidance();
|
|
453
|
+
pi.sendMessage({ customType: GUIDANCE_TYPE, content: text, display: true }, { triggerTurn: false });
|
|
429
454
|
const guidance = {
|
|
430
455
|
role: "user" as const,
|
|
431
|
-
content: [{ type: "text" as const, text
|
|
456
|
+
content: [{ type: "text" as const, text }],
|
|
432
457
|
timestamp: Date.now(),
|
|
433
458
|
};
|
|
434
459
|
return { messages: [...event.messages, guidance] };
|
|
@@ -472,6 +497,22 @@ export default function piContext(pi: ExtensionAPI) {
|
|
|
472
497
|
if (!enabled) return undefined; // Default Pi compaction applies; keepRecentTokens is honored again.
|
|
473
498
|
// Never let an aborted or failed custom reset fall through to Pi's default summary.
|
|
474
499
|
if (event.signal.aborted) return { cancel: true };
|
|
500
|
+
// Codex auto_compact_fallback_prompt parity, adapted to Pi's trigger points:
|
|
501
|
+
// - threshold, post-run (agent still streaming): cancel once per window and steer a
|
|
502
|
+
// note-taking turn in; _runAutoCompaction then returns hasQueuedMessages() and the
|
|
503
|
+
// post-run loop delivers it via agent.continue(). Safe, intended path.
|
|
504
|
+
// - threshold, pre-prompt (idle): cancelling still sends the user prompt with an
|
|
505
|
+
// over-threshold context, and sendMessage would race _runAgentPrompt. Reset instead.
|
|
506
|
+
// - overflow: never cancel; that would abandon Pi's one-shot compact-and-retry recovery.
|
|
507
|
+
if (event.reason === "threshold" && !ctx.isIdle()) {
|
|
508
|
+
const windowId = currentWindowId(ctx);
|
|
509
|
+
if (fallbackSentInWindow !== windowId) {
|
|
510
|
+
fallbackSentInWindow = windowId;
|
|
511
|
+
continueAfterFallback = true;
|
|
512
|
+
pi.sendMessage({ customType: FALLBACK_TYPE, content: FALLBACK_PROMPT, display: true }, { triggerTurn: true });
|
|
513
|
+
return { cancel: true };
|
|
514
|
+
}
|
|
515
|
+
}
|
|
475
516
|
try {
|
|
476
517
|
pi.appendEntry(RESET_MARKER_TYPE, { version: 1, reason: event.reason, requested: rollover === "compacting" });
|
|
477
518
|
const markerId = ctx.sessionManager.getLeafId();
|
|
@@ -483,11 +524,18 @@ export default function piContext(pi: ExtensionAPI) {
|
|
|
483
524
|
});
|
|
484
525
|
|
|
485
526
|
pi.on("session_compact", (event, ctx) => {
|
|
486
|
-
if (!enabled)
|
|
527
|
+
if (!enabled) {
|
|
528
|
+
continueAfterFallback = false;
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
487
531
|
// Overflow retry is already continued once by Pi core. Sending another turn would duplicate it.
|
|
488
532
|
if (event.willRetry) return;
|
|
489
|
-
|
|
490
|
-
|
|
533
|
+
// Continue after our own rollover, and after a fallback reset (Codex rolls over
|
|
534
|
+
// mid-turn and keeps going). A user's manual /compact gets no continuation.
|
|
535
|
+
const shouldContinue = rollover === "compacting" || continueAfterFallback;
|
|
536
|
+
continueAfterFallback = false;
|
|
537
|
+
if (rollover === "compacting") rollover = "continued";
|
|
538
|
+
if (!shouldContinue) return;
|
|
491
539
|
pi.appendEntry(STATE_TYPE, { version: 1, lastResetEntryId: event.compactionEntry.id });
|
|
492
540
|
persistHint(ctx);
|
|
493
541
|
pi.sendMessage({ customType: CONTINUATION_TYPE, content: CONTINUATION, display: false }, { triggerTurn: true });
|
|
@@ -498,4 +546,4 @@ export default function piContext(pi: ExtensionAPI) {
|
|
|
498
546
|
});
|
|
499
547
|
}
|
|
500
548
|
|
|
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 };
|
|
549
|
+
export const internal = { MAX_NOTE_BYTES, NOTE_TYPE, HINT_TYPE, GUIDANCE_TYPE, FALLBACK_TYPE, FALLBACK_PROMPT, RESET_MARKER_TYPE, RESET_SUMMARY, CONTINUATION, CONTEXT_WINDOW_OPEN_TAG, GUIDANCE_OPEN_TAG, REMINDER_THRESHOLD_TOKENS, lineRange, assertVirtualPath };
|