@astrosheep/pi-context 0.25.2 → 0.26.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 (112) hide show
  1. package/README.md +88 -7
  2. package/dist/build-info.json +2 -2
  3. package/dist/extension.js +613 -367
  4. package/dist/src/context/boot.d.ts +24 -0
  5. package/dist/src/context/boot.js +33 -24
  6. package/dist/src/context/budget.d.ts +9 -0
  7. package/dist/src/context/budget.js +16 -12
  8. package/dist/src/context/context-window.d.ts +41 -0
  9. package/dist/src/context/context-window.js +16 -1
  10. package/dist/src/context/prompts.d.ts +20 -0
  11. package/dist/src/context/prompts.js +1 -1
  12. package/dist/src/context/reset-artifacts.d.ts +26 -0
  13. package/dist/src/context/reset-artifacts.js +18 -17
  14. package/dist/src/context/reset-lifecycle.d.ts +89 -0
  15. package/dist/src/context/reset-lifecycle.js +103 -75
  16. package/dist/src/context/runtime.d.ts +3 -0
  17. package/dist/src/context/runtime.js +53 -21
  18. package/dist/src/context/thresholds.d.ts +33 -0
  19. package/dist/src/context/thresholds.js +1 -1
  20. package/dist/src/dream/cli.d.ts +10 -0
  21. package/dist/src/dream/cli.js +1 -1
  22. package/dist/src/dream/doctor.d.ts +2 -0
  23. package/dist/src/dream/doctor.js +6 -2
  24. package/dist/src/dream/gates.d.ts +10 -0
  25. package/dist/src/dream/git.d.ts +21 -0
  26. package/dist/src/dream/lock.d.ts +31 -0
  27. package/dist/src/dream/runner.d.ts +30 -0
  28. package/dist/src/dream/settings.d.ts +16 -0
  29. package/dist/src/history/history-tools.d.ts +2 -0
  30. package/dist/src/history/history.d.ts +57 -0
  31. package/dist/src/index.d.ts +39 -0
  32. package/dist/src/index.js +4 -4
  33. package/dist/src/notes/address.d.ts +26 -0
  34. package/dist/src/notes/address.js +8 -14
  35. package/dist/src/notes/constants.d.ts +3 -0
  36. package/dist/src/notes/constants.js +3 -0
  37. package/dist/src/notes/context.d.ts +10 -0
  38. package/dist/src/notes/context.js +33 -0
  39. package/dist/src/notes/frontmatter.d.ts +46 -0
  40. package/dist/src/notes/frontmatter.js +10 -5
  41. package/dist/src/notes/index.d.ts +4 -0
  42. package/dist/src/notes/index.js +2 -0
  43. package/dist/src/notes/paths.d.ts +21 -0
  44. package/dist/src/notes/paths.js +72 -76
  45. package/dist/src/notes/store.d.ts +94 -0
  46. package/dist/src/notes/store.js +298 -242
  47. package/dist/src/pi/notes/adapter.d.ts +12 -0
  48. package/dist/src/pi/notes/adapter.js +39 -0
  49. package/dist/src/pi/notes/session-replay.d.ts +16 -0
  50. package/dist/src/{notes → pi/notes}/session-replay.js +2 -2
  51. package/dist/src/pi/notes/snapshot.d.ts +33 -0
  52. package/dist/src/{notes/notes-snapshot.js → pi/notes/snapshot.js} +11 -3
  53. package/dist/src/pi/notes/tools.d.ts +2 -0
  54. package/dist/src/{notes → pi/notes}/tools.js +24 -21
  55. package/dist/src/protocol.d.ts +41 -0
  56. package/dist/src/protocol.js +4 -6
  57. package/dist/src/session-reader.d.ts +5 -0
  58. package/dist/src/settings.d.ts +6 -0
  59. package/dist/src/tool-output.d.ts +101 -0
  60. package/dist/src/tool-schema.d.ts +17 -0
  61. package/dist/test/agent-loop.test.d.ts +1 -0
  62. package/dist/test/agent-loop.test.js +309 -10
  63. package/dist/test/boot.integration.test.d.ts +1 -0
  64. package/dist/test/boot.integration.test.js +55 -29
  65. package/dist/test/budget-settings.integration.test.d.ts +1 -0
  66. package/dist/test/budget-settings.integration.test.js +8 -7
  67. package/dist/test/doctor.test.d.ts +1 -0
  68. package/dist/test/doctor.test.js +10 -2
  69. package/dist/test/dream-skill.test.d.ts +1 -0
  70. package/dist/test/dream-skill.test.js +69 -0
  71. package/dist/test/dream.test.d.ts +1 -0
  72. package/dist/test/helpers/extension.d.ts +115 -0
  73. package/dist/test/helpers/extension.js +6 -6
  74. package/dist/test/helpers/notes.d.ts +6 -0
  75. package/dist/test/helpers/notes.js +13 -0
  76. package/dist/test/history.integration.test.d.ts +1 -0
  77. package/dist/test/notes-library.test.d.ts +1 -0
  78. package/dist/test/notes-library.test.js +111 -0
  79. package/dist/test/notes.integration.test.d.ts +1 -0
  80. package/dist/test/notes.integration.test.js +22 -24
  81. package/dist/test/notes.test.d.ts +1 -0
  82. package/dist/test/notes.test.js +137 -7
  83. package/dist/test/reset-lifecycle.test.d.ts +1 -0
  84. package/dist/test/reset-lifecycle.test.js +142 -85
  85. package/docs/architecture.md +8 -8
  86. package/docs/reset-lifecycle.md +63 -79
  87. package/package.json +35 -2
  88. package/playbook.md +33 -32
  89. package/skills/dream/SKILL.md +12 -0
  90. package/src/context/boot.ts +44 -25
  91. package/src/context/budget.ts +19 -11
  92. package/src/context/context-window.ts +16 -1
  93. package/src/context/prompts.ts +2 -2
  94. package/src/context/reset-artifacts.ts +26 -24
  95. package/src/context/reset-lifecycle.ts +117 -111
  96. package/src/context/runtime.ts +50 -22
  97. package/src/context/thresholds.ts +1 -1
  98. package/src/dream/cli.ts +1 -1
  99. package/src/dream/doctor.ts +5 -2
  100. package/src/index.ts +4 -4
  101. package/src/notes/address.ts +9 -15
  102. package/src/notes/constants.ts +3 -0
  103. package/src/notes/context.ts +40 -0
  104. package/src/notes/frontmatter.ts +18 -12
  105. package/src/notes/index.ts +22 -0
  106. package/src/notes/paths.ts +64 -78
  107. package/src/notes/store.ts +308 -244
  108. package/src/pi/notes/adapter.ts +44 -0
  109. package/src/{notes → pi/notes}/session-replay.ts +3 -3
  110. package/src/{notes/notes-snapshot.ts → pi/notes/snapshot.ts} +13 -4
  111. package/src/{notes → pi/notes}/tools.ts +25 -23
  112. package/src/protocol.ts +5 -6
@@ -29,30 +29,30 @@ Dependencies flow from the composition root and tool adapters to projections and
29
29
 
30
30
  ## State and persistence
31
31
 
32
- `turn_end` has one composer in `reset-lifecycle.ts`: it accepts the incoming drafts, drains the budget instance's staged guidance/warning drafts, and only then appends reset drafts. Reset requests are committed after the complete tool batch with `continue: true`, so Pi owns queue scheduling. Repeated `wipe_memory` requests in one batch deduplicate; a later window may still request another reset. Aborts and reset-construction failures preserve already-built drafts without manufacturing a continuation. See [reset lifecycle](reset-lifecycle.md).
32
+ `reset-lifecycle.ts` is the sole `turn_end` composer: it accepts incoming drafts, drains budget-owned guidance/warning drafts, and appends a reset boundary only for an explicit tool request or the hard-reserve safety path. Manual and budget close-outs stay armed across note/tool turns and fall back from `agent_before_settle` only after a successful stop and queued work are drained. Repeated requests for the same window deduplicate. See [reset lifecycle](reset-lifecycle.md).
33
33
 
