@astrosheep/pi-context 0.9.0 → 0.9.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/README.md CHANGED
@@ -20,7 +20,7 @@ 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>` boot block** — the head of every fresh window. For a reset it IS the summary returned from `session_before_compact` (position 0, persisted, no extra message); for the root window `session_start` persists it once as a visible custom message. It carries the agent name and first/current/previous window IDs, the recent-notes index, and a `<context_window_protocol>` teaching block. The notes index lists up to three most-recent notes, each with its `X lines, Y UTF-8 bytes` metadata plus a local-time ISO 8601 `updated` timestamp (explicit UTC offset, never `Z`) and an inline preview. A note of 200 Unicode characters or fewer is shown whole; a longer one shows its first 120 and last 80 Unicode characters joined by an ellipsis, so the two ends never overlap and no text is repeated. The notes index is a window-open snapshot with the same frozen-at-write semantics as the reminder count. Nothing is injected transiently per request: the boot block is static once-per-window content, so the head of the window stays cache-stable. Codex diverges here — its `<context_window>` block carries only the agent path and window IDs, while the notes index is our own addition.
23
- - **Low-budget guidance** — when estimated remaining context first drops to the reminder threshold (by default **40,960 tokens**: Pi's default 16,384 `reserveTokens` plus a 24,576 reminder margin; see [Reminder timing](#reminder-timing)), a `<context_window_guidance>` reminder is **persisted once per window** into history (TUI-visible, no extra turn; `sendMessage` safely defers mid-stream). There is deliberately no transient copy: a bridge would make the model meet the same text twice at shifted positions, because history records the persisted copy after the crossing request's assistant reply. The reminder is an early warning, so arriving from the next request on costs nothing and keeps the model's view identical to recorded history. The measured remaining count is frozen into the text at the threshold crossing, so the persisted reminder is a snapshot true at write time; `get_context_remaining` remains the live source for the current figure. The text is appended rather than prepended; existing history is not rewritten.
23
+ - **Low-budget guidance** — when estimated remaining context first drops to the reminder threshold (by default **40,960 tokens**: Pi's default 16,384 `reserveTokens` plus a 24,576 reminder margin; see [Reminder timing](#reminder-timing)), a `<context_window_guidance>` reminder is **persisted once per window** into history (TUI-visible, no extra turn; `sendMessage` safely defers mid-stream). There is deliberately no transient copy: a bridge would make the model meet the same text twice at shifted positions, because history records the persisted copy after the crossing request's assistant reply. The reminder is an early warning, so arriving from the next request on costs nothing and keeps the model's view identical to recorded history. The measured remaining budget excludes `reserveTokens` and is frozen into the text at the threshold crossing, so the persisted reminder is a snapshot true at write time; `get_context_remaining` remains the live source for the current figure. The text is appended rather than prepended; existing history is not rewritten.
24
24
  - **Two-phase automatic fallback** — while Pi is streaming, the first automatic `threshold`/`overflow` crossing of the reserve line does not reset immediately. `session_before_compact` queues the final note-taking instruction with `pi.sendMessage(..., { triggerTurn: true })` — which Pi routes to `agent.steer()` while streaming, so the message is queued synchronously and reaches the model before any pending user input — and returns `{ cancel: true }`. Pi records that as an aborted compaction, spends no summary, and continues the same run with the borrowed turn; no user text or images are copied, intercepted, or replayed, and no `input` handler is registered. Once the borrowed run has finished, `agent_end` arms the real reset and `agent_settled` requests it through `ctx.compact()` for both `threshold` and `overflow`, unless another compaction has already completed. A threshold crossing does not guarantee another automatic check: the agent loop can end without preparing another assistant response. Overflow recovery also has a one-shot guard. Requesting the reset after the run settles avoids issuing it from `agent_end` while Pi is still finishing the run. A phase flag makes the cancel happen at most once per window — it re-arms only after a completed reset starts a fresh window — and a failed reset keeps retrying the real compaction instead of borrowing another turn. The borrow is skipped when the crossing arrives while Pi is idle (the pre-prompt check in `AgentSession.prompt()`, where `triggerTurn` would start a nested run and make the pending `Agent.prompt()` reject); that crossing resets directly. `manual` `/compact` and `new_context` never take the borrowed-turn path. The reminder asks the model to write notes early; if it misses that opportunity, old history remains searchable.
25
25
  - **Runtime toggle** — `/pi-context off` disables the boot block, 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.
26
26
  - **History tools** — the model searches pre-reset conversation with case-sensitive literal substring search, exactly like Codex's `history.*` namespace.
@@ -75,7 +75,7 @@ Unlike Codex, the history tools do not advertise `agent_name`: Pi has no cross-a
75
75
 
76
76
  Two extra controls compose Pi public APIs:
77
77
 
78
- - `get_context_remaining` returns `{ "remaining_tokens": number | null }`. `null` means Pi itself cannot make a reliable estimate (notably immediately after compaction). The low-budget guidance uses the same source and stays silent when the estimate is unknown.
78
+ - `get_context_remaining` returns `{ "remaining_tokens": number | null }`, computed as `max(0, contextWindow - usedTokens - reserveTokens)`: the estimated budget available before Pi's compaction reserve. `null` means Pi itself cannot make a reliable estimate (notably immediately after compaction). The low-budget guidance reports this same reserve-adjusted budget and stays silent when the estimate is unknown. Its trigger still compares physical remaining capacity against `reserveTokens + reminderMarginTokens`, so subtracting reserve from the reported number does not change reminder timing.
79
79
  - `new_context` returns terminal tool output, then waits for Pi's `agent_end`, triggers public `ctx.compact()`, installs a short deterministic reset compaction, and sends exactly one hidden continuation turn after compaction succeeds. Call it by itself in a tool batch. Pi only ends a tool turn when every parallel tool result is terminal, so Pi 0.85.1 cannot force an atomic rollover from the middle of a mixed parallel tool batch.
80
80
 
81
81
  ## Reset behavior and limits
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/pi-context",
3
- "version": "0.9.0",
3
+ "version": "0.9.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
@@ -46,7 +46,7 @@ ${CONTEXT_WINDOW_PROTOCOL_CLOSE_TAG}`;
46
46
  const FALLBACK_PROMPT =
