pi-midcompact 0.6.0 → 0.7.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 CHANGED
@@ -17,7 +17,7 @@ The name `pi-midcompact` points to the other option: **mid-context compression**
17
17
 
18
18
  ## What it does
19
19
 
20
- At a natural checkpoint, `pi-midcompact` freezes the active session leaf as an anchor and opens a separate transaction. The Agent proposes ranges for completed low-value phases, preserves load-bearing messages verbatim, and prepares a draft for review. Nothing changes until you explicitly commit the plan.
20
+ At a natural checkpoint, `pi-midcompact` freezes the active session leaf as an anchor and opens a separate transaction. The Agent proposes ranges for completed low-value phases, preserves load-bearing messages verbatim, and prepares a plan for review. Nothing changes until you explicitly commit the plan.
21
21
 
22
22
  ### User-directed depth, Agent-designed plan
23
23
 
@@ -29,7 +29,7 @@ That is planning guidance, not an enforced token target: semantic importance win
29
29
 
30
30
  ### A selective projection, prepared on a temporary branch
31
31
 
32
- `/midcompact:start` freezes the current session leaf as an **anchor**. Planning happens on a disposable child branch, so the discussion used to create and edit the draft never becomes part of the committed working context.
32
+ `/midcompact:start` freezes the current session leaf as an **anchor**. Planning happens on a disposable child branch, so the discussion used to create and edit the plan never becomes part of the committed working context.
33
33
 
34
34
  ```text
35
35
  Frozen anchor: raw session history
@@ -53,9 +53,9 @@ The raw session JSONL still contains:
53
53
  [original d1]──[decision to KEEP]──[original d2]──[latest work]
54
54
  ```
55
55
 
56
- ### A reviewed draft can reclaim meaningful context
56
+ ### A reviewed plan can reclaim meaningful context
57
57
 
58
- The earlier browser and TUI captures below illustrate a draft with **2 ranges** covering **42 of 73 atoms**, while the other 31 atoms remain verbatim. The current UI reports Pi-provided anchor usage as the baseline and derives a **display-only** projection of post-commit usage from documented char-class assumptions (labeled `est.`, shown as a range, never used for gating); factual content chars and image counts stay alongside it. Click either image to open it at full resolution.
58
+ The earlier browser and TUI captures below illustrate a plan with **2 ranges** covering **42 of 73 atoms**, while the other 31 atoms remain verbatim. The current UI reports Pi-provided anchor usage as the baseline and derives a **display-only** projection of post-commit usage from documented char-class assumptions (labeled `est.`, shown as a range, never used for gating); factual content chars and image counts stay alongside it. Click either image to open it at full resolution.
59
59
 
60
60
  <p align="center">
61
61
  <a href="./figures/review-webui.png">
@@ -94,7 +94,7 @@ pi-midcompact — mid-context compression, review, then human commit
94
94
  | Starts | Automatically near the context limit, or with `/compact` | At an explicit natural checkpoint with `/midcompact:start` |
95
95
  | Selects | One older contiguous prefix; keeps a recent token-budgeted tail | One or more reviewed ranges, including non-contiguous ranges and `KEEP` holes |
96
96
  | Planning | Optional one-shot instruction to focus the generated summary | User states scope and retention depth; the Agent discusses trade-offs and drafts selective ranges and summaries |
97
- | Decision gate | Generates a compaction checkpoint directly | Draft → TUI or browser review → explicit human `/midcompact:commit` |
97
+ | Decision gate | Generates a compaction checkpoint directly | Plan → TUI or browser review → explicit human `/midcompact:commit` |
98
98
  | Best fit | Automatic context maintenance and overflow recovery | Deliberate cleanup of completed phases while retaining specific decisions verbatim |
99
99
 
100
100
  `pi-midcompact` does not disable or replace Pi's automatic compaction; it gives you a separate, human-reviewed way to make selective reductions. See [Pi's compaction documentation](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/docs/compaction.md) for the built-in mechanism.
@@ -127,7 +127,7 @@ Run:
127
127
  /midcompact:start
128
128
  ```
129
129
 
130
- Pi opens a three-way chooser before creating transaction state: **Agent direct**, **User manual**, or **Drop**. Agent direct is the first and default-highlighted option, matching the previous fast path. The chooser is the standard `select` dialog, identical in interactive and RPC mode; RPC carries it as an extension UI `select` message with a bounded timeout, so an unresponsive client cancels instead of blocking. print/JSON modes have no dialog and default to **Agent direct**. Agent direct starts the existing inventory-first Agent workflow. User manual sends the same transaction guidance with a final “acknowledge only” instruction; after the Agent replies briefly, the Selection workbench opens (browser-based outside interactive mode). It does not start planning or mutate the DraftPlan until the user hands off later. Save the initial DraftPlan, close the UI, then tell the Agent to continue when you are ready. You can include an initial focus in the same command:
130
+ Pi opens a three-way chooser before creating transaction state: **Agent direct**, **User manual**, or **Drop**. Agent direct is the first and default-highlighted option, matching the previous fast path. The chooser is the standard `select` dialog, identical in interactive and RPC mode; RPC carries it as an extension UI `select` message with a bounded timeout, so an unresponsive client cancels instead of blocking. print/JSON modes have no dialog and default to **Agent direct**. Agent direct starts the existing inventory-first Agent workflow. User manual sends the same transaction guidance with a final “acknowledge only” instruction; after the Agent replies briefly, the Selection workbench opens (browser-based outside interactive mode). It does not start planning or mutate the plan until the user hands off later. Save the initial plan, close the UI, then tell the Agent to continue when you are ready. You can include an initial focus in the same command:
131
131
 
132
132
  ```text
133
133
  /midcompact:start Compress the early repository exploration, but keep user requirements verbatim.
@@ -168,7 +168,7 @@ Use Selection to create or change ranges and KEEP holes:
168
168
  /midcompact:select-webui