34
- The durable boundary is one `pi-context/reset-marker` custom entry with `{ windowId: string }`, followed by one hidden `pi-context/boot` custom message with `details.windowId` equal to the marker identity. The marker is the only window boundary. `context/context-window.ts` owns the marker predicate, active-branch scan, root/current IDs, and per-window message lookup; `history/history.ts` consumes those identity primitives while projecting entries. The scan never uses a global entry tail. Native compaction and branch-summary entries remain history items in the current window, so the old compaction-entry identity is not a window identity.
34
+ A new reset is a native empty-summary compaction checkpoint configured to retain none, followed by one `pi-context/reset-marker` custom entry with `{ windowId: string }`, a hidden `pi-context/boot` whose `details.windowId` matches, and a hidden continuation. The checkpoint is the canonical-history cut; the marker is the durable logical window identity. Raw session entries remain available to history tools. Legacy marker-only branches keep a narrow projection fallback. `context/context-window.ts` owns marker/checkpoint validation, active-branch scan, root/current IDs, and per-window message lookup; `history/history.ts` consumes those identity primitives while projecting entries. The scan never uses a global entry tail.
35
35
 
36
36
  The runtime in `context/runtime.ts` performs final context projection by selecting the active boot through `details.windowId` and folding only the dropped system prefix through Pi's `getCurrentSystemMessage`. Later prompt patches and new messages stay in order. A missing boot aborts the hook with a safe head and notice rather than silently sending raw history. Startup/tree handling repairs only a genuinely incomplete marker tail: a missing boot with no later raw message, custom message, compaction, branch summary, or authoritative raw boot. If later work exists, boot creation is refused and `/wipe-memory` is the explicit recovery path; it does not parse or migrate the legacy reset-v2 protocol.
37
37
 
38
38
  Boot and reminder deduplication inspect the current branch-local window. Reloading JSONL therefore does not duplicate messages, while navigation to a sibling branch cannot inherit another branch's window state. A fork/clone receives a new session ID while copying its selected path, so startup must also verify that a root boot's `details.windowId` matches the new `rootWindowId(sessionId)` before treating it as present.
39
39
 
40
- Startup and idle manual resets use `pi.sendMessage(..., { triggerTurn: false })` for the boot. Pi appends it to the session and refreshes context without initiating a model request; during streaming the same call is deferred until the completed tool batch. This is session persistence, not a promise of immediate disk durability: Pi 0.87 defers a new session file until its first assistant message, and the extension API reports send failures through extension errors rather than an awaitable result. Running resets therefore use boundary drafts for ordering and continuation, not `sendMessage`. Drafts are validated together but disk writes are not transactional.
40
+ Manual `/wipe-memory` first waits for idle, then records the same hidden warning used by budget-triggered close-out and starts an ordinary model turn. The agent can checkpoint notes through multiple turns; an explicit `wipe_memory` commits at `turn_end`, while a successful stop without the tool falls back at `agent_before_settle`. Abort/error never counts as successful close-out. The final checkpoint, marker, boot, and continuation are emitted as ordered session-boundary drafts. Pi's retain-none compaction entry persists the canonical cut; the marker/boot preserve pi-context's logical identity and protocol.
41
41
 
42
- Boot is a fixed snapshot for its window, stored as an extension custom message and converted by Pi to a user-role model message. History's `developer` classification records extension authorship, not provider instruction priority. A system-role projection is technically possible through `context_with_system`, but would change the authority of the mixed protocol/MAP content and provider-specific prompt/cache behavior; it is not a prerequisite for persistence. Boot injection is silent, including startup, reload, and boot repair. Only an actual context reset produces `pi-context: memory cleared · <windowId>` through `ctx.ui.notify`, after both marker and boot are present in the session. Manual clear is checked immediately; running resets are checked at the next turn start or final settlement. Uncommitted reset drafts never announce success. Boot and continuation messages retain `display: false`.
42
+ Boot is a fixed snapshot for its window, stored as an extension custom message and converted by Pi to a user-role model message. History's `developer` classification records extension authorship, not provider instruction priority. A system-role projection is technically possible through `context_with_system`, but would change the authority of the mixed protocol/MAP content and provider-specific prompt/cache behavior; it is not a prerequisite for persistence. Boot injection is silent, including startup, reload, and boot repair. Only an actual context reset produces `pi-context: memory cleared · <windowId>` through `ctx.ui.notify`, after the verified checkpoint, marker, boot, and continuation are present in the session. Committed resets are checked at the next turn start or final settlement. Uncommitted reset drafts never announce success. Boot and continuation messages retain `display: false`.
43
43
 
44
44
  History reads reconstruct the selected session branch on demand without a cache, so branch navigation cannot expose history from a sibling.
45
45
 
46
- The boot notes index in `notes/notes-snapshot.ts` is a closed snapshot: the current session, project, human, agent, and model homes are each loaded at most once while constructing a boot. `context/prompts.ts` then renders that explicit snapshot without reading the filesystem or consulting the clock. MAP bodies and pocket metadata are derived from the same snapshot, so a boot cannot mix two filesystem reads. A missing home (`ENOENT`) is normal. A real read failure omits only that home's index, preserves healthy homes, adds a model-facing `notes_list` recovery notice, and notifies the human once for that window. The window identity, reset/protocol text, and lifecycle boundary are still constructed through the normal runtime path; note reads never mutate files or create fallback state.
46
+ The boot notes index in `pi/notes/snapshot.ts` is a closed snapshot: the current session, project, human, agent, and model homes are each loaded at most once while constructing a boot. `context/prompts.ts` then renders that explicit snapshot without reading the filesystem or consulting the clock. MAP bodies and pocket metadata are derived from the same snapshot, so a boot cannot mix two filesystem reads. Note storage and home traversal use `node:fs/promises`; same-file operations queue by absolute physical filename across store instances in this process, without promising symlink/case-alias or cross-process locking. Known persisted metadata is camelCase, and a known legacy snake_case key refuses use until the root-coordinated manual migration; startup does not migrate notes. A missing home (`ENOENT`) is normal. A real read failure omits only that home's index, preserves healthy homes, adds a model-facing `notes_list` recovery notice, and notifies the human once for that window. Boot/reset construction captures session, agent, and model identity before awaiting the snapshot, then checks lifecycle generation, active window, enabled state, and abort status before sending or returning artifacts; stale completions cannot commit into a switched or shut-down session. Note reads never mutate files or create fallback state.
47
47
 
48
48
  `notes/session-replay.ts` accepts only supported operations, safe virtual paths, representable timestamps and results within the UTF-8 size limit. Invalid operations are ignored; they cannot replace a valid note. Notes remain in their filesystem-backed homes, unchanged by session branch navigation.
49
49
 