47
47
  "Context budget is almost exhausted. This is the final fallback turn before the window resets automatically. Write task state, decisions, open issues, and next steps with notes_write_file now. Do not start new work; old conversation remains searchable through history_*.";
48
48
 
49
- type ResolvedThresholds = { reminder: number };
49
+ type ResolvedThresholds = { reminder: number; reserve: number };
50
50
  type PiContextMargins = { reminderMarginTokens: unknown };
51
51
 
52
52
  type NoteFile = { text: string; createdAt: number; updatedAt: number };
@@ -399,7 +399,7 @@ export function deriveThresholds(reserveTokens: number, margins: PiContextMargin
399
399
  reminderMargin = DEFAULT_REMINDER_MARGIN_TOKENS;
400
400
  } else reminderMargin = parsed;
401
401
  }
402
- return { thresholds: { reminder: reserveTokens + reminderMargin }, warnings };
402
+ return { thresholds: { reminder: reserveTokens + reminderMargin, reserve: reserveTokens }, warnings };
403
403
  }
404
404
 
405
405
  export default function piContext(pi: ExtensionAPI) {
@@ -433,7 +433,7 @@ export default function piContext(pi: ExtensionAPI) {
433
433
  thresholds = derived.thresholds;
434
434
  } catch (error) {
435
435
  ctx.ui.notify(`pi-context: could not read settings; using defaults (${String(error)}).`, "warning");
436
- thresholds = { reminder: DEFAULT_RESERVE_TOKENS + DEFAULT_REMINDER_MARGIN_TOKENS };
436
+ thresholds = { reminder: DEFAULT_RESERVE_TOKENS + DEFAULT_REMINDER_MARGIN_TOKENS, reserve: DEFAULT_RESERVE_TOKENS };
437
437
  }
438
438
  return thresholds;
439
439
  };
@@ -597,7 +597,8 @@ export default function piContext(pi: ExtensionAPI) {
597
597
  if (usage && usage.tokens !== null) {
598
598
  const remaining = Math.max(0, usage.contextWindow - usage.tokens);
599
599
  const windowId = currentWindowId(ctx);
600
- if (remaining <= resolveThresholds(ctx).reminder && guidancePersistedInWindow !== windowId) {
600
+ const { reminder, reserve } = resolveThresholds(ctx);
601
+ if (remaining <= reminder && guidancePersistedInWindow !== windowId) {
601
602
  guidancePersistedInWindow = windowId;
602
603
  // Persist once per window — no transient copy. A transient bridge would
603
604
  // cover the crossing request, but history would record the reminder after
@@ -607,7 +608,7 @@ export default function piContext(pi: ExtensionAPI) {
607
608
  // on (sendMessage defers safely to end of turn while streaming, queueing
608
609
  // instead of splitting a tool call/result pair) costs nothing, and the
609
610
  // model's view stays identical to recorded history, Codex-style.
610
- pi.sendMessage({ customType: GUIDANCE_TYPE, content: tokenBudgetGuidance(remaining), display: true }, { triggerTurn: false });
611
+ pi.sendMessage({ customType: GUIDANCE_TYPE, content: tokenBudgetGuidance(Math.max(0, remaining - reserve)), display: true }, { triggerTurn: false });
611
612
  }
612
613
  }
613
614
  return undefined;
@@ -616,11 +617,11 @@ export default function piContext(pi: ExtensionAPI) {
616
617
  pi.registerTool(defineTool({
617
618
  name: "get_context_remaining",
618
619
  label: "Get context remaining",
619
- description: "Return remaining context tokens when Pi can estimate them, otherwise null.",
620
+ description: "Return estimated context tokens available before the compaction reserve, clamped to zero; null when Pi cannot estimate usage.",
620
621
  parameters: Type.Object({}, { additionalProperties: false }),
621
622
  async execute(_id, _params, _signal, _update, ctx) {
622
623
  const usage = ctx.getContextUsage();
623
- const remaining = usage?.tokens === null || usage === undefined ? null : Math.max(0, usage.contextWindow - usage.tokens);
624
+ const remaining = usage?.tokens === null || usage === undefined ? null : Math.max(0, usage.contextWindow - usage.tokens - resolveThresholds(ctx).reserve);
624
625
  return output({ remaining_tokens: remaining });
625
626
  },
626
627
  }));