169
169
  ```
170
170
 
171
- Use the TUI or local browser Review surface to edit summaries/topics and reject ranges. Review deliberately does not create or resize ranges; reopen Selection for boundary changes. To continue with Agent after a user-created plan, send a normal message asking it to continue the current midcompact draft; the Agent is prompted to read the existing plan first.
171
+ Use the TUI or local browser Review surface to edit summaries/topics and reject ranges. Review deliberately does not create or resize ranges; reopen Selection for boundary changes. To continue with Agent after a user-created plan, send a normal message asking it to continue the current midcompact plan; the Agent is prompted to read the existing plan first.
172
172
 
173
173
  ### 4. Commit the reviewed compression
174
174
 
@@ -216,9 +216,9 @@ Enter/Esc/q close
216
216
  | `/midcompact:select-webui` | Opens the local browser Selection workbench. |
217
217
  | `/midcompact:review` | Opens summary/topic review in the native TUI. |
218
218
  | `/midcompact:review-webui` | Opens summary/topic review in a local browser. |
219
- | `/midcompact:commit` | Commits the reviewed draft. Human only. |
219
+ | `/midcompact:commit` | Commits the reviewed plan. Human only. |
220
220
  | `/midcompact:abort` | Abandons the transaction and returns to the anchor. |
221
- | `/midcompact:status` | Displays the current draft, or the committed compression state on this branch. |
221
+ | `/midcompact:status` | Displays the current plan, or the committed compression state on this branch. |
222
222
 
223
223
  The extension shows planning status in Pi's footer only while a transaction is active. It disappears after commit or abort.
224
224
 
@@ -234,7 +234,7 @@ npm run dev:webui -- --port=4180 --no-open
234
234
 
235
235
  The command opens a fixture router for `review-ready`, `review-pending`,
236
236
  `selection-mixed`, `no-telemetry`, and `wide-content`. Each button opens an
237
- isolated workbench with its own in-memory draft. Fixture pages survive browser
237
+ isolated workbench with its own in-memory plan. Fixture pages survive browser
238
238
  refresh and the page's Close action, reload HTML changes automatically, and
239
239
  restart for imported TypeScript changes. Stop the router with `Ctrl+C`. Use
240
240
  `dev/midcompact-debug-ui.ts` only when validating against an actual Pi session.
@@ -250,4 +250,4 @@ restart for imported TypeScript changes. Stop the router with `Ctrl+C`. Use
250
250
  - **Native Pi `/compact` interaction needs more real-session validation.** Avoid relying on mixed automatic/native compaction behavior for critical work until it has been exercised in your environment.
251
251
  - **Provider and extension interoperability needs more real-session validation.** Unusual message shapes, third-party context-transform ordering, and long-lived exact message fingerprints have not been broadly exercised.
252
252
  - **Very long sessions are not stress-tested.** Large review snapshots and repeated block accumulation may eventually require consolidation.
253
- - **Browser workbenches are local.** `select-webui` and `review-webui` bind to loopback and mutate the same branch-local DraftPlan as the native TUI surfaces.
253
+ - **Browser workbenches are local.** `select-webui` and `review-webui` bind to loopback and mutate the same branch-local plan as the native TUI surfaces.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-midcompact",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Branch-aware mid-context compression for the Pi coding agent",
5
5
  "author": "frostime",
6
6
  "repository": {
@@ -1,134 +1,101 @@
1
1
  ---
2
2
  name: midcompact
3
- description: Use when a midcompact runtime prompt starts or hands off a compression-planning transaction, or when current work needs details from an active committed midcompact block. Guides user-aligned candidate discovery, effort-scaled inspect/locate/plan work, replacement-summary writing, and recall.
3
+ description: "Use when runtime starts or hands off midcompact planning, or current work needs originals from a committed block. Guides selective compression, local summaries, and recall."
4
4
  ---
5
5
 
6
6
  # Midcompact
7
7
 
8
- ## Route the activation
9
-
10
- This skill handles two independent tasks: planning compression and recalling committed content. Choose the route before calling the tool.
11
-
12
- | Signal | Immediate duty | First action |
13
- |--------|----------------|--------------|
14
- | Runtime prompt says `FINAL STATE: USER MANUAL` | Let the user create the initial DraftPlan | Reply exactly `OK`; call no midcompact tool |
15
- | Runtime prompt says `FINAL STATE: AGENT DIRECT` | Start from the new empty draft | `request={action:"inspect"}` |
16
- | A handoff reports a persisted DraftPlan and the user asks to continue | Read the shared selection and determine what help the user wants | `request={action:"plan", op:"show"}` |
17
- | The user or a projected summary needs detail from a committed block | Retrieve that history only | Follow **Recall workflow** |
18
-
19
- The state-specific runtime prompt is authoritative. Recall does not enter planning or mutate the DraftPlan. During the User-manual acknowledgement turn, the no-tool instruction overrides every other route.
20
-
21
- The tool groups parameters by `action`: each action accepts only its own fields, and fields from other actions are rejected. Read `references/tool-interface.md` before adding workload-specific parameters, and treat the action-specific field lists there as exhaustive.
22
-
23
- ## Plan compression
8
+ The frozen **anchor** contains **atoms**, the smallest selectable units (ref `a0001`); tool calls and their results stay together. Agent and user share one **plan** of contiguous **ranges** with replacement summaries (id `d1`). **Measure** compares candidates without changing the plan. Human commit turns each range into a **block** (`c0001`), replacing its originals in future context. **Recall** retrieves those originals.
24
9
 
25
- Selected ranges become summaries in future model context; content outside them stays verbatim. Originals remain stored, but recall is a recovery path, not a substitute for a sufficient summary. Agent and user edit one DraftPlan; the user retains final control through review and `/midcompact:commit`.
26
-
27
- The start mode controls how the first draft is created. It does not determine whether a user selection is final, how much initiative the Agent should take, or how deeply the history should be investigated.
28
-
29
- Apply one invariant:
30
-
31
- > Every fact that can still affect future work must remain with sufficient fidelity, either outside the selected ranges or in their replacement summaries.
32
-
33
- ### Planning workflow
34
-
35
- #### 1. Read the entry state and user intent
10
+ ## Route the activation
36
11
 
37
- For a fresh Agent-direct transaction, begin with `request={action:"inspect"}`. For a handed-off DraftPlan, begin with `request={action:"plan", op:"show"}`; inspect the anchor only if the requested work needs broader context.
12
+ | Signal | Immediate action |
13
+ |---|---|
14
+ | Runtime says `FINAL STATE: USER MANUAL` | Reply exactly `OK`; call no midcompact tool. |
15
+ | Runtime says `FINAL STATE: AGENT DIRECT` | Call `request={action:"inspect"}`. |
16
+ | Handoff reports a persisted plan and user asks to continue | Call `request={action:"plan_show"}` before other midcompact actions. |
17
+ | Current work needs originals from a committed block | Follow **Recall**; do not enter planning. |
38
18
 
39
- Establish the user's desired scope, fidelity, and planning effort from their words, current selection, `User focus: ...`, and surrounding interaction. Treat answers collected through question, questionnaire, or similar tools as user-originated input even when represented as tool results.
19
+ Runtime instructions are authoritative. User-manual acknowledgement also forbids questions. Otherwise, complete the required first action before establishing direction.
40
20
 
41
- A manual selection records what the user selected, not whether they mean "only this" or "help me refine it." Follow a clear intent directly. If different interpretations would materially change the work, ask briefly whether to limit work to the selection or inspect and suggest changes. Do not force clarification when the user has already expressed a preference or delegated the judgment.
21
+ Read `references/tool-interface.md` when composing calls or handling protection/truncation. Use the schema for fields and defaults.
42
22
 
43
- Match effort to the requested fidelity. A quick or approximate request calls for bounded planning; a precision-sensitive request may justify deeper work after alignment.
23
+ ## Establish the compression direction
44
24
 
45
- #### 2. Form a bounded semantic view
25
+ If the user has not expressed a compression preference, ask before selecting ranges or investigating deeply:
46
26
 
47
- `inspect` returns factual structure with bounded user landmarks, grouped by user message. Use it for structure and volume, not to invent semantics. Ground candidates in visible conversation or a few targeted `locate` calls, and stop paging after the potentially relevant region. When exact candidate spans are known and their relative volume could change the choice, use read-only `inspect` spans to compare them without mutating the DraftPlan.
27
+ > Do you want light cleanup or substantial room for what comes next? What should I focus on, and what must stay verbatim? You can give a target usage or delegate the choice.
48
28
 
49
- Segment by completed work phase, not message count. User-originated input and concluding Agent responses are useful landmarks for intent and outcome, but are not automatically KEEP. Intermediate tool exchanges may also contain decisions or evidence absent from the final response.
29
+ Honor existing preferences; otherwise accept qualitative goals or delegation. Clarify ambiguous handoffs: refine the selection or work only within it? Respect explicit scope limits.
50
30
 
51
- | Often able to yield to a summary | Often load-bearing |
52
- |-----------------------------------|--------------------|
53
- | Repetitive or superseded exploration | Current intent and active constraints |
54
- | Tool output absorbed into a conclusion | Exact errors, evidence, or wording whose form matters |
55
- | A completed subtask's intermediate process | Decisions and rationale still governing the work |
56
- | A rejected attempt's mechanics | Its still-relevant failure reason |
31
+ Match compression and investigation effort to that direction. Compare factual character/image measurements; do not promise exact post-commit token savings. If meeting a target requires sacrificing important information, explain the tradeoff.
57
32
 
58
- Use little or no `locate` during this reconnaissance. If a phase cannot yet be described faithfully, omit it from the proposal or mark it as needing confirmation rather than exploring the anchor broadly.
33
+ ## Select worthwhile ranges
59
34
 
60
- #### 3. Present semantic options and align
35
+ Keep originals unless replacement serves the agreed goal; full-history coverage is not success.
61
36
 
62
- Before deep `locate` work or substantial DraftPlan mutation, establish the user's compression preference through explicit instruction, a reliable implication, or concise clarification.
37
+ Use `inspect` for structure and volume, not to infer unseen content. Ground candidates in visible conversation or targeted `locate_ref`/`locate_search` calls. Investigate only what could change selection; omit uncertain candidates or ask if investigation exceeds the agreed effort. Use `measure` when relative sizes affect the choice.
63
38
 
64
- Describe each proposal in recognizable conversation terms:
39
+ Weigh bulk removed against original detail lost and what retained context already carries. Technical eligibility alone does not justify compression.
65
40
 
66
- > From `<semantic start>` to `<semantic end>`, `<replace the whole phase / keep the endpoints and compress the work between>`; preserve `<load-bearing information>` in the summary.
41
+ ### Common patterns
67
42
 
68
- Use short recognizable excerpts when available and clear paraphrases otherwise. State whether the endpoint messages remain verbatim, what intermediate work disappears, and what the summary carries. Do not identify a user-facing range primarily by atom refs, DraftPlan ids, or arbitrary item numbers.
43
+ These are examples, not fixed rules about message roles or age.
69
44
 
70
- When treatments involve a meaningful tradeoff, present concise alternatives and recommend one. A clear quick request may need only one proportionate proposal. Add factual content or image measurements only when they help the choice, and do not convert them into unsupported token-savings claims.
45
+ - **Long execution between user messages.** Replace repetitive search, reading, editing, and testing when only results matter. Retain useful request/conclusion text and summarize key findings absent from it; tool output is not automatically noise.
46
+ - **Multi-turn discussion.** Summarize superseded proposals, repeated clarification, and detours. Keep failure reasons that explain decisions or prevent repeated mistakes; age alone does not make discussion obsolete.
71
47
 
72
- #### 4. Resolve boundaries and build the DraftPlan
48
+ One range reduces repetition in coherent discussion but may flatten distinctions. Semantic ranges allow different detail levels and KEEP holes, but risk duplication or broken causal links. Choose by content, not turn count; split around important originals.
73
49
 
74
- After the intended treatment is clear, use `request={action:"locate"}` for targeted content and boundary checks. An atom is the smallest selectable unit; a tool call and its matching results form one indivisible `tool_exchange` atom. Keep source text outside a range when exact wording or provenance matters and a summary cannot preserve it equivalently.
50
+ ### Preserve important user expression
75
51
 
76
- Choose boundaries from the information that must survive, not from a fixed category. A range may replace a whole semantic phase, including its initiating and concluding messages. It may instead retain a load-bearing user instruction and concluding Agent response while replacing only the execution between them. It may split around important material to leave KEEP holes. These are reasoning patterns, not rules tied to start mode, message age, or one prescribed kind of work.
52
+ Keep important user goals, constraints, preferences, corrections, authorizations, and refusals verbatim by default. Summaries can alter their scope or force. Ask for explicit agreement before summarizing them; general permission for aggressive compression is insufficient.
77
53
 
78
- Build or refine the shared DraftPlan with `plan add`, `update`, `remove`, and `show`. A handed-off selection may be preserved or revised as the user's intent permits. Use separate ranges for non-contiguous phases; fill pending summaries before commit. If deeper inspection would materially change the agreed treatment, surface the change instead of silently applying it.
54
+ Ask Question, questionnaire, and similar tool results may contain user expression. Judge by the speaker, not entry type. Retain the whole indivisible tool-exchange atom containing an important answer; split around it.
79
55
 
80
- Read `references/tool-interface.md` before retrying a rejected operation or when exact parameters, protected-atom causes, or measurements matter.
56
+ Routine acknowledgements can be candidates; task completion alone does not invalidate important statements.
81
57
 
82
- #### 5. Write replacement summaries
58
+ ### Propose and stop
83
59
 
84
- A replacement summary is successor context for a future Agent, not a transcript or a prompt to perform compression. State the resulting knowledge and working state directly.
60
+ Use conversation landmarks to explain replacements, originals kept, and summary contents. Offer alternatives for meaningful tradeoffs, not every delegated choice.
85
61
 
86
- Preserve, when applicable:
62
+ Stop when the goal is reasonably served or further compression is not worth the loss, not when all eligible content is covered. Re-align if findings materially change the treatment.
87
63
 
88
- - user intent and active constraints;
89
- - decisions, conclusions, and necessary rationale;
90
- - relevant files, symbols, interfaces, commands, or configuration;
91
- - completed changes and validation results;
92
- - rejected approaches only when their failure reason still matters;
93
- - unresolved issues and any established next step or continuation state.
64
+ ## Write a local replacement
94
65
 
95
- Remove repetitive exploration, superseded hypotheses, raw output captured by a conclusion, and chronology with no remaining consequence. Do not turn uncertainty into fact, invent a next action, or use references that only make sense inside the removed text. If retained endpoint messages already carry part of the intended context, do not duplicate them mechanically; use the summary to preserve what would otherwise be lost.
66
+ Summaries belong between retained earlier and later context, not as compression instructions or standalone handoffs.
96
67
 
97
- Organize by future utility rather than original chronology. When useful, use this compact frame without forcing empty fields:
68
+ Preserve necessary causes, consequential actions and findings, and resulting knowledge. Name important files, symbols, commands, or artifacts with their roles. Distinguish proposals from changes and assumptions from observed results. Length follows information needs, not a fixed ratio.
98
69
 
99
- ```text
100
- Goal and constraints:
101
- Established state and decisions:
102
- Artifacts and validation:
103
- Open issues and established next state:
104
- ```
70
+ Preserve consequential process, not activity logs. Replace vague “files changed, tests passed” with specific evidence: “Compared caller and parser, found ignored boundary arguments, added rejection, and verified an invalid-argument test.” Include relevant paths and results from the source.
105
71
 
106
- Length follows the information that must survive, not a target ratio. Final test: could a fresh Agent continue correctly from this summary plus retained context, without repeating work or violating a prior decision?
72
+ ### Fit the retained surroundings
107
73
 
108
- #### 6. Verify and hand off
74
+ Read enough surrounding context to avoid duplication and preserve what it depends on from this range. Resolve dangling references without inventing facts. Recall is recovery, not a substitute for sufficient summaries.
109
75
 
110
- Call `request={action:"plan", op:"show"}`. Check that the intended semantic phases are covered, KEEP holes remain outside ranges, every range has a summary, and the summaries conserve the future working state.
76
+ A range's end is a historical boundary, not automatically the present state:
111
77
 
112
- Describe the completed proposal with the same recognizable landmarks used during alignment. Direct the user to `/midcompact:select` or `/midcompact:select-webui` for boundaries and KEEP holes, and to `/midcompact:review` or `/midcompact:review-webui` for summary inspection or rejection. Use browser variants when the TUI is unavailable or preferred. Ask the user to run `/midcompact:commit` when ready; never commit for them.
78
+ - Do not turn an old unfinished task into a current TODO. Omit it if later retained context resolves it and its earlier status adds nothing.
79
+ - If the earlier status explains later events, describe it as historical. Do not attribute a later result to work within this range.
80
+ - Include current open issues and established next steps only when the range reaches the current working frontier and continuation needs them. Never invent a next action.
113
81
 
114
- ## Recall compressed content
82
+ If later retained turns test and reject approach A, “Next: validate A” is stale. Preserve why A was proposed if needed to explain the rejection; leave later outcomes where they belong.
115
83
 
116
- Recall works with or without an active planning transaction. It reads committed blocks active on the current branch without changing projection or DraftPlan.
84
+ ## Build, check, and hand off
117
85
 
118
- ### Recall workflow
86
+ Resolve boundaries with targeted lookups; split around protected atoms. Use `plan_add`, `plan_update`, and `plan_remove`. Updates change summary/topic; boundaries require remove + add. Fill pending summaries before handoff.
119
87
 
120
- #### 1. Find the block
88
+ Call `plan_show`; use `plan_read` when a stored summary needs full review. Check both:
121
89
 
122
- If its id is unknown, call `request={action:"recall", pattern:"..."}` to search active topics and summaries. A projected summary also states its block id and exact recall call.
90
+ - **Selection:** Does each range serve the user's direction? Are important originals and intended KEEP holes retained? Has unnecessary full-history coverage crept in?
91
+ - **Replacement:** Mentally remove the originals. Do summaries plus retained context explain the necessary causes, actions, evidence, and outcomes without gaps, vague claims, stale instructions, or state conflicts?
123
92
 
124
- #### 2. Retrieve the detail
93
+ Report replacements, retained originals, and stopping rationale. Boundaries: `/midcompact:select` or `/midcompact:select-webui`. Summaries: `/midcompact:review` or `/midcompact:review-webui`. Prefer browser variants when TUI is unavailable or unwanted. Only the user runs `/midcompact:commit`.
125
94
 
126
- Call `request={action:"recall", ref:"c0001"}`. If the readable, structure-flattened result ends with a truncation marker, retry with `detail="full"`. Retrieve only what the current task needs; do not start or change a plan merely to recall history.
95
+ ## Recall
127
96
 
128
- ## Tool conventions
97
+ Recall reads active committed blocks without changing a plan and needs no transaction. If the id is unknown, use `request={action:"recall_list",pattern:"..."}` to search ids, topics, and summaries, not originals. Projected summaries carry block ids and recall calls.
129
98
 
130
- - `g0001` labels an inventory group; use its `a...` span as internal landmarks.
131
- - `a0001` is a transaction-local atom ref; `d1` is a DraftPlan range id; `c0001` is an active committed-block id.
132
- - Protected atoms cannot enter a range. Split around them.
99
+ Use `request={action:"recall_read",block:"c0001"}`; retry truncation with `detail:"full"`. If still truncated, read `references/tool-interface.md`. Retrieve only needed content.
133
100
 
134
- These refs are planning handles, not the primary way to explain compression to the user. Read `references/tool-interface.md` for exact defaults and limits, rejected operations, protected-atom causes, measurements, repeated compression, or recall truncation.
101
+ `a...` refs are transaction-local; `d...` identifies a plan range; `c...` a committed block. Inventory `g...` labels are display-only; use listed atom refs for calls.
@@ -1,87 +1,69 @@
1
- # Midcompact Tool Interface
1
+ # Tool Call Patterns
2
2
 
3
- Read this reference when exact call requirements, limits, rejection behavior, repeated compression, or recall truncation matters. The main workflow remains in `../SKILL.md`.
3
+ Use the schema for fields/defaults. Follow only the steps needed. Example refs and summaries must be replaced with verified content.
4
4
 
5
- ## Parameter grouping
5
+ ## Survey where context space goes
6
6
 
7
- The parameters are one object with a single `request` field; `request` is a discriminated union where each branch binds one `action` value to exactly that action's fields and is closed (`additionalProperties: false`). A call that mixes actions (for example `locate` fields on `plan`) is rejected by the schema itself; do not repair it by dropping fields, re-issue the call with only the selected action's parameters. Shared field names (`ref`, `pattern`, `limit`, `detail`) are defined independently per action with the meaning documented in that section.
7
+ Start a fresh Agent-direct transaction with:
8
8
 
9
- ## Inspect
10
-
11
- Without `spans`, `request.action="inspect"` inventories the frozen anchor. It returns factual structure and bounded user landmarks, not full message bodies, assistant/tool previews, summaries, or image base64.
12
-
13
- - `page_size`: default 20 groups, maximum 50.
14
- - `cursor`: opaque value returned by the previous page.
15
- - Results include group refs, atom spans, content chars, image facts, protected/compressible counts, and Pi-reported anchor usage.
16
-
17
- Stop paging after the candidate regions are covered.
18
-
19
- To compare explicit candidates without mutating the DraftPlan, pass one or more possibly overlapping spans:
20
-
21
- ```text
22
- midcompact(request={action:"inspect", spans=[
23
- {"start":"a0006","end":"a0014"},
24
- {"start":"a0006","end":"a0020"}
25
- ]})
9
+ ```json
10
+ {"request":{"action":"inspect"}}
26
11
  ```