50
- The `/wipe-memory` command waits for idle, appends the marker and boot with Pi's public `appendEntry`/`sendMessage` APIs, and never calls a model. While enabled, `/compact` is cancelled with an actionable `/wipe-memory` notice. Disabling pi-context stops new automatic resets, but an existing marker still excludes earlier history and native compaction is still cancelled on that marked branch; a fresh root may use native Pi semantics. Threshold and warning accounting use active-window provider usage rather than pre-reset global usage. Budget policy and staged prompts are instance-owned, so concurrent sessions cannot share reserve/enablement or uncommitted drafts; default file-backed policy is cached per instance, while injected policy is resolved live on each decision and model/session transitions reset the diagnostic lifecycle.
50
+ The `/wipe-memory` command captures the current window before waiting for idle, refuses a duplicate if the wait crossed into a new window, then records a hidden warning and starts a model close-out turn. While enabled, `/compact` is cancelled with an actionable `/wipe-memory` notice. Disabling pi-context stops new automatic resets, but an existing native checkpoint/marker still excludes earlier history and native compaction is still cancelled on that marked branch; a fresh root may use native Pi semantics. Threshold and warning accounting use active-window provider usage rather than pre-reset global usage. Budget policy and staged prompts are instance-owned, so concurrent sessions cannot share reserve/enablement or uncommitted drafts; default file-backed policy is cached per instance, while injected policy is resolved live on each decision and model/session transitions reset the diagnostic lifecycle.
51
51
 
52
52
  ## Evidence and limits
53
53
 
54
54
  The retained integration tests use real SessionManager and SettingsManager instances. They check reset-window history retention, bounded history/notes reads with resumable cursors, notes-home isolation, and settings precedence. The suite is a representative regression set; the standalone coherence, pagination property, history, and threshold suites were removed during test reduction.
55
55
 
56
- Scripted SDK tests execute the real Pi agent loop with no model network request. They inspect provider contexts and durable entries for reset boundaries, mixed-tool completion, queued steering/follow-up delivery, overflow recovery, and settings authority. Smaller dream tests retain lock exclusion/ownership, jailed writes, CLI audit/report failure propagation, and read-only doctor behavior. Removing duplicate scenarios and edge-case matrices reduces coverage; the remaining tests do not establish every malformed-input case, branch interleaving, external-provider behavior, or filesystem failure mode.
56
+ Scripted SDK tests execute the real Pi agent loop with no model/network request. They inspect provider contexts and durable entries for manual/budget close-out, multi-turn notes, explicit and normal-stop commits, abort/error cleanup, same-window command deduplication, queued steering/follow-up delivery, retain-none checkpoints across two resets and disk reopen, bounded overflow recovery, and settings authority. Smaller dream tests retain lock exclusion/ownership, jailed writes, CLI audit/report failure propagation, and read-only doctor behavior. Removing duplicate scenarios and edge-case matrices reduces coverage; the remaining tests do not establish every malformed-input case, branch interleaving, external-provider behavior, or filesystem failure mode.
57
57
 
58
- Mixed tool batches finish before the marker/boot boundary. Queued steering/follow-up messages are delivered exactly once in the new window; they are neither dropped nor replayed. Runtime overflow recovery is bounded to one reset/retry per failure chain, while ordinary retryable provider errors remain Pi-owned. When either the source or destination branch has a reset marker, `/tree` navigation still succeeds but its generated summary is replaced by an empty summary plus a notice; raw history and branch selection remain available. If neither branch has a marker, Pi's native tree summary is retained.
58
+ Mixed tool batches finish before a direct tool reset boundary. Manual/budget close-outs let queued steering/follow-up work run once in the old window before fallback; direct tool resets let Pi schedule queued work across the new boundary. Scripted loop tests check that these messages are neither dropped nor replayed. Runtime overflow recovery is bounded to one reset/retry per failure chain, while ordinary retryable provider errors remain Pi-owned. When either the source or destination branch has a reset marker, `/tree` navigation still succeeds but its generated summary is replaced by an empty summary plus a notice; raw history and branch selection remain available. If neither branch has a marker, Pi's native tree summary is retained.
@@ -1,83 +1,67 @@
1
1
  # Reset lifecycle
2
2
 
3
- `src/context/reset-lifecycle.ts` owns reset requests, turn-end batching, recovery, and continuation. It is the sole `turn_end` composer: incoming drafts, budget drafts, and reset drafts are ordered here. `src/context/budget.ts` owns the default-path instance-local policy cache, resolves injected policy live, stages guidance/warning drafts, and keeps the final warning text in the budget/protocol path; `src/context/thresholds.ts` only reads settings and derives values, using the shared merge in `src/settings.ts`. `src/index.ts` is the thin public entrypoint; `src/context/runtime.ts` composes the runtime hooks and constructs marker/boot boundaries. `src/notes/notes-snapshot.ts` acquires the notes snapshot, while `src/context/prompts.ts` only renders the explicit boot data and low-budget reminder. Projections and tools have separate modules described in [Architecture](architecture.md).
3
+ `src/context/reset-lifecycle.ts` owns the close-out request phases, turn-end batching, bounded overflow recovery, and reset scheduling. `src/context/budget.ts` owns budget policy and stages the early reminder/final warning; `src/context/reset-artifacts.ts` builds the checkpoint, marker, boot, and continuation drafts; `src/context/context-window.ts` validates and projects the active window. `src/context/runtime.ts` wires these parts to Pi's public lifecycle hooks and `/wipe-memory` command.
4
4
 
5
- | Event | Transition / owner |
5
+ ## Lifecycle at a glance
6
+
7
+ | Trigger | Behavior |
6
8
  | --- | --- |