27
12
 
28
- Span inspection reports bounded endpoint landmarks, atom/message and role counts, tool exchanges and calls, factual content share, images, and protected/compressible counts. It has a 12,000-character total output budget and reports how many requested spans fit. It does not report per-span tokens: Pi supplies usage for the whole anchor, not token attribution by range. Do not combine `spans` with `page_size` or `cursor`.
29
-
30
- ## Locate
13
+ Read overall Pi token usage and chronological groups: user landmarks, endpoints, characters, images, protection counts. This paginated volume distribution is not a per-group token curve. Large groups invite investigation, not automatic compression.
31
14
 
32
- `request.action="locate"` returns atoms from the frozen anchor. Supply either:
15
+ Continue with the returned `cursor` only where relevant. Use listed `a...` refs, not display-only `g...` labels. Landmarks are not full content; combine the overview with user preferences before selection.
33
16
 
34
- - `ref`: one direct atom lookup; or
35
- - at least one real filter: `pattern`, `tool_name`, or `source` other than `any`.
17
+ ## Find evidence and check boundaries
36
18
 
37
- With no lookup or filter it returns no matches rather than an error. Filtered searches return at most three brief candidates; when more match, the result reports the total and asks for a more specific pattern or additional filter. `direction` is `oldest` by default or `newest`; `limit` may request one to three results.
19
+ For a recent bash result containing a known phrase:
38
20
 
39
- Brief direct lookups preserve both ends of an atom landmark. Pattern searches show a bounded excerpt around the match rather than the atom prefix. `detail="full"` is allowed only with one direct `ref` and preserves both ends with an explicit middle-omission marker when the 12,000-character atom limit is exceeded.
21
+ ```json
22
+ {"request":{"action":"locate_search","source":"tool_result","tool_name":"bash","pattern":"timeout","direction":"newest"}}
23
+ ```
40
24
 
41
- A `g...` inventory ref is not a locate ref; use the group's `a...` start/end landmarks.
25
+ Filters combine with AND. Use a returned atom ref to inspect the match:
42
26
 
43
- ## Plan
27
+ ```json
28
+ {"request":{"action":"locate_ref","ref":"a0007","detail":"full"}}
29
+ ```
44
30
 
45
- `request.action="plan"` uses `op="show"` by default. Agent and user mutate the same DraftPlan.
31
+ Known refs need no search. Excerpts are incomplete; `full` can omit the middle. Check markers before drawing conclusions. Narrow excessive matches with filters. Recover unknown refs using `inspect` or `locate_search`; `measure` needs known refs.
46
32
 
47
- | op | Required fields |
48
- |----|-----------------|
49
- | `show` | none; optional `draft_id` for one range |
50
- | `add` | `start`, `end`; optional `summary`, `topic` |
51
- | `update` | `draft_id` and at least one of `summary`, `topic` |
52
- | `remove` | `draft_id` |
33
+ ## Compare candidates without changing the plan
53
34
 
54
- Default show lists each range with bounded `from`, `to`, and `summary` landmarks plus factual metrics. Use `op="show", detail="full", draft_id="d1"` for one stored summary and endpoint previews under a 40,000-character total budget; full detail without `draft_id` is rejected. Show is read-only and does not persist a duplicate DraftPlan entry.
35
+ ```json
36
+ {"request":{"action":"measure","candidates":[{"start":"a0006","end":"a0014"},{"start":"a0006","end":"a0020"}]}}
37
+ ```
55
38
 
56
- Add/update return the changed range in brief form; remove returns its id and compact draft totals. Mutation responses omit the Pi-awareness header and unchanged ranges. Use explicit show when complete awareness is needed.
39
+ Candidates may overlap without changing the plan. Compare landmarks, content share, images, and protection, not assumed token savings. If fewer candidates are reported than requested, measure the remainder separately.
57
40
 
58
- An omitted or empty `summary` creates a pending range. Review can open it, but commit requires at least one range and a non-empty summary for every range. Changing boundaries requires removing the old range and adding the replacement.
41
+ ## Keep important originals between ranges
59
42
 
60
- ### Add rejection conditions
43
+ If an important user answer occupies `a0010`, locate it and verify boundaries, then add separate ranges on its two sides:
61
44
 
62
- | Condition | Response |
63
- |-----------|----------|
64
- | The range crosses a protected atom | Split the range around that atom |
65
- | The range overlaps an existing draft range | Remove or replace the existing range first |
66
- | `start` occurs after `end` | Correct the positional order |
67
- | An atom ref is unknown | Re-run inspect/locate against the current transaction snapshot |
45
+ ```json
46
+ {"request":{"action":"plan_add","start":"a0006","end":"a0009","summary":"<replacement for the earlier work>"}}
47
+ {"request":{"action":"plan_add","start":"a0011","end":"a0014","summary":"<replacement for the later work>"}}
48
+ ```
68
49
 
69
- An atom is protected when its tool protocol is incomplete or orphaned, it represents an existing compressed block, its message kind is unsupported, or it lacks the persistent session entry needed to anchor compression.
50
+ Question-tool answers are user expression. Keep the whole tool-exchange atom containing an important answer; do not split a call from its results. Unlike measurement candidates, plan ranges cannot overlap or contain protected atoms.
70
51
 
71
- ## Telemetry
52
+ ## Refine an existing plan
72
53
 
73
- Explicit `plan show` includes Pi-reported anchor usage when available and factual draft measurements: original/replacement content chars, image count, and decoded payload bytes. Mutation results omit awareness and report only compact totals plus the changed range where one remains. Local character or image measurements are not converted into token savings or projected-token percentages. Use measurements to compare the proposal with user-directed depth, not as an optimization target.
54
+ Start with `plan_show`. Use `plan_read` with its returned `range_id` when the stored summary or endpoints need full review. Change summary/topic with `plan_update`.
74
55
 
75
- ## Repeated compression
56
+ For new boundaries, first read and retain the existing summary/topic and verify the replacement endpoints. Then `plan_remove` the old range and `plan_add` its replacement. Do not assume the new id is unchanged. Check the resulting plan; mutation replies show the changed item and compact totals, not all ranges. Fill pending summaries before handoff; only the user commits.
76
57
 
77
- Committed blocks appear as protected atoms in later transaction snapshots and cannot be compressed again. A later transaction may compress newly accumulated raw history around those blocks. Re-run inspect/locate because atom refs are transaction-local.
58
+ ## Work around protected content
78
59
 
79
- ## Recall
60
+ Split around protected atoms. Causes include incomplete tool exchanges, unsupported message kinds, missing persistent entries, and existing committed blocks. Later transactions can compress remaining raw history around those blocks, not recompress the blocks themselves. Obtain current atom refs from `inspect`; refs are transaction-local.
80
61
 
81
- `request.action="recall"` works independently of a planning transaction and reads committed blocks active on the current branch.
62
+ ## Retrieve committed evidence
82
63
 
83
- - Without `ref`, `pattern` searches block topics and summaries; `limit` defaults to 8 and has a maximum of 20.
84
- - With `ref="c0001"`, the tool renders that block's stored messages.
85
- - `detail="full"` raises the rendering cap when the normal result is truncated.
64
+ ```json
65
+ {"request":{"action":"recall_list","pattern":"timeout"}}
66
+ {"request":{"action":"recall_read","block":"c0001"}}
67
+ ```
86
68
 
87
- Recall has no paging. The truncation marker is `[truncated; refine the recall request or inspect the source session for more]`. If `detail="full"` still truncates before the needed detail, report that recall cannot expose the omitted content; do not infer it.
69
+ Use the actual block id returned by the list; skip listing when known. Listing searches ids/topics/summaries, not originals. Recall needs no transaction. If truncated, retry the same block with `detail:"full"`. There is no paging: if still truncated, report that recall cannot expose the omitted evidence. Do not infer it or start another transaction as a recovery workaround. Inspecting the source session requires a separate available means; this tool offers none.
package/src/SPEC.md CHANGED
@@ -71,12 +71,16 @@ package.json → `pi.extensions`).
71
71
  ## External contracts (reference, don't duplicate)
72
72
 
73
73
  - Tool: one `midcompact` tool whose parameters are `{ request: <union> }` —
74
- a root `type: "object"` wrapping a discriminated union on `action`
75
- (inspect/locate/plan/recall); each branch is closed
76
- (`additionalProperties: false`), so cross-action parameters are
77
- schema-rejected. The `request` wrapper exists because some providers
78
- (e.g. DeepSeek) reject a root-level `anyOf` before the model sees the
79
- schema.
74
+ a root `type: "object"` wrapping a union of 11 single-purpose actions
75
+ (inspect/measure/locate_ref/locate_search/plan_show/plan_read/plan_add/
76
+ plan_update/plan_remove/recall_list/recall_read); each branch is closed
77
+ (`additionalProperties: false`), so extra fields are rejected (schema where
78
+ enforced, runtime backstop everywhere — a silently ignored field is a bug).
79
+ Former second-level discriminators (plan `op`, locate ref-vs-filter, recall
80
+ list-vs-render, inspect inventory-vs-spans) are flattened into branches so
81
+ field legality is schema-visible. The `request` wrapper exists because some
82
+ providers (e.g. DeepSeek) reject a root-level `anyOf` before the model sees
83
+ the schema.
80
84
  Details: `skills/midcompact/references/tool-interface.md`.
81
85
  - Commands: `midcompact:start|abort|commit|review|review-webui|select|select-webui|status`;
82
86
  no composite `/midcompact`; native naming convention `name:sub` (Pi's
@@ -104,6 +108,7 @@ package.json → `pi.extensions`).
104
108
 
105
109
  ## Change rules
106
110
 
111
+ - Model-facing copy (schema descriptions, prompts, errors, notices, skill docs) follows `.dev/docs/agent-vocabulary.md`; a new action, field, or concept is registered there **before** implementation.
107
112
  - Adding a tool action → new request branch + handler type + tool-interface
108
113
  section + SKILL.md routing; adding parameters to an action → its branch
109
114
  only (the union stays nested under `request`).