7
- | `wipe_memory` | Record an explicit reset request. Repeated calls in one tool batch deduplicate; the tool returns terminal output. |
8
- | `turn_end` | The sole composer drains current-window budget drafts after the event entries, then appends reset drafts: one `pi-context/reset-marker` with `{ windowId }`, one hidden boot message with matching `details.windowId`, and the continuation marker; continue the turn through Pi's public queue. |
9
- | Abort before the boundary | Drop the pending boundary. Never manufacture a continuation for an aborted turn. |
10
- | Threshold / provider overflow | Request the same marker/boot boundary for the active provider window. Actual overflow/length recovery retries at most once per failure chain; ordinary retryable provider errors stay Pi-owned. |
11
- | `/wipe-memory` | Wait for idle, append marker and boot through public session APIs, and do not call a model. |
12
- | `/compact` while enabled | Cancel with an actionable `/wipe-memory` notice so native compaction cannot summarize erased canonical history back into the active window. |
13
- | Startup / tree / partial append | Repair only a marker followed by an otherwise empty metadata tail; refuse hidden/absent boots once later work exists, leaving `/wipe-memory` as the explicit recovery path. Do not interpret legacy reset-v2 details. |
14
- | `/pi-context off` | Stop new automatic resets, but retain the boundary of an existing marker. Marked branches still cancel native compaction; a fresh root may use Pi's native semantics. |
15
-
16
- ## Reset-control state machine
17
-
18
- `registerResetLifecycle` is the effect adapter. `reduceResetControl` in `src/context/reset-lifecycle.ts` is the pure transition surface: given the current state and one lifecycle event it returns the next state and one named effect, performing no writes, policy resolution, or UI work of its own. The adapter captures Pi's events, supplies the guards only in the branches that consult them, then performs the marker/boot/continuation writes, continuation requests, and notices.
19
-
20
- State is a single value with two explicitly typed axes. There are no independently combinable lifecycle booleans.
21
-
22
- | Axis | Value | Meaning |
23
- | --- | --- | --- |
24
- | `request` | `none` | no explicit wipe request pending |
25
- | | `explicit` | one explicit request is pending and will commit at the next `turn_end` |
26
- | `overflow` | `idle` | no overflow failure pending; a future recovery is available |
27
- | | `pending` | active-provider overflow failure pending; recovery still available |
28
- | | `pending-spent` | overflow failure pending, but its one recovery was already spent |
29
- | | `spent` | recovery spent and the failure is no longer pending |
30
-
31
- All eight combinations of the two axes are reachable and each has a defined transition.
32
-
33
- | Event | Guard | Next state | Effect |
34
- | --- | --- | --- | --- |
35
- | `request` | `request=none` | `request=explicit`, overflow unchanged | `requested` |
36
- | `request` | `request=explicit` | unchanged | `already-requested` |
37
- | `turn_end` | turn aborted | `(request=none, overflow=idle)` | `none` |
38
- | `turn_end` | overflow-like and `!queued && enabled && automaticResetEnabled` | `request=none`; overflow `pending`, or `pending-spent` if already spent | `none` |
39
- | `turn_end` | overflow-like and (`queued \|\| !enabled \|\| !automaticResetEnabled`) | `request=none`; overflow `idle`, or `spent` if already spent | `none` |
40
- | `turn_end` | completed, `enabled`, and (`explicit` requested or `thresholdDue`) | `(request=none, overflow=idle)` | `commit-boundary` |
41
- | `turn_end` | completed, `enabled`, but neither trigger | `(request=none, overflow=idle)` | `none` |
42
- | `turn_end` | non-overflow `failed` | `request=none`; overflow unchanged | `none` |
43
- | `turn_end` | `!enabled` | `request=none`; completed clears overflow, error keeps it | `none` |
44
- | `before_settle` | `overflow=pending`, not queued, `enabled`, `automaticResetEnabled`, not aborted | `overflow=spent` | `recover-overflow` |
45
- | `before_settle` | `overflow=pending-spent` | `overflow=spent` | `none` |
46
- | `before_settle` | `overflow` not pending, or queued | unchanged | `none` |
47
- | `before_settle` | pending but `!enabled`, `!automaticResetEnabled`, or aborted | disarms to `idle` (`spent` if already spent) | `none` |
48
- | `settled` | — | `overflow=idle`; `request` unchanged | `none` |
49
- | `abort` / `clear` | — | `(request=none, overflow=idle)` | `none` |
50
-
51
- ### Invariants
52
-
53
- 1. At most one explicit request is pending at a time; duplicate requests in one tool batch dedupe and never queue a second boundary.
54
- 2. Every `turn_end` consumes a pending explicit request, whether or not that turn commits a reset.
55
- 3. A committed reset is exactly `marker -> boot -> continuation`, and ordinary and budget drafts are ordered before the marker. Budget staging is drained once per turn and discarded on abort or disabled mode.
56
- 4. Reset drafts are built once per commit. If construction throws, already-built incoming and budget drafts survive and no continuation is requested.
57
- 5. Overflow recovery is one attempt per failure chain: a failure arms `pending` only when nothing is queued, the extension is enabled, and automatic reset is enabled; a settle spends it to `spent`; a later failure becomes `pending-spent` and cannot recover again until settlement ends the chain.
58
- 6. Aborted turns never manufacture a continuation and clear both axes. Non-overflow errors keep the armed overflow chain for the settle boundary.
59
- 7. A successful non-overflow turn clears the overflow chain first, so a queued success supersedes a stale overflow failure before settle recovery can act.
60
- 8. `settled` ends the failure chain but preserves a pending explicit request; `clear` (session start, tree navigation, shutdown, `/pi-context off`, `/wipe-memory`) resets both axes.
61
- 9. Disabled mode never commits a reset and never arms or spends overflow recovery; an existing persisted marker stays authoritative for native-compaction cancellation.
62
- 10. Policy guards (`queued`, `automaticResetEnabled`, `thresholdDue`) are consulted only in the branch that needs them, preserving the adapter's original resolution order.
63
-
64
- ### Critical sequences
65
-
66
- - **Normal reset.** A completed, enabled turn whose explicit request is pending or whose usage is due returns `commit-boundary`. The adapter drains budget drafts, keeps incoming drafts in order, appends `marker -> boot -> continuation`, and requests continuation. A second request in the same batch returns `already-requested`; the next turn commits it alone.
67
- - **Overflow recovery.** An overflow-like `turn_end` with no queued message, enabled mode, and automatic reset arms `pending`; a `before_settle` with no queued message returns `recover-overflow` and spends the attempt to `spent`. A repeated settle is a no-op, and a later overflow failure re-arms only `pending-spent`.
68
- - **Abort.** An aborted `turn_end` returns only the drafts already collected for that boundary, clears both axes, and never appends reset drafts.
69
- - **Queued success.** A queued message defers `before_settle`; the queued turn's successful `turn_end` clears the stale overflow chain, so no recovery fires.
70
- - **Process interruption.** If Pi stops between the marker and the later reset messages, the marker remains the authoritative boundary. On the next start, startup repair may append only the missing boot and continuation when the tail is otherwise repairable; it never moves or reinterprets the boundary.
71
- - **Startup tail repair.** On `session_start` / `session_tree`, `ensureBoot` asks `repairResetTail` to inspect the marker tail and emit only the missing artifacts in the closed order: the boot, then the continuation. A boot/continuation sequence that is already complete, or a tail containing real conversation, a foreign message, a later marker, or a misordered/duplicate artifact, refuses repair and leaves `/wipe-memory` as the explicit recovery path.
72
-
73
- The budget owner stages the early guidance and final checkpoint warning from active-window usage. The warning is visible in the current provider request, while both drafts are committed only by the lifecycle composer and are discarded on abort, settlement without a `turn_end`, transition, or window mismatch. After the warning, the model either writes its note and calls `wipe_memory`, or runtime recovery requests the same marker/boot boundary. Guidance and warning drafts precede reset drafts so stale reminders cannot be queued into the new window; durable entries remain the authority for redelivery. Their UI notices are emitted at the next turn start or settlement only after the matching reminder is committed in the active window, so aborted requests and retries cannot repeat an uncommitted reminder's notification.
74
-
75
- The turn-end commit is the scheduling boundary: Pi receives the finished tool batch and then the marker/boot drafts as one append operation. Pi owns queue scheduling and deduplication of the next request; the extension does not run a parallel compaction state machine or use a compaction completion callback.
76
-
77
- Boot construction reads the five note homes into one closed snapshot before rendering the hidden boot. An absent home is empty; a real read failure removes only that home's MAP/pocket rows. The boot retains its identity, reset line, notes-home instructions, and recovery protocol, and includes a concise `notes_list` retry notice. The human receives one incomplete-index notification per boot. This notes failure isolation does not bypass the marker/boot commit, alter raw history, or turn a partial boot into an empty fallback.
78
-
79
- The hidden boot is selected by its durable `details.windowId`, not by timestamp or content equality. The context hook folds only the dropped system prefix before that boot and preserves later prompt patches and messages in order. If the boot is missing, the hook aborts with a safe head and notice rather than sending raw history. Startup/tree repair is deliberately narrow: it appends a boot only when the marker tail is otherwise incomplete metadata; later conversation or an authoritative raw boot causes safe refusal. `/wipe-memory` is the recovery path for that refused branch. A fork/clone creates a new session ID while copying branch entries, so startup refreshes the root boot identity while retaining copied root messages.
80
-
81
- Public APIs let a mixed tool batch finish before the marker/boot boundary. Queued steering/follow-up messages are delivered exactly once in the new window, neither dropped nor replayed. `/tree` navigation remains available, but when either source or destination branch contains a reset marker, Pi's raw summary generator is bypassed: the summary is empty and a notice explains why, preventing erased history from re-entering through a path outside the context hook. With no marker on either branch, native summaries remain unchanged.
82
-
83
- Validation uses a reduced set of persisted-data integration tests, isolated lifecycle event tests, and scripted SDK tests running Pi's actual agent loop. Representative cases check reset-window history retention, resumable reads, native-compaction cancellation, mixed-tool completion, steering/follow-up delivery, bounded overflow recovery, and settings authority. Tree-summary suppression no longer has a dedicated retained test. The separate coherence and pagination property suites have been removed; the retained cases do not cover the previous full matrix of branch, cancellation, malformed-input, and pagination edges.
9
+ | `wipe_memory` tool | Mark the current window tool-requested. At `turn_end`, after the complete tool batch and any accepted budget drafts, append the reset boundary and ask Pi to continue. Repeated requests for that window deduplicate. |
10
+ | `/wipe-memory` | Capture the current window, wait for idle, and stop if another invocation has already moved the branch to a new window. Arm a manual close-out, persist the shared hidden warning, and trigger an ordinary model turn. The agent can write notes/use tools over several turns. A `wipe_memory` call commits at `turn_end`; otherwise a successful normal stop commits at `agent_before_settle` after queued messages drain. |
11
+ | Budget warning | When automatic compaction is enabled and remaining active-window budget reaches reserve plus the warning runway, put the same hidden warning in the request and arm an automatic close-out. An explicit `wipe_memory` commits the boundary; a successful normal stop is the fallback. |
12
+ | Hard reserve | Independent safety path: if automatic resets are enabled and completed-turn usage reaches Pi's reserve, commit a boundary at `turn_end`. |
13
+ | Abort or error | Never count as successful close-out. Abort clears lifecycle state. An ordinary close-out error is dropped without reset; provider-overflow recovery remains separately bounded. |
14
+ | `/compact` while active | Cancel native compaction, which could otherwise summarize pre-reset history back into the active window. A manual attempt receives an actionable `/wipe-memory` notice. |
15
+
16
+ A new boundary is ordered as:
17
+
18
+ 1. A native `compaction` entry with `summary: ""` and `firstKeptEntryId` set to its own ID. This is Pi's retain-none canonical checkpoint; it is not a generated summary request.
19
+ 2. A `pi-context/reset-marker` custom entry with `{ windowId }`.
20
+ 3. A hidden `pi-context/boot` custom message with matching `details.windowId`.
21
+ 4. A hidden continuation message.
22
+
23
+ The raw session branch is preserved for history. New checkpoint-backed branches use Pi's canonical retain-none projection, which preserves the empty summary wrapper and system/tool state while excluding earlier conversational context. Older marker-only sessions use marker slicing as a narrow compatibility fallback. A marker is trusted as checkpoint-backed only when it directly follows an empty native compaction whose `firstKeptEntryId` is the compaction's own ID; a generic preceding compaction is not enough.
24
+
25
+ `turn_end` is the sole composer for completed turns. It preserves incoming entries, consumes current-window budget drafts, and appends the ordered reset boundary only when the reducer requests one. `agent_before_settle` handles successful close-out fallback after Pi has drained queued work. Pi owns the ensuing continuation and request scheduling. Reset construction awaits the asynchronous notes snapshot; the handler captures its session/window generation before awaiting, then rechecks generation, session, window, enabled state, and abort status before returning drafts. A stale completion is discarded while incoming/budget entries survive, and success notices are staged only after that guard. Construction failures notify only while the initiating lifecycle is still current and preserve already-collected entries without claiming a continuation.
26
+
27
+ ## Reducer state
28
+
29
+ `reduceResetControl` is pure: the adapter supplies all lifecycle facts, and the reducer returns the next state plus a named effect. Its state combines a request phase with a bounded overflow phase:
30
+
31
+ | State | Meaning |
32
+ | --- | --- |
33
+ | `{ phase: "none" }` | No close-out/tool request is pending. |
34
+ | `{ phase: "close-out", windowId, source }` | Manual or automatic warning close-out remains armed across ordinary note/tool turns. |
35
+ | `{ phase: "tool-requested", windowId }` | A direct `wipe_memory` request will commit after its complete turn batch. |
36
+ | `overflow: "idle"` | No overflow recovery is pending. |
37
+ | `overflow: "pending"` | An overflow failure may recover once at pre-settlement. |
38
+ | `overflow: "pending-spent"` | Recovery was already spent in this failure chain. |
39
+ | `overflow: "spent"` | The one recovery has been used until the chain settles. |
40
+
41
+ ### Transition rules
42
+
43
+ - `close_out(windowId, source)` arms a close-out for that window. Repeating an already-pending request deduplicates; a manual request can upgrade an automatic request for the same window.
44
+ - `tool_request(windowId)` marks that window for a turn-end reset. A matching duplicate is reported as already pending.
45
+ - `turn_end` first clears everything on abort. Overflow-like errors disarm the reset request and arm overflow recovery only when there is no queued work, the extension is enabled, and automatic reset is enabled. Other failures drop the request but preserve the overflow chain. A successful, enabled turn commits for a tool request or hard-reserve condition; a close-out alone remains armed.
46
+ - `agent_before_settle` clears state on actual abort. Pending overflow recovery gets its one bounded attempt when enabled and unqueued. Otherwise a close-out commits only if the outcome is successful, the request belongs to the current window, the extension is enabled, and no queued work remains. Automatic close-outs also require automatic resets to remain enabled.
47
+ - `agent_settled`, session start/tree navigation/shutdown, `/pi-context off`, and abort clear transient request state. Durable checkpoint/marker history remains authoritative after transient state is gone.
48
+
49
+ This separation is intentional: a note write or ordinary tool turn during a close-out must not consume it. Direct `wipe_memory` is not armed by the shared warning and remains valid without one. A user abort, an assistant message with a synthetic `stopReason: "aborted"`, and a generic provider error are distinct cases; none commits an ordinary close-out, while only Pi's actual operation cancellation is a user abort.
50
+
51
+ ## Budget staging and notifications
52
+
53
+ The early reminder is staged once per window when remaining budget reaches `reserve + reminderMarginTokens`. The final close-out warning is staged/attached at `reserve + WARNING_RUNWAY_TOKENS`, only when automatic compaction is enabled. The hard reserve itself remains a separate safety cutoff. The warning text is shared with `/wipe-memory`; after it is durable, the extension does not repeat it in that window. Warning persistence deduplicates only injection: each eligible low-budget request re-arms transient automatic close-out before checking for the durable warning, so an abort/error can clear its attempt and the next request can retry without another warning.
54
+
55
+ Budget drafts are instance-local and are consumed at turn end. They are discarded on abort, failed turn, transition, or window mismatch. UI reminder notices are emitted only after the matching hidden entry is durable, so failed requests do not report an uncommitted warning. Active-window usage and the exact `SettingsManager` authority drive threshold checks; default file-backed policy is cached per extension instance, while injected managers are read through their public API.
56
+
57
+ ## Projection, repair, and history
58
+
59
+ `context_with_system` selects the active boot by durable `details.windowId`. For a verified native checkpoint, Pi's canonical projection is used directly; for a legacy marker-only reset, the compatibility projection slices at the matching boot. The projection preserves system/tool state and later prompt patches. If the boot is absent, the runtime aborts safely instead of sending raw history. Usage estimation uses the same checkpoint-aware projection decision.
60
+
61
+ Startup/tree repair is narrow: it may complete a repairable marker tail when later conversation does not make the missing metadata ambiguous. It awaits the notes snapshot and rechecks lifecycle/window identity before sending a repaired boot or continuation. Legacy marker-only tails remain supported; repair never moves a boundary or promotes an arbitrary compaction to a retain-none checkpoint. `/tree` summary generation is suppressed with an empty summary when either branch crosses a reset, because the raw summary generator bypasses the provider projection.
62
+
63
+ ## Validation and known limits
64
+
65
+ The test suite combines isolated reducer tests with scripted SDK tests executing Pi's real agent loop without model/network calls. The agent-loop coverage includes manual warning plus multi-turn note writes and explicit wipe, direct wipe without warning, normal-stop fallback, same-window concurrent command deduplication, actual and synthetic abort/error cleanup, queued steering/follow-up delivery, hard-reserve and bounded-overflow recovery, successive resets, and real SessionManager file reopen after two retain-none checkpoints. It also checks raw-history preservation, canonical projection, empty-summary retention, system/tool state, warning/marker order, and fresh-window continuation.
66
+
67
+ The broader coherence and pagination property suites were removed during test reduction; retained cases are representative rather than exhaustive. Tree-summary suppression has no dedicated retained test, and external-provider behavior, every malformed persisted shape, and every filesystem failure mode are not established by this suite.
package/package.json CHANGED
@@ -1,7 +1,24 @@
1
1
  {
2
2
  "name": "@astrosheep/pi-context",
3
- "version": "0.25.2",
3
+ "version": "0.26.0",
4
4
  "type": "module",
5
+ "main": "./dist/src/index.js",
6
+ "types": "./dist/src/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/src/index.d.ts",
10
+ "import": "./dist/src/index.js"
11
+ },
12
+ "./notes": {
13
+ "types": "./dist/src/notes/index.d.ts",
14
+ "import": "./dist/src/notes/index.js"
15
+ },
16
+ "./dist/src/index.js": {
17
+ "types": "./dist/src/index.d.ts",
18
+ "import": "./dist/src/index.js"
19
+ },
20
+ "./package.json": "./package.json"
21
+ },
5
22
  "description": "Codex-style context windows for Pi: durable reset windows, session history tools, and persistent notes.",
6
23
  "license": "MIT",
7
24
  "keywords": [
@@ -16,6 +33,9 @@
16
33
  "pi": {
17
34
  "extensions": [
18
35
  "./dist/extension.js"
36
+ ],
37
+ "skills": [
38
+ "./skills"
19
39
  ]
20
40
  },
21
41
  "files": [
@@ -24,7 +44,8 @@
24
44
  "docs",
25
45
  "LICENSE",
26
46
  "README.md",
27
- "playbook.md"
47
+ "playbook.md",
48
+ "skills"
28
49
  ],
29
50
  "bin": {
30
51
  "dream": "dist/src/dream/cli.js"
@@ -33,6 +54,7 @@
33
54
  "build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json && node scripts/build-extension.mjs && node -e \"require('node:fs').chmodSync('dist/src/dream/cli.js', 0o755)\"",
34
55
  "typecheck": "tsc -p tsconfig.json --noEmit",
35
56
  "test": "npm run build && node --test dist/test/*.test.js",
57
+ "test:notes-package": "npm run build && node scripts/check-notes-package.mjs",
36
58
  "prepublishOnly": "npm run typecheck",
37
59
  "prepack": "npm run build"
38
60
  },
@@ -41,6 +63,17 @@
41
63
  "@earendil-works/pi-ai": "^0.87.0",
42
64
  "@earendil-works/pi-coding-agent": "^0.87.0"
43
65
  },