package/src/index.ts CHANGED
@@ -63,13 +63,16 @@ import {
63
63
 
64
64
  const TOOL_NAME = "midcompact";
65
65
  const TOOL_DESCRIPTION =
66
- "Inventory, locate, draft, or recall mid-context compression. Use the `midcompact` skill to route planning versus recall; during an active transaction, follow the runtime prompt for the state-specific first action.";
66
+ "Inspect, measure, locate, plan, or recall mid-context compression. Use the `midcompact` skill to route planning versus recall; during an active transaction, follow the runtime prompt for the state-specific first action.";
67
67
  const STATUS_KEY = "midcompact";
68
68
  const START_PROMPT_PREFIX = "A mid-compaction transaction is active on a frozen anchor snapshot.";
69
69
 
70
- // Canonical request model: one branch per action, and each branch owns exactly
71
- // its own fields (additionalProperties: false). The discriminant is a
72
- // single-value StringEnum instead of Type.Literal so it serializes as
70
+ // Canonical request model: one branch per operation, and each branch owns
71
+ // exactly its own fields (additionalProperties: false). Second-level operation
72
+ // discriminators (the former plan `op`, locate ref-vs-filter, recall
73
+ // list-vs-render, inspect inventory-vs-spans) are flattened into top-level
74
+ // branches so field legality is visible in the schema itself. The discriminant
75
+ // is a single-value StringEnum instead of Type.Literal so it serializes as
73
76
  // string+enum, which restricted JSON-Schema subsets (e.g. DeepSeek) accept
74
77
  // more readily than const.
75
78
  //
@@ -80,66 +83,165 @@ const START_PROMPT_PREFIX = "A mid-compaction transaction is active on a frozen
80
83
  // skills/midcompact/references/tool-interface.md.
81
84
  const InspectRequest = Type.Object(
82
85
  {
83
- action: StringEnum(["inspect"] as const, { description: "Inventory the frozen anchor, or measure explicit candidate spans." }),
84
- spans: Type.Optional(Type.Array(Type.Object({ start: Type.String(), end: Type.String() }), { description: "Candidate spans to measure, as {start,end} atom refs." })),
85
- page_size: Type.Optional(Type.Number({ description: "Inventory groups per page (default 20, max 50)." })),
86
+ action: StringEnum(["inspect"] as const, { description: "Page through the frozen anchor inventory: groups, refs, sizes, protected/compressible counts." }),
87
+ page_size: Type.Optional(Type.Number({ description: "Groups per page (default 20, max 50; out-of-range values are clamped)." })),
86
88
  cursor: Type.Optional(Type.String({ description: "Pagination cursor from the previous page." })),
87
89
  },
88
90
  { additionalProperties: false },
89
91
  );
90
92
 
91
- const LocateRequest = Type.Object(
93
+ const MeasureRequest = Type.Object(
92
94
  {
93
- action: StringEnum(["locate"] as const, { description: "Locate atoms in the frozen anchor by ref or filters." }),
94
- ref: Type.Optional(Type.String({ description: "One direct atom ref; mutually exclusive with search filters." })),
95
- pattern: Type.Optional(Type.String({ description: "Content filter over anchor atoms." })),
96
- source: Type.Optional(StringEnum(["any", "user", "assistant", "tool_call", "tool_result"] as const, { description: "Filter by entry source." })),
97
- tool_name: Type.Optional(Type.String({ description: "Filter by originating tool name." })),
95
+ action: StringEnum(["measure"] as const, { description: "Measure candidate {start,end} atom spans without changing the plan." }),
96
+ candidates: Type.Array(
97
+ Type.Object({ start: Type.String({ description: "Candidate start atom ref." }), end: Type.String({ description: "Candidate end atom ref." }) }),
98
+ { minItems: 1, description: "Candidate spans to measure, e.g. [{start:\"a0006\",end:\"a0014\"}]." },
99
+ ),
100
+ },
101
+ { additionalProperties: false },
102
+ );
103
+
104
+ const LocateRefRequest = Type.Object(
105
+ {
106
+ action: StringEnum(["locate_ref"] as const, { description: "Look up one atom by ref in the frozen anchor." }),
107
+ ref: Type.String({ description: "Atom ref, e.g. a0001. Group labels shown by inspect are not atom refs." }),
108
+ detail: Type.Optional(StringEnum(["brief", "full"] as const, { description: "brief (default) bounded preview; full atom text up to 12,000 characters." })),
109
+ },
110
+ { additionalProperties: false },
111
+ );
112
+
113
+ const LocateSearchRequest = Type.Object(
114
+ {
115
+ action: StringEnum(["locate_search"] as const, { description: "Search anchor atoms with at least one filter; filters combine conjunctively (AND)." }),
116
+ pattern: Type.Optional(Type.String({ description: "Case-insensitive substring filter over atom text." })),
117
+ source: Type.Optional(StringEnum(["user", "assistant", "tool_call", "tool_result"] as const, { description: "Filter by entry source class." })),
118
+ tool_name: Type.Optional(Type.String({ description: "Exact (case-insensitive) match on the originating tool name." })),
98
119
  direction: Type.Optional(StringEnum(["oldest", "newest"] as const, { description: "Match ordering, oldest (default) or newest." })),
99
- limit: Type.Optional(Type.Number({ description: "1-3 results for filtered searches." })),
100
- detail: Type.Optional(StringEnum(["brief", "full"] as const, { description: "brief (default) or full atom output." })),
120
+ limit: Type.Optional(Type.Number({ description: "1-3 results (out-of-range values are clamped)." })),
121
+ },
122
+ { additionalProperties: false },
123
+ );
124
+
125
+ const PlanShowRequest = Type.Object(
126
+ {
127
+ action: StringEnum(["plan_show"] as const, { description: "List every plan range in brief form with plan telemetry." }),
101
128
  },
102
129
  { additionalProperties: false },
103
130
  );
104
131
 
105
- const PlanRequest = Type.Object(
132
+ const PlanReadRequest = Type.Object(
106
133
  {
107
- action: StringEnum(["plan"] as const, { description: "Show or mutate the shared DraftPlan." }),
108
- op: Type.Optional(StringEnum(["show", "add", "update", "remove"] as const, { description: "show (default) / add / update / remove." })),
109
- start: Type.Optional(Type.String({ description: "add: range start atom ref." })),
110
- end: Type.Optional(Type.String({ description: "add: range end atom ref." })),
111
- draft_id: Type.Optional(Type.String({ description: "show/update/remove: target draft range id." })),
112
- topic: Type.Optional(Type.String({ description: "add/update: range topic." })),
113
- summary: Type.Optional(Type.String({ description: "add/update: range summary (omitted or empty = pending range)." })),
114
- detail: Type.Optional(StringEnum(["brief", "full"] as const, { description: "show: brief (default) or full range output." })),
134
+ action: StringEnum(["plan_read"] as const, { description: "Read one plan range in full: stored summary and endpoint previews under a 40,000-character budget." }),
135
+ range_id: Type.String({ description: "Target range id from plan_show, e.g. d1." }),
115
136
  },
116
137
  { additionalProperties: false },
117
138
  );
118
139
 
119
- const RecallRequest = Type.Object(
140
+ const PlanAddRequest = Type.Object(
120
141
  {
121
- action: StringEnum(["recall"] as const, { description: "Read committed compression blocks; works without a transaction." }),
122
- ref: Type.Optional(Type.String({ description: "One committed block id, e.g. c0001; renders its messages." })),
123
- pattern: Type.Optional(Type.String({ description: "Filter block topics and summaries." })),
124
- limit: Type.Optional(Type.Number({ description: "Blocks to list (default 8, max 20)." })),
125
- detail: Type.Optional(StringEnum(["brief", "full"] as const, { description: "full raises the rendering cap on truncated blocks." })),
142
+ action: StringEnum(["plan_add"] as const, { description: "Add one range over contiguous atoms; boundaries are immutable after add." }),
143
+ start: Type.String({ description: "Range start atom ref." }),
144
+ end: Type.String({ description: "Range end atom ref (inclusive)." }),
145
+ summary: Type.Optional(Type.String({ description: "Replacement summary; omitted or empty leaves the range pending." })),
146
+ topic: Type.Optional(Type.String({ description: "Optional range topic." })),
147
+ },
148
+ { additionalProperties: false },
149
+ );
150
+
151
+ const PlanUpdateRequest = Type.Object(
152
+ {
153
+ action: StringEnum(["plan_update"] as const, { description: "Update one range's summary and/or topic; boundaries change via plan_remove + plan_add." }),
154
+ range_id: Type.String({ description: "Target range id, e.g. d1." }),
155
+ summary: Type.Optional(Type.String({ description: "New summary; empty string marks the range pending." })),
156
+ topic: Type.Optional(Type.String({ description: "New topic." })),
157
+ },
158
+ { additionalProperties: false },
159
+ );
160
+
161
+ const PlanRemoveRequest = Type.Object(
162
+ {
163
+ action: StringEnum(["plan_remove"] as const, { description: "Remove one range from the plan." }),
164
+ range_id: Type.String({ description: "Target range id, e.g. d1." }),
165
+ },
166
+ { additionalProperties: false },
167
+ );
168
+
169
+ const RecallListRequest = Type.Object(
170
+ {
171
+ action: StringEnum(["recall_list"] as const, { description: "List committed blocks; works without a transaction." }),
172
+ pattern: Type.Optional(Type.String({ description: "Case-insensitive filter over block id, topic, and summary (not original content)." })),
173
+ limit: Type.Optional(Type.Number({ description: "Blocks to list (default 8, max 20; out-of-range values are clamped)." })),
174
+ },
175
+ { additionalProperties: false },
176
+ );
177
+
178
+ const RecallReadRequest = Type.Object(
179
+ {
180
+ action: StringEnum(["recall_read"] as const, { description: "Render one committed block's original messages." }),
181
+ block: Type.String({ description: "Committed block id, e.g. c0001." }),
182
+ detail: Type.Optional(StringEnum(["brief", "full"] as const, { description: "full raises the rendering cap from 12,000 to 40,000 characters on truncated blocks." })),
126
183
  },
127
184
  { additionalProperties: false },
128
185
  );
129
186
 
130
187
  const Params = Type.Object(
131
- { request: Type.Union([InspectRequest, LocateRequest, PlanRequest, RecallRequest]) },
188
+ {
189
+ request: Type.Union([
190
+ InspectRequest,
191
+ MeasureRequest,
192
+ LocateRefRequest,
193
+ LocateSearchRequest,
194
+ PlanShowRequest,
195
+ PlanReadRequest,
196
+ PlanAddRequest,
197
+ PlanUpdateRequest,
198
+ PlanRemoveRequest,
199
+ RecallListRequest,
200
+ RecallReadRequest,
201
+ ]),
202
+ },
132
203
  {
133
204
  additionalProperties: false,
134
- description: "`request.action` selects exactly one request shape; fields of the other actions are not valid.",
205
+ description: "`request.action` selects exactly one request shape; each shape accepts only its own fields.",
135
206
  },
136
207
  );
137
208
 
138
209
  type ToolParams = Static<typeof Params>;
139
210
  type InspectRequestType = Static<typeof InspectRequest>;
140
- type LocateRequestType = Static<typeof LocateRequest>;
141
- type PlanRequestType = Static<typeof PlanRequest>;
142
- type RecallRequestType = Static<typeof RecallRequest>;
211
+ type MeasureRequestType = Static<typeof MeasureRequest>;
212
+ type LocateRefRequestType = Static<typeof LocateRefRequest>;
213
+ type LocateSearchRequestType = Static<typeof LocateSearchRequest>;
214
+ type PlanShowRequestType = Static<typeof PlanShowRequest>;
215
+ type PlanReadRequestType = Static<typeof PlanReadRequest>;
216
+ type PlanAddRequestType = Static<typeof PlanAddRequest>;
217
+ type PlanUpdateRequestType = Static<typeof PlanUpdateRequest>;
218
+ type PlanRemoveRequestType = Static<typeof PlanRemoveRequest>;
219
+ type RecallListRequestType = Static<typeof RecallListRequest>;
220
+ type RecallReadRequestType = Static<typeof RecallReadRequest>;
221
+
222
+ // Runtime closure backstop: providers are not trusted to enforce
223
+ // additionalProperties at call time, and a silently ignored field is worse
224
+ // than a rejection. Keys are the per-branch optional/required fields besides
225
+ // the discriminant.
226
+ const BRANCH_FIELDS: Record<ToolParams["request"]["action"], readonly string[]> = {
227
+ inspect: ["page_size", "cursor"],
228
+ measure: ["candidates"],
229
+ locate_ref: ["ref", "detail"],
230
+ locate_search: ["pattern", "source", "tool_name", "direction", "limit"],
231
+ plan_show: [],
232
+ plan_read: ["range_id"],
233
+ plan_add: ["start", "end", "summary", "topic"],
234
+ plan_update: ["range_id", "summary", "topic"],
235
+ plan_remove: ["range_id"],
236
+ recall_list: ["pattern", "limit"],
237
+ recall_read: ["block", "detail"],
238
+ };
239
+
240
+ function rejectExtraFields(request: ToolParams["request"]): void {
241
+ const allowed: readonly string[] = BRANCH_FIELDS[request.action];
242
+ const extras = Object.keys(request).filter((key) => key !== "action" && !allowed.includes(key));
243
+ if (extras.length > 0) throw new Error(`${request.action} does not accept: ${extras.join(", ")}.`);
244
+ }
143
245
 
144
246
  type RuntimeSnapshot = { atoms: Atom[]; anchorState?: CompressionState };
145
247
 
@@ -193,9 +295,9 @@ export default function (pi: ExtensionAPI) {
193
295
  message: {
194
296
  customType: "midcompact-handoff",
195
297
  content: [
196
- "An active midcompact transaction exists with a persisted DraftPlan.",
197
- `Draft revision ${currentDraft.revision}; ${currentDraft.ranges.length} existing range(s), which may have been created by the user.`,
198
- "If the current user request asks to continue midcompact, read the `midcompact` skill first, then call midcompact(request={action:\"plan\", op:\"show\"}) before any other midcompact action. Treat the existing plan as the current shared draft. Infer from the user's request whether to preserve, refine, or extend it; ask only if materially ambiguous.",
298
+ "An active midcompact transaction exists with a persisted plan.",
299
+ `Plan revision ${currentDraft.revision}; ${currentDraft.ranges.length} existing range(s), which may have been created by the user.`,
300
+ "If the current user request asks to continue midcompact, read the `midcompact` skill first, then call midcompact(request={action:\"plan_show\"}) before any other midcompact action. Treat the existing plan as the shared starting point. Infer from the user's request whether to preserve, refine, or extend it; ask only if materially ambiguous.",
199
301
  ].join("\n"),
200
302
  display: false,
201
303
  },
@@ -239,21 +341,21 @@ export default function (pi: ExtensionAPI) {
239
341
  },
240
342
  });
241
343
  pi.registerCommand("midcompact:commit", {
242
- description: "Commit the current draft to the branch state",
344
+ description: "Commit the current plan to the branch state",
243
345
  handler: async (_args: string, ctx: ExtensionCommandContext) => {
244
346
  await ctx.waitForIdle();
245
347
  return commitTransaction(ctx);
246
348
  },
247
349
  });
248
350
  pi.registerCommand("midcompact:review", {
249
- description: "Open the interactive TUI review to inspect and edit the draft",
351
+ description: "Open the interactive TUI review to inspect and edit the plan",
250
352
  handler: async (_args: string, ctx: ExtensionCommandContext) => {
251
353
  await ctx.waitForIdle();
252
354
  return reviewTransaction(ctx, "tui");
253
355
  },
254
356
  });
255
357
  pi.registerCommand("midcompact:review-webui", {
256
- description: "Open a local web page to inspect and edit the draft (works without TUI)",
358
+ description: "Open a local web page to inspect and edit the plan (works without TUI)",
257
359
  handler: async (_args: string, ctx: ExtensionCommandContext) => {
258
360
  await ctx.waitForIdle();
259
361
  return reviewTransaction(ctx, "web");
@@ -274,7 +376,7 @@ export default function (pi: ExtensionAPI) {
274
376
  },
275
377
  });
276
378
  pi.registerCommand("midcompact:status", {
277
- description: "Show current transaction and draft status",
379
+ description: "Show current transaction and plan status",
278
380
  handler: async (_args: string, ctx: ExtensionCommandContext) => {
279
381
  await ctx.waitForIdle();
280
382
  return showStatus(ctx);
@@ -346,7 +448,7 @@ export default function (pi: ExtensionAPI) {
346
448
  return;
347
449
  }
348
450
  if (!tryAcquireUi(planningLock)) {
349
- ctx.ui.notify("The Agent is currently processing the midcompact draft. Try Selection after the Agent turn ends.", "warning");
451
+ ctx.ui.notify("The Agent is currently processing the midcompact plan. Try Selection after the Agent turn ends.", "warning");
350
452
  return;
351
453
  }
352
454
 
@@ -364,12 +466,12 @@ export default function (pi: ExtensionAPI) {
364
466
  if (action.action === "save") {
365
467
  try {
366
468
  applySelection(action.spans ?? [], action.keepRefs ?? []);
367
- ctx.ui.notify("DraftPlan saved. Tell the Agent to continue processing it when ready.", "info");
469
+ ctx.ui.notify("Plan saved. Tell the Agent to continue processing it when ready.", "info");
368
470
  } catch (error) {
369
471
  ctx.ui.notify(`Selection could not be saved: ${error instanceof Error ? error.message : String(error)}`, "warning");
370
472
  }
371
473
  } else {
372
- ctx.ui.notify("Selection closed. The DraftPlan remains available; reopen select or tell the Agent to continue.", "info");
474
+ ctx.ui.notify("Selection closed. The plan remains available; reopen select or tell the Agent to continue.", "info");
373
475
  }
374
476
  return;
375
477
  }
@@ -386,7 +488,7 @@ export default function (pi: ExtensionAPI) {
386
488
  updateStatus(ctx, currentTx, draft, planningLock.owner);
387
489
  },
388
490
  }, "selection", { openBrowser: openReviewWebBrowser });
389
- ctx.ui.notify("Selection closed. The DraftPlan is saved; tell the Agent to continue when ready.", "info");
491
+ ctx.ui.notify("Selection closed. The plan is saved; tell the Agent to continue when ready.", "info");
390
492
  } finally {
391
493
  releaseUi(planningLock);
392
494
  }
@@ -397,17 +499,17 @@ export default function (pi: ExtensionAPI) {
397
499
  const promptLines = [
398
500
  START_PROMPT_PREFIX,
399
501
  awareness,
400
- "The extension provides inspect for bounded inventory, locate for local details, plan show/add/update/remove for one shared DraftPlan, and recall for committed blocks.",
401
- "The user owns the final compression decision. You may edit the DraftPlan, but you must not commit. Preserve facts that future work still needs; local character and image counts are not token estimates.",
502
+ "The extension provides inspect for the bounded inventory, measure for candidate spans, locate for atom details, plan_show/plan_read/plan_add/plan_update/plan_remove for one shared plan, and recall_list/recall_read for committed blocks.",
503
+ "The user owns the final compression decision. You may edit the plan, but you must not commit. Preserve facts that future work still needs; local character and image counts are not token estimates.",
402
504
  ];
403
505
  if (customInstructions) promptLines.push(`User focus: ${customInstructions}`);
404
506
  if (mode === "agent") {
405
507
  promptLines.push(
406
- "FINAL STATE: AGENT DIRECT. The new DraftPlan is empty. Read the `midcompact` skill before doing any planning work, then call inspect first and use locate and plan to create ranges and summaries. Stop before commit.",
508
+ "FINAL STATE: AGENT DIRECT. The new plan is empty. Read the `midcompact` skill before doing any planning work, then call inspect first and use measure, locate, and the plan actions to create ranges and summaries. Stop before commit.",
407
509
  );
408
510
  } else {
409
511
  promptLines.push(
410
- "FINAL STATE: USER MANUAL. The user is about to edit the initial DraftPlan. Acknowledge with OK only. Do not call any midcompact tool, inspect, locate, plan, or recall; do not change the draft or commit. Wait until the user finishes editing and sends a later request. On that later request, read the `midcompact` skill before doing any planning work, then call plan show first.",
512
+ "FINAL STATE: USER MANUAL. The user is about to edit the initial plan. Acknowledge with OK only. Do not call any midcompact action; do not change the plan or commit. Wait until the user finishes editing and sends a later request. On that later request, read the `midcompact` skill before doing any planning work, then call plan_show first.",
411
513
  );
412
514
  }
413
515
  await pi.sendUserMessage(promptLines.join("\n"));
@@ -415,7 +517,7 @@ export default function (pi: ExtensionAPI) {
415
517
 
416
518
  async function abortTransaction(ctx: ExtensionCommandContext): Promise<void> {
417
519
  if (planningLock.owner === "agent") {
418
- ctx.ui.notify("The Agent is currently processing the midcompact draft. Abort after the Agent turn ends.", "warning");
520
+ ctx.ui.notify("The Agent is currently processing the midcompact plan. Abort after the Agent turn ends.", "warning");
419
521
  return;
420
522
  }
421
523
  const restored = restoreTransaction(ctx.sessionManager.getBranch() as SessionEntry[]);
@@ -439,7 +541,7 @@ export default function (pi: ExtensionAPI) {
439
541
 
440
542
  async function commitTransaction(ctx: ExtensionCommandContext): Promise<void> {
441
543
  if (planningLock.owner === "agent") {
442
- ctx.ui.notify("The Agent is currently processing the midcompact draft. Commit after the Agent turn ends.", "warning");
544
+ ctx.ui.notify("The Agent is currently processing the midcompact plan. Commit after the Agent turn ends.", "warning");
443
545
  return;
444
546
  }
445
547
  const restored = restoreTransaction(ctx.sessionManager.getBranch() as SessionEntry[]);
@@ -450,7 +552,7 @@ export default function (pi: ExtensionAPI) {
450
552
  return;
451
553
  }
452
554
  if (!currentDraft?.ranges.length) {
453
- ctx.ui.notify("Draft is empty; nothing to commit.", "warning");
555
+ ctx.ui.notify("Plan is empty; nothing to commit.", "warning");
454
556
  return;
455
557
  }
456
558
  // Commit validation: reject empty summary, invalid boundaries, overlaps, protected atoms.
@@ -508,7 +610,7 @@ export default function (pi: ExtensionAPI) {
508
610
  return;
509
611
  }
510
612
  if (!tryAcquireUi(planningLock)) {
511
- ctx.ui.notify("The Agent is currently processing the midcompact draft. Try opening review after the Agent turn ends.", "warning");
613
+ ctx.ui.notify("The Agent is currently processing the midcompact plan. Try opening review after the Agent turn ends.", "warning");
512
614
  return;
513
615
  }
514
616
  try {
@@ -579,7 +681,7 @@ export default function (pi: ExtensionAPI) {
579
681
  /** Agent tool path: all active-transaction operations yield to an editing UI. */
580
682
  function requireAgentAccess(ctx: ExtensionContext): boolean {
581
683
  if (!acquireAgent(planningLock)) {
582
- ctx.ui.notify("A Selection/Review UI is currently editing the midcompact draft. Close it before the Agent can continue.", "warning");
684
+ ctx.ui.notify("A Selection/Review UI is currently editing the midcompact plan. Close it before the Agent can continue.", "warning");
583
685
  return false;
584
686
  }
585
687
  return true;
@@ -600,7 +702,9 @@ export default function (pi: ExtensionAPI) {
600
702
  async execute(_id: string, params: ToolParams, _signal: AbortSignal | undefined, _onUpdate: unknown, ctx: ExtensionContext) {
601
703
  try {
602
704
  const request = params.request;
603
- if (request.action === "recall") return toolResult(handleRecall(request, ctx));
705
+ rejectExtraFields(request);
706
+ if (request.action === "recall_list") return toolResult(handleRecallList(request, ctx));
707
+ if (request.action === "recall_read") return toolResult(handleRecallRead(request, ctx));
604
708
  const restored = restoreTransaction(ctx.sessionManager.getBranch() as SessionEntry[]);
605
709
  const currentTx = withCompatDefaults(restored.transaction ?? transaction);
606
710
  if (!currentTx) return toolResult("No active midcompact transaction. Ask the user to run `/midcompact:start` first.");
@@ -611,17 +715,22 @@ export default function (pi: ExtensionAPI) {
611
715
  }
612
716
  const snapshot = buildAnchorSnapshot(ctx.sessionManager, currentTx);
613
717
 
614
- if (request.action === "inspect") return toolResult(handleInspect(request, snapshot.atoms, currentTx));
615
- if (request.action === "locate") return toolResult(handleLocate(request, snapshot.atoms));
616
- if (request.action === "plan") {
617
- const result = handlePlan(request, draft!, snapshot.atoms);
618
- if (result.op === "show") {
619
- return toolResult(formatDraft(draft!, draftTelemetry(transaction, draft), {
620
- detail: request.detail,
621
- draftId: request.draft_id,
622
- atoms: snapshot.atoms,
623
- }));
624
- }
718
+ if (request.action === "inspect") return toolResult(handleInventory(request, snapshot.atoms, currentTx));
719
+ if (request.action === "measure") return toolResult(handleMeasure(request, snapshot.atoms));
720
+ if (request.action === "locate_ref") return toolResult(handleLocateRef(request, snapshot.atoms));
721
+ if (request.action === "locate_search") return toolResult(handleLocateSearch(request, snapshot.atoms));
722
+ if (request.action === "plan_show") {
723
+ return toolResult(formatDraft(draft!, draftTelemetry(transaction, draft), { atoms: snapshot.atoms }));
724
+ }
725
+ if (request.action === "plan_read") {
726
+ return toolResult(formatDraft(draft!, draftTelemetry(transaction, draft), {
727
+ detail: "full",
728
+ draftId: request.range_id,
729
+ atoms: snapshot.atoms,
730
+ }));
731
+ }
732
+ if (request.action === "plan_add" || request.action === "plan_update" || request.action === "plan_remove") {
733
+ const result = handlePlanMutation(draft!, snapshot.atoms, request);
625
734
  draft = result.draft;
626
735
  pi.appendEntry(DRAFT_ENTRY, draft);
627
736
  updateStatus(ctx, transaction, draft, planningLock.owner);
@@ -634,31 +743,37 @@ export default function (pi: ExtensionAPI) {
634
743
  },
635
744
  });
636
745
 
637
- function handleInspect(params: InspectRequestType, atoms: Atom[], tx: TransactionState): string {
638
- if (params.spans) {
639
- if (params.page_size !== undefined || params.cursor !== undefined) {
640
- throw new Error("inspect spans cannot be combined with inventory pagination.");
641
- }
642
- return formatSpanInspection(atoms, params.spans);
643
- }
746
+ function handleInventory(params: InspectRequestType, atoms: Atom[], tx: TransactionState): string {
644
747
  const page = buildInventory(atoms, { pageSize: params.page_size, cursor: params.cursor }, { transaction: tx });
645
748
  return formatInventory(page);
646
749
  }
647
750
 
648
- function handleRecall(params: RecallRequestType, ctx: ExtensionContext): string {
751
+ function handleMeasure(params: MeasureRequestType, atoms: Atom[]): string {
752
+ if (!params.candidates?.length) throw new Error("measure requires at least one start/end candidate.");
753
+ return formatSpanInspection(atoms, params.candidates);
754
+ }
755
+
756
+ function restoreBranchState(ctx: ExtensionContext): CompressionState | undefined {
757
+ return restoreCompressionState(ctx.sessionManager.getBranch() as SessionEntry[]) ?? activeState;
758
+ }
759
+
760
+ function handleRecallList(params: RecallListRequestType, ctx: ExtensionContext): string {
761
+ const branchState = restoreBranchState(ctx);
762
+ if (!branchState?.blocks.length) return "No compressed blocks are active on this branch.";
763
+ const query = (params.pattern ?? "").trim().toLocaleLowerCase();
764
+ const matches = branchState.blocks.filter((block) => !query || `${block.id}\n${block.topic ?? ""}\n${block.summary}`.toLocaleLowerCase().includes(query));
765
+ if (!matches.length) return "No compressed blocks matched.";
766
+ return matches.slice(0, Math.max(1, Math.min(params.limit ?? 8, 20))).map((block) =>
767
+ `${block.id}${block.topic ? ` | ${block.topic}` : ""} | ${block.originalContentChars ?? 0} original chars${block.originalImageCount ? ` · ${block.originalImageCount} images` : ""}\n${block.summary}`
768
+ ).join("\n\n");
769
+ }
770
+
771
+ function handleRecallRead(params: RecallReadRequestType, ctx: ExtensionContext): string {
649
772
  const sm = ctx.sessionManager;
650
- const branchState = restoreCompressionState(sm.getBranch() as SessionEntry[]) ?? activeState;
773
+ const branchState = restoreBranchState(ctx);
651
774
  if (!branchState?.blocks.length) return "No compressed blocks are active on this branch.";
652
- if (!params.ref) {
653
- const query = (params.pattern ?? "").trim().toLocaleLowerCase();
654
- const matches = branchState.blocks.filter((block) => !query || `${block.id}\n${block.topic ?? ""}\n${block.summary}`.toLocaleLowerCase().includes(query));
655
- if (!matches.length) return "No compressed blocks matched.";
656
- return matches.slice(0, Math.max(1, Math.min(params.limit ?? 8, 20))).map((block) =>
657
- `${block.id}${block.topic ? ` | ${block.topic}` : ""} | ${block.originalContentChars ?? 0} original chars${block.originalImageCount ? ` · ${block.originalImageCount} images` : ""}\n${block.summary}`
658
- ).join("\n\n");
659
- }
660
- const block = branchState.blocks.find((candidate) => candidate.id === params.ref);
661
- if (!block) return `Unknown compressed block ${params.ref}.`;
775
+ const block = branchState.blocks.find((candidate) => candidate.id === params.block);
776
+ if (!block) return `Unknown compressed block ${params.block}.`;
662
777
  const byId = new Map((sm.getEntries() as SessionEntry[]).map((entry) => [entry.id, entry]));
663
778
  const parts: string[] = [];
664
779
  for (const id of block.entryIds) {
@@ -681,59 +796,58 @@ export default function (pi: ExtensionAPI) {
681
796
 
682
797
  // ---- Pure handlers ----
683
798
 
684
- function handleLocate(params: LocateRequestType, atoms: Atom[]): string {
685
- const hasFilter = Boolean(params.pattern || params.tool_name || (params.source && params.source !== "any"));
686
- if (params.ref && hasFilter) {
687
- throw new Error("locate accepts either one direct ref or search filters, not both.");
688
- }
689
- if (params.detail === "full" && !params.ref) {
690
- throw new Error("locate detail=full requires one direct atom ref.");
799
+ function handleLocateRef(params: LocateRefRequestType, atoms: Atom[]): string {
800
+ const result = locateAtomMatches(atoms, { ref: params.ref });
801
+ if (!result.atoms.length) return "No matching atoms in the frozen anchor snapshot.";
802
+ return formatLocatedAtom(result.atoms[0]!, params.detail ?? "brief");
803
+ }
804
+
805
+ function handleLocateSearch(params: LocateSearchRequestType, atoms: Atom[]): string {
806
+ if (!params.pattern && !params.tool_name && !params.source) {
807
+ throw new Error("locate_search requires at least one filter: pattern, tool_name, or source.");
691
808
  }
692
809
  const result = locateAtomMatches(atoms, {
693
- ref: params.ref,
694
810
  pattern: params.pattern,
695
811
  source: params.source,
696
812
  toolName: params.tool_name,
697
813
  direction: params.direction,
698
814
  limit: params.limit,
699
- detail: params.detail,
700
815
  });
701
816
  if (!result.atoms.length) return "No matching atoms in the frozen anchor snapshot.";
702
817
  const rendered = result.atoms
703
- .map((atom) => formatLocatedAtom(atom, params.detail ?? "brief", params.pattern))
818
+ .map((atom) => formatLocatedAtom(atom, "brief", params.pattern))
704
819
  .join("\n\n---\n\n");
705
820
  if (result.totalMatches <= result.atoms.length) return rendered;
706
821
  return [
707
- `Showing ${result.atoms.length} of ${result.totalMatches} matches (${params.direction ?? "oldest"} first). Refine pattern or add source, tool_name, or direction.`,
822
+ `Showing ${result.atoms.length} of ${result.totalMatches} matches (${params.direction ?? "oldest"} first). Refine pattern or add source or tool_name.`,
708
823
  rendered,
709
824
  ].join("\n\n");
710
825
  }
711
826
 
712
- type PlanHandleResult =
713
- | { op: "show"; draft: DraftPlan }
714
- | { op: "add" | "update" | "remove"; draft: DraftPlan; changedId: string };
715
-
716
- function handlePlan(params: PlanRequestType, current: DraftPlan, atoms: Atom[]): PlanHandleResult {
717
- const op = params.op ?? "show";
718
- if (op === "show") return { op, draft: current };
719
- if (op === "remove") {
720
- if (!params.draft_id) throw new Error("plan remove requires draft_id.");
721
- return { op, draft: removeDraftRange(current, params.draft_id), changedId: params.draft_id };
827
+ type PlanMutationOp = "add" | "update" | "remove";
828
+
829
+ function handlePlanMutation(
830
+ current: DraftPlan,
831
+ atoms: Atom[],
832
+ request: PlanAddRequestType | PlanUpdateRequestType | PlanRemoveRequestType,
833
+ ): { op: PlanMutationOp; draft: DraftPlan; changedId: string } {
834
+ if (request.action === "plan_add") {
835
+ const draft = addDraftRange(current, atoms, { start: request.start, end: request.end, summary: request.summary, topic: request.topic });
836
+ const previousIds = new Set(current.ranges.map((range) => range.id));
837
+ const changedId = draft.ranges.find((range) => !previousIds.has(range.id))!.id;
838
+ return { op: "add", draft, changedId };
722
839
  }
723
- if (op === "update") {
724
- if (!params.draft_id) throw new Error("plan update requires draft_id.");
725
- if (params.summary === undefined && params.topic === undefined) throw new Error("plan update requires summary or topic.");
840
+ if (request.action === "plan_update") {
841
+ if (request.summary === undefined && request.topic === undefined) {
842
+ throw new Error("plan_update requires summary and/or topic; boundaries change via plan_remove + plan_add.");
843
+ }
726
844
  return {
727
- op,
728
- draft: updateDraftRange(current, params.draft_id, { summary: params.summary, topic: params.topic }),
729
- changedId: params.draft_id,
845
+ op: "update",
846
+ draft: updateDraftRange(current, request.range_id, { summary: request.summary, topic: request.topic }),
847
+ changedId: request.range_id,
730
848
  };
731
849
  }
732
- if (!params.start || !params.end) throw new Error("plan add requires start and end.");
733
- const next = addDraftRange(current, atoms, { start: params.start, end: params.end, summary: params.summary, topic: params.topic });
734
- const previousIds = new Set(current.ranges.map((range) => range.id));
735
- const changedId = next.ranges.find((range) => !previousIds.has(range.id))!.id;
736
- return { op, draft: next, changedId };
850
+ return { op: "remove", draft: removeDraftRange(current, request.range_id), changedId: request.range_id };
737
851
  }
738
852
 
739
853
  function validateDraftForCommit(draft: DraftPlan, atoms: Atom[]): void {
package/src/inventory.ts CHANGED
@@ -217,7 +217,7 @@ export function formatInventory(page: InventoryPage): string {
217
217
 
218
218
  /** Measure explicit, possibly overlapping candidate spans without mutating the DraftPlan. */
219
219
  export function formatSpanInspection(atoms: readonly Atom[], spans: readonly InspectSpan[]): string {
220
- if (spans.length === 0) throw new Error("inspect spans requires at least one start/end span.");
220
+ if (spans.length === 0) throw new Error("measure requires at least one start/end candidate.");
221
221
  const byRef = new Map(atoms.map((atom) => [atom.ref, atom]));
222
222
  const anchorChars = aggregateMetrics(atoms.map((atom) => atom.metrics)).contentChars;
223
223
  const lines = [
@@ -228,7 +228,7 @@ export function formatSpanInspection(atoms: readonly Atom[], spans: readonly Ins
228
228
  for (const span of spans) {
229
229
  const start = byRef.get(span.start);
230
230
  const end = byRef.get(span.end);
231
- if (!start || !end) throw new Error(`Unknown span ref ${!start ? span.start : span.end}; re-run inspect against the current snapshot.`);
231
+ if (!start || !end) throw new Error(`Unknown span ref ${!start ? span.start : span.end}; re-run measure against the current snapshot.`);
232
232
  if (start.index > end.index) throw new Error(`Span ${span.start} → ${span.end} is reversed.`);
233
233
 
234
234
  const selected = atoms.slice(start.index, end.index + 1);
package/src/plan.ts CHANGED
@@ -22,14 +22,14 @@ export function addDraftRange(
22
22
  ): DraftPlan {
23
23
  const start = atoms.find((atom) => atom.ref === input.start);
24
24
  const end = atoms.find((atom) => atom.ref === input.end);
25
- if (!start || !end) throw new Error("Unknown atom ref; run locate/inspect again against the current anchor snapshot.");
25
+ if (!start || !end) throw new Error("Unknown atom ref; run inspect or locate again against the current anchor snapshot.");
26
26
  if (start.index > end.index) throw new Error("start must not occur after end.");
27
27
  const selected = atoms.slice(start.index, end.index + 1);
28
28
  if (selected.length === 0) throw new Error("Empty range.");
29
29
  const unsafe = selected.find((atom) => isProtectedAtom(atom));
30
30
  if (unsafe) throw new Error(`Range crosses protected atom ${unsafe.ref} (${unsafe.kind}). Split the plan around it.`);
31
31
  const overlaps = draft.ranges.some((range) => !(end.index < range.startIndex || start.index > range.endIndex));
32
- if (overlaps) throw new Error("Range overlaps an existing draft range.");
32
+ if (overlaps) throw new Error("Range overlaps an existing plan range; remove or replace it first.");
33
33
 
34
34
  const metrics = rangeMetricsForAtoms(selected);
35
35
  const summary = input.summary ?? "";
@@ -76,7 +76,7 @@ export function updateDraftRange(
76
76
  patch: { summary?: string; topic?: string },
77
77
  ): DraftPlan {
78
78
  const target = draft.ranges.find((range) => range.id === draftId);
79
- if (!target) throw new Error(`Unknown draft range ${draftId}.`);
79
+ if (!target) throw new Error(`Unknown plan range ${draftId}.`);
80
80
  const summary = patch.summary ?? target.summary;
81
81
  const topic = patch.topic ?? target.topic;
82
82
  const replacement = replacementContentChars(summary, topic);
@@ -90,7 +90,7 @@ export function updateDraftRange(
90
90
  }
91
91
 
92
92
  export function removeDraftRange(draft: DraftPlan, draftId: string): DraftPlan {
93
- if (!draft.ranges.some((range) => range.id === draftId)) throw new Error(`Unknown draft range ${draftId}.`);
93
+ if (!draft.ranges.some((range) => range.id === draftId)) throw new Error(`Unknown plan range ${draftId}.`);
94
94
  return { ...draft, revision: draft.revision + 1, ranges: draft.ranges.filter((range) => range.id !== draftId) };
95
95
  }
96
96
 
@@ -153,18 +153,18 @@ export interface DraftFormatOptions {
153
153
  /** Agent-facing plan output with bounded semantic landmarks and summaries. */
154
154
  export function formatDraft(draft: DraftPlan, telemetry?: DraftTelemetry, options: DraftFormatOptions = {}): string {
155
155
  if (options.detail === "full" && !options.draftId) {
156
- throw new Error("plan show detail=full requires draft_id.");
156
+ throw new Error("Full plan output requires a known range id.");
157
157
  }
158
158
  const selected = options.draftId
159
159
  ? draft.ranges.filter((range) => range.id === options.draftId)
160
160
  : draft.ranges;
161
161
  const atomsByRef = options.atoms ? new Map(options.atoms.map((atom) => [atom.ref, atom])) : undefined;
162
- if (options.draftId && selected.length === 0) throw new Error(`Unknown draft range ${options.draftId}.`);
162
+ if (options.draftId && selected.length === 0) throw new Error(`Unknown plan range ${options.draftId}.`);
163
163
 
164
164
  const lines: string[] = [];
165
165
  if (telemetry) lines.push(formatTelemetry(telemetry));
166
166
  if (draft.ranges.length === 0) {
167
- lines.push(`Draft v${draft.revision}: no compression ranges.`);
167
+ lines.push(`Plan v${draft.revision}: no compression ranges.`);
168
168
  return lines.join("\n\n");
169
169
  }
170
170
  lines.push(draftHeader(draft));
@@ -180,7 +180,7 @@ export function formatDraft(draft: DraftPlan, telemetry?: DraftTelemetry, option
180
180
  const block = formatRangeBrief(range, atomsByRef);
181
181
  const currentLength = lines.join("\n\n").length;
182
182
  if (currentLength + 2 + block.length > DRAFT_OUTPUT_LIMIT) {
183
- const notice = `Output budget reached: showed ${shown} of ${selected.length} range(s). Use draft_id to inspect one range.`;
183
+ const notice = `Output budget reached: showed ${shown} of ${selected.length} range(s). Use plan_read to inspect one range.`;
184
184
  if (currentLength + 2 + notice.length <= DRAFT_OUTPUT_LIMIT) lines.push(notice);
185
185
  break;
186
186
  }
@@ -199,7 +199,7 @@ export function formatPlanMutation(
199
199
  ): string {
200
200
  const pendingCount = draft.ranges.filter((range) => range.summary.trim().length === 0).length;
201
201
  const verb = op === "add" ? "added" : op === "update" ? "updated" : "removed";
202
- const lines = [`Draft v${draft.revision}: ${verb} ${changedId} · ${draft.ranges.length} range(s) · ${pendingCount} pending summary.`];
202
+ const lines = [`Plan v${draft.revision}: ${verb} ${changedId} · ${draft.ranges.length} range(s) · ${pendingCount} pending summary.`];
203
203
  if (op !== "remove") {
204
204
  const changed = draft.ranges.find((range) => range.id === changedId);
205
205
  const atomsByRef = atoms ? new Map(atoms.map((atom) => [atom.ref, atom])) : undefined;
@@ -211,7 +211,7 @@ export function formatPlanMutation(
211
211
  function draftHeader(draft: DraftPlan): string {
212
212
  const pendingCount = draft.ranges.filter((range) => range.summary.trim().length === 0).length;
213
213
  const reviewState = pendingCount === 0 ? "ready for review" : `${pendingCount} pending summary`;
214
- return `Draft v${draft.revision}: ${draft.ranges.length} range(s) (${reviewState}).`;
214
+ return `Plan v${draft.revision}: ${draft.ranges.length} range(s) (${reviewState}).`;
215
215
  }
216
216
 
217
217
  function formatRangeBrief(range: DraftRange, atomsByRef?: ReadonlyMap<string, Atom>): string {
package/src/review-ui.ts CHANGED
@@ -46,7 +46,7 @@ export async function showReviewUi(
46
46
 
47
47
  const header: string[] = [
48
48
  h("╭"),
49
- framed(accent(theme.bold(`Midcompact Review · Draft v${draft.revision} · ${draft.ranges.length} range(s)`))),
49
+ framed(accent(theme.bold(`Midcompact Review · Plan v${draft.revision} · ${draft.ranges.length} range(s)`))),
50
50
  framed(usageLine(telemetry, theme)),
51
51
  h("├"),
52
52
  ];
@@ -195,7 +195,7 @@ export async function showReviewUi(
195
195
 
196
196
  export function buildReviewText(atoms: Atom[], draft: DraftPlan, telemetry: DraftTelemetry): string {
197
197
  const lines = [
198
- `Midcompact Review · Draft v${draft.revision}`,
198
+ `Midcompact Review · Plan v${draft.revision}`,
199
199
  plainUsageLine(telemetry),
200
200
  "This is awareness, not a target.",
201
201
  "",
@@ -159,7 +159,7 @@ export async function showSelectionUi(
159
159
 
160
160
  const header = [
161
161
  rule(w, "+", "+"),
162
- frame(accent(theme.bold(`Midcompact Selection | Draft v${draft.revision}`)), w),
162
+ frame(accent(theme.bold(`Midcompact Selection | Plan v${draft.revision}`)), w),
163
163
  frame(dim(`Anchor Pi usage: ${usageLine(telemetry)}`), w),
164
164
  frame(dim(`Selected ${selectedAtoms.length}/${atoms.length} atoms | ${chars}/${totalChars} chars (${charShare} of anchor) | up to ${charShare} fewer anchor chars | ${images} images`), w),
165
165
  rule(w, "+", "+"),
package/src/start-ui.ts CHANGED
@@ -16,8 +16,8 @@ interface StartChoiceOption {
16
16
  * Agent-first start path, and RPC clients see the recommended entry first.
17
17
  */
18
18
  const START_CHOICES: StartChoiceOption[] = [
19
- { value: "agent", label: "Agent direct — Inspect and draft with Agent" },
20
- { value: "user", label: "User manual — Select the initial DraftPlan yourself" },
19
+ { value: "agent", label: "Agent direct — Inspect and plan with Agent" },
20
+ { value: "user", label: "User manual — Select the initial plan yourself" },
21
21
  { value: "cancelled", label: "Drop — Leave the session unchanged" },
22
22
  ];
23
23
 
package/src/telemetry.ts CHANGED
@@ -100,7 +100,7 @@ export function formatTelemetry(telemetry: DraftTelemetry): string {
100
100
  lines.push("anchor: Pi context usage unavailable [Pi reported; not derived from local char counts].");
101
101
  }
102
102
  lines.push(
103
- `draft: ${telemetry.rangeCount} range(s) · ${telemetry.selectedOriginalContentChars} → ${telemetry.selectedReplacementContentChars} content chars · ${telemetry.selectedImageCount} images (${telemetry.selectedImagePayloadBytes} payload bytes) · ${telemetry.pendingSummaryCount} pending summary`,
103
+ `plan: ${telemetry.rangeCount} range(s) · ${telemetry.selectedOriginalContentChars} → ${telemetry.selectedReplacementContentChars} content chars · ${telemetry.selectedImageCount} images (${telemetry.selectedImagePayloadBytes} payload bytes) · ${telemetry.pendingSummaryCount} pending summary`,
104
104
  );
105
105
  return lines.join("\n");
106
106
  }