66
+ "peerDependenciesMeta": {
67
+ "@earendil-works/pi-agent-core": {
68
+ "optional": true
69
+ },
70
+ "@earendil-works/pi-ai": {
71
+ "optional": true
72
+ },
73
+ "@earendil-works/pi-coding-agent": {
74
+ "optional": true
75
+ }
76
+ },
44
77
  "devDependencies": {
45
78
  "@earendil-works/pi-agent-core": "^0.87.0",
46
79
  "@earendil-works/pi-ai": "^0.87.0",
package/playbook.md CHANGED
@@ -1,32 +1,33 @@
1
- I am dreaming over my notes. They are plain markdown files in three homes, addressed as:
2
-
3
- - bare `<vpath>` for this session
4
- - `@project/<vpath>` for this project
5
- - `@personal/<vpath>` for personal notes
6
-
7
- Every note has this frontmatter block:
8
-
9
- ```yaml
10
- ---
11
- origin: user | self | external
12
- status: active
13
- stale: false
14
- created_at: <timestamp>
15
- updated_at: <timestamp>
16
- last_accessed: <timestamp>
17
- access_count: 0
18
- ---
19
- ```
20
-
21
- ## Dream rules
22
-
23
- 1. **Probe before you trust.** Before keeping or promoting a note, verify its world referents with read-only file tools: paths in the body do they still exist? branches still present? Dead referents are why a note gets merged away or marked stale, never promoted.
24
- 2. **Merge threshold.** Supersede another note only when all three hold: same topic (name it in the survivor's body), same kind of note (checkpoint/design/log…), and the survivor is strictly newer or strictly more specific. Otherwise keep both and record the open conflict in the survivor.
25
- 3. **Size budget.** Keep every note under ~200 lines / ~8KB. Oversized notes get split by topic with a one-line cross-link in each (`see also: @home/<vpath>`). Checkpoints may exceed the budget trim prose, never facts.
26
- 4. **Keep the maps.** Each home's `MAP.md` maps that home's durable notes: one line per entry its address and a short gist in your own words, never a mechanical body slice. Project notes go on `@project/MAP.md`, cross-project knowledge on `@personal/MAP.md`; session notes are never mapped — the pocket covers them. When a note is promoted across homes, move its line to the destination map; when a note goes stale, drop its line. Maps obey the same size budget as any note.
27
- 5. **Jurisdiction.** Read the whole store, including every session home, to extract durable knowledge. `pi/session/**` is a live agent's write-ahead log: never write or edit anything there, including frontmatter or stale markers. Promote useful facts by writing to project or personal instead. Other notes are never physically deleted and every run is bracketed by git commits, so the human gate can audit and revert whatever you touch. Group your report by home so the gate can see what moved. In writable homes: map entry lines are yours to maintain, but prose that carries rules or guidance is not — flag it in your report instead of rewriting it.
28
- 6. **Leave stable notes alone.** Change notes to incorporate new evidence, resolve verified errors, merge genuine duplicates, or split oversized files—not merely to shorten or rephrase them. Preserve facts, conditions, exceptions, and uncertainty. No change is a valid outcome.
29
-
30
- Read the files and merge genuinely duplicate notes in writable homes by editing the survivor, then set `stale: true` in the absorbed note's frontmatter if it is writable. Session notes remain untouched even when promoted; record their source in the destination. Nothing is physically deleted; stale notes remain readable. Promote durable cross-project knowledge by writing or editing at `@personal/<vpath>`. Keep notes compact and preserve useful provenance in the body.
31
-
32
- Do not write skill ideas as files. Put skill ideas and unresolved questions in your final assistant message as proposals for the human. Your final message should be a concise report of what you inspected, changed, and left unresolved. If you made no file writes, say so.
1
+ # Dream
2
+
3
+ I am reviewing my notes: keeping what is useful, connecting what belongs together, and leaving a clearer record for whoever comes next. Notes are record, not memory.
4
+
5
+ ## Scope
6
+
7
+ Work within the readable sources and writable homes established for this run. Do not broaden that scope while exploring. If a boundary is unclear, ask before crossing it.
8
+
9
+ A project dream draws from that project's material, project notes, and session notes identified as belonging to the project; it organizes only that project's notes. A store-wide dream may inspect across homes, but authorship and write permissions still apply. Unknown ownership is a coverage gap to report, not a reason to guess or repair ownership.
10
+
11
+ The homes serve different readers:
12
+
13
+ - Session notes are a trip's working record. Read them as sources where permitted; never edit them, even after promoting a useful finding.
14
+ - Project notes hold knowledge for whoever next works on that project.
15
+ - Human notes hold the human's cross-project preferences and standing rules. Do not turn project facts or your own lessons into rules in the human's voice.
16
+ - Agent notes belong to their author. Other agents' notes are read-only unless the human authorizes revision.
17
+ - Model notes describe the substrate's behavior. Other models' notes are read-only unless the human authorizes revision.
18
+
19
+ ## Review and consolidate
20
+
21
+ 1. **Leave stable notes alone.** Change a note to incorporate new evidence, resolve a verified error, merge a genuine duplicate, or split an oversized file—not merely to shorten or rephrase it. No change is a valid outcome.
22
+ 2. **Verify only what the proposed change depends on.** Before correcting or promoting a conclusion, check the relevant evidence as needed. Do not revalidate every stable note or turn dream into a source-code audit. An unavailable referent is unverified, not proof that it is dead; do not promote an unsupported claim as current fact.
23
+ 3. **Merge only genuine duplicates.** Supersede a note only when both notes have the same topic and kind (checkpoint, design, log…), and the survivor is strictly newer or more specific. Name the shared topic in the survivor. Otherwise keep both and record any unresolved conflict.
24
+ 4. **Preserve meaning and provenance.** Keep facts, conditions, exceptions, uncertainty, authorship, and sources. When promoting a finding, cite its source. Do not relabel agent-authored material as human-authored. If its destination or authority is unclear, propose the promotion instead.
25
+ 5. **Keep notes manageable.** Aim below about 200 lines / 8KB per note. Split oversized notes by topic with one-line cross-links. Checkpoints may exceed the budget: trim prose, never facts.
26
+ 6. **Keep maps useful.** Each writable home's MAP.md lists its durable notes, one line per entry, with an unambiguous address and a short gist in your own words. Add promoted notes and drop stale entries. Session notes are never mapped. Maps follow the same size budget.
27
+ 7. **Preserve the record.** Never physically delete notes. After merging, mark the absorbed note stale only if it is writable. Preserve existing metadata and provenance. Map entry lines are yours to maintain within the writable scope.
28
+
29
+ ## Report
30
+
31
+ Give a concise report grouped by home: what you inspected, changed, promoted, and left unresolved. Mention coverage gaps and partial failures. If no notes changed, say so. Distinguish actual note changes from execution or audit artifacts; do not claim success for an incomplete run.
32
+
33
+ Put skill ideas and unresolved questions in the final message as proposals for the human, not new skill files.
@@ -0,0 +1,12 @@
1
+ ---
2
+ name: dream
3
+ description: Review and consolidate durable notes using pi-context's shared dream playbook. Use when the human asks to dream over notes, merge genuine duplicates, update maps, or promote durable learning.
4
+ ---
5
+
6
+ # Dream
7
+
8
+ Read `../../playbook.md` completely, resolving the path from this skill's directory. It is the shared guide to reviewing and organizing notes.
9
+
10
+ Dream in this agent's current session. Do not start another agent or model unless the human asks.
11
+
12
+ Work within the supplied readable and writable scope. If the scope or permission to write has not been established, ask rather than inventing it or setting up execution machinery yourself.
@@ -1,9 +1,9 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import { loadNotesSnapshot, type NotesSnapshot } from "../notes/notes-snapshot.js";
3
- import { agentSlug, modelSlug } from "../notes/paths.js";
2
+ import { notesContextFromPi } from "../pi/notes/adapter.js";
3
+ import { loadNotesSnapshot, type NotesSnapshot } from "../pi/notes/snapshot.js";
4
4
  import { BOOT_TYPE } from "../protocol.js";
5
5
  import { renderBootBlock } from "./prompts.js";
6
- import { currentReset, isWindowBoot, rootWindowId } from "./context-window.js";
6
+ import { currentReset, currentWindowId, isWindowBoot, rootWindowId } from "./context-window.js";
7
7
  import { repairResetTail } from "./reset-artifacts.js";
8
8
 
9
9
  export type IncompleteNotesNotifier = (ctx: ExtensionContext, windowId: string, snapshot: NotesSnapshot) => void;
@@ -16,31 +16,38 @@ export type BootMessage = {
16
16
  readonly details: { windowId: string };
17
17
  };
18
18
 
19
- /** Render the boot block from the live context; acquisition stays with loadNotesSnapshot. */
20
- function bootContent(ctx: ExtensionContext, currentId: string, previousId: string | undefined, notes: NotesSnapshot): string {
21
- return renderBootBlock({
22
- agentName: agentSlug(ctx),
23
- modelName: modelSlug(ctx),
24
- firstWindowId: rootWindowId(ctx.sessionManager.getSessionId()),
25
- currentWindowId: currentId,
26
- previousWindowId: previousId,
27
- notes,
28
- });
19
+ /** Render the boot block from one pre-await identity and acquired notes snapshot. */
20
+ function bootContent(
21
+ agentName: string,
22
+ modelName: string,
23
+ firstWindowId: string,
24
+ currentId: string,
25
+ previousId: string | undefined,
26
+ notes: NotesSnapshot,
27
+ ): string {
28
+ return renderBootBlock({ agentName, modelName, firstWindowId, currentWindowId: currentId, previousWindowId: previousId, notes });
29
29
  }
30
30
 
31
31
  /**
32
- * Acquire one notes snapshot and build the boot custom message for a window. The caller
33
- * supplies `previousId` only when a reset boundary needs the prior window identity.
32
+ * Acquire one notes snapshot and build the boot custom message for a window. All identity
33
+ * fields are captured synchronously before the first filesystem await.
34
34
  */
35
- export function buildBootMessage(
35
+ export async function buildBootMessage(
36
36
  ctx: ExtensionContext,
37
37
  windowId: string,
38
38
  previousId: string | undefined,
39
39
  notifyIncompleteNotes?: IncompleteNotesNotifier,
40
- ): BootMessage {
41
- const notes = loadNotesSnapshot(ctx);
42
- notifyIncompleteNotes?.(ctx, windowId, notes);
43
- return { customType: BOOT_TYPE, content: bootContent(ctx, windowId, previousId, notes), display: false, details: { windowId } };
40
+ isCurrent: () => boolean = () => true,
41
+ ): Promise<BootMessage> {
42
+ const identity = notesContextFromPi(ctx);
43
+ const notes = await loadNotesSnapshot(ctx, undefined, identity);
44
+ if (isCurrent()) notifyIncompleteNotes?.(ctx, windowId, notes);
45
+ return {
46
+ customType: BOOT_TYPE,
47
+ content: bootContent(identity.agent, identity.model, rootWindowId(identity.sessionId), windowId, previousId, notes),
48
+ display: false,
49
+ details: { windowId },
50
+ };
44
51
  }
45
52
 
46
53
  /** Persist one hidden boot message without triggering a model turn. */
@@ -54,15 +61,27 @@ export function sendBoot(pi: ExtensionAPI, boot: BootMessage): void {
54
61
  /**
55
62
  * Boot entry point for `session_start` / `session_tree`. Ordinary startup ensures one root
56
63
  * boot; a reset marker instead asks reset-artifact repair to complete its persisted tail.
57
- * Boot idempotence lives here: an already-projected root boot or a complete reset tail emits nothing.
58
64
  */
59
- export function ensureBoot(pi: ExtensionAPI, ctx: ExtensionContext, notifyIncompleteNotes?: IncompleteNotesNotifier): void {
65
+ export async function ensureBoot(
66
+ pi: ExtensionAPI,
67
+ ctx: ExtensionContext,
68
+ notifyIncompleteNotes?: IncompleteNotesNotifier,
69
+ isCurrent: () => boolean = () => true,
70
+ ): Promise<void> {
71
+ const sessionId = ctx.sessionManager.getSessionId();
60
72
  const reset = currentReset(ctx);
73
+ const windowId = reset?.data.windowId ?? rootWindowId(sessionId);
74
+ const stillCurrent = () => isCurrent() &&
75
+ ctx.signal?.aborted !== true &&
76
+ ctx.sessionManager.getSessionId() === sessionId &&
77
+ currentWindowId(ctx) === windowId;
61
78
  if (reset) {
62
- repairResetTail(pi, ctx, reset, notifyIncompleteNotes);
79
+ await repairResetTail(pi, ctx, reset, notifyIncompleteNotes, stillCurrent);
63
80
  return;
64
81
  }
65
- const windowId = rootWindowId(ctx.sessionManager.getSessionId());
66
82
  if (ctx.sessionManager.buildSessionProjection().messages.some((message) => isWindowBoot(message, windowId))) return;
67
- sendBoot(pi, buildBootMessage(ctx, windowId, undefined, notifyIncompleteNotes));
83
+ const boot = await buildBootMessage(ctx, windowId, undefined, notifyIncompleteNotes, stillCurrent);
84
+ if (!stillCurrent()) return;
85
+ if (ctx.sessionManager.buildSessionProjection().messages.some((message) => isWindowBoot(message, windowId))) return;
86
+ sendBoot(pi, boot);
68
87
  }
@@ -1,6 +1,6 @@
1
1
  import { Type } from "@earendil-works/pi-ai";
2
2
  import { defineTool, type ExtensionAPI, type ExtensionContext, type SessionBoundaryDraft, type SettingsManager } from "@earendil-works/pi-coding-agent";
3
- import { GUIDANCE_CLOSE_TAG, GUIDANCE_OPEN_TAG, GUIDANCE_TYPE, WARNING_PROMPT, WARNING_TYPE } from "../protocol.js";
3
+ import { GUIDANCE_TYPE, WARNING_CONTENT, WARNING_TYPE } from "../protocol.js";
4
4
  import { readThresholdSettings, type ResolvedThresholds, type ThresholdSettingsResolution } from "./thresholds.js";
5
5
  import { currentWindowId, hasWindowMessage, windowUsage } from "./context-window.js";
6
6
  import { tokenBudgetGuidance } from "./prompts.js";
@@ -12,7 +12,12 @@ export function remainingTokens(ctx: Pick<ExtensionContext, "sessionManager" | "
12
12
  return !usage || usage.tokens === null ? null : Math.max(0, usage.contextWindow - usage.tokens);
13
13
  }
14
14
 
15
- export function registerBudget(pi: ExtensionAPI, isEnabled: () => boolean, settingsManager?: SettingsManager) {
15
+ export function registerBudget(
16
+ pi: ExtensionAPI,
17
+ isEnabled: () => boolean,
18
+ settingsManager?: SettingsManager,
19
+ onCloseOut: (windowId: string) => void = () => {},
20
+ ) {
16
21
  let cachedPolicy: { thresholds: ResolvedThresholds; automatic: boolean } | undefined;
17
22
  const notifiedWarnings = new Set<string>();
18
23
  const resolvePolicy = (ctx: ExtensionContext): ThresholdSettingsResolution => {
@@ -32,13 +37,12 @@ export function registerBudget(pi: ExtensionAPI, isEnabled: () => boolean, setti
32
37
  const automaticResetEnabled = (ctx: ExtensionContext): boolean => {
33
38
  return resolvePolicy(ctx).automatic;
34
39
  };
35
- const resetDue = (ctx: ExtensionContext): boolean => {
40
+ const hardReserveDue = (ctx: ExtensionContext): boolean => {
36
41
  if (!automaticResetEnabled(ctx)) return false;
37
42
  const usage = windowUsage(ctx);
38
43
  return usage !== undefined && usage.tokens !== null && usage.contextWindow - usage.tokens <= thresholdsFor(ctx).reserve;
39
44
  };
40
45
  const invalidateThresholds = () => { cachedPolicy = undefined; };
41
- const formatRemaining = (remaining: number): string => `${Math.max(0, Math.ceil(remaining / 1000))}k`;
42
46
  let pendingGuidance: { windowId: string; content: string; remaining: number } | undefined;
43
47
  let pendingWarning: { windowId: string; content: string; remaining: number } | undefined;
44
48
  let pendingNotices: Array<{ windowId: string; customType: string; remaining: number }> = [];
@@ -47,8 +51,8 @@ export function registerBudget(pi: ExtensionAPI, isEnabled: () => boolean, setti
47
51
  for (const notice of pendingNotices) {
48
52
  if (notice.windowId !== windowId || !hasWindowMessage(ctx, notice.customType)) continue;
49
53
  ctx.ui.notify(notice.customType === WARNING_TYPE
50
- ? `pi-context: Context almost full ${formatRemaining(notice.remaining)} remaining`
51
- : `pi-context: Context running low ${formatRemaining(notice.remaining)} remaining`, "warning");
54
+ ? "pi-context: Context almost full; close out the current memory window."
55
+ : "pi-context: Context running low; checkpoint your notes soon.", "warning");
52
56
  }
53
57
  pendingNotices = [];
54
58
  };
@@ -101,11 +105,14 @@ export function registerBudget(pi: ExtensionAPI, isEnabled: () => boolean, setti
101
105
  if (remaining === null) return undefined;
102
106
  const windowId = currentWindowId(ctx);
103
107
  const { reminder, warning } = thresholdsFor(ctx);
104
- if (hasWindowMessage(ctx, WARNING_TYPE) || pendingWarning?.windowId === windowId) return undefined;
105
- if (remaining <= warning) {
106
- // A not-yet-committed shallow reminder is superseded by the final warning.
108
+ if (remaining <= warning && automaticResetEnabled(ctx)) {
109
+ // Re-arm close-out on each eligible request. A prior request may have aborted
110
+ // after persisting the warning, so warning deduplication must not own this state.
111
+ onCloseOut(windowId);
112
+ if (hasWindowMessage(ctx, WARNING_TYPE) || pendingWarning?.windowId === windowId) return undefined;
113
+ // The critical close-out starts at reserve + runway, not at the hard reserve.
107
114
  pendingGuidance = undefined;
108
- const content = `${GUIDANCE_OPEN_TAG}\n${WARNING_PROMPT}\n${GUIDANCE_CLOSE_TAG}`;
115
+ const content = WARNING_CONTENT;
109
116
  pendingWarning = { windowId, content, remaining };
110
117
  const warningMessage = {
111
118
  role: "custom" as const,
@@ -116,6 +123,7 @@ export function registerBudget(pi: ExtensionAPI, isEnabled: () => boolean, setti
116
123
  };
117
124
  return { messages: [..._event.messages, warningMessage] };
118
125
  }
126
+ if (hasWindowMessage(ctx, WARNING_TYPE) || pendingWarning?.windowId === windowId) return undefined;
119
127
  if (hasWindowMessage(ctx, GUIDANCE_TYPE) || pendingGuidance?.windowId === windowId) return undefined;
120
128
  if (remaining <= reminder) {
121
129
  // Persist at turn_end, before any reset drafts. A queued sendMessage could
@@ -141,7 +149,7 @@ export function registerBudget(pi: ExtensionAPI, isEnabled: () => boolean, setti
141
149
 
142
150
  return {
143
151
  automaticResetEnabled,
144
- resetDue,
152
+ hardReserveDue,
145
153
  consumeTurnEnd,
146
154
  clear: () => { clearStaged(); pendingNotices = []; },
147
155
  };