pi-midcompact 0.5.1 → 0.5.3
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 +22 -22
- package/package.json +2 -2
- package/skills/midcompact/SKILL.md +11 -9
- package/skills/midcompact/references/tool-interface.md +10 -6
- package/src/SPEC.md +93 -0
- package/src/index.ts +144 -79
- package/src/projection.ts +1 -1
- package/src/types.ts +1 -1
package/README.md
CHANGED
|
@@ -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
|
|
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.
|
|
33
33
|
|
|
34
34
|
```text
|
|
35
35
|
Frozen anchor: raw session history
|
|
@@ -42,7 +42,7 @@ Planning is isolated on a temporary branch:
|
|
|
42
42
|
... [latest work] ──┬── [transaction] ── [draft v1] ── [draft v2] ◀ review / edit
|
|
43
43
|
│ (abandoned at commit)
|
|
44
44
|
└── [midcompact-state] ◀ committed leaf
|
|
45
|
-
(reviewed selection metadata, not a model message; written only by /midcompact
|
|
45
|
+
(reviewed selection metadata, not a model message; written only by /midcompact:commit)
|
|
46
46
|
|
|
47
47
|
Later model requests see a selective projection:
|
|
48
48
|
|
|
@@ -91,10 +91,10 @@ pi-midcompact — mid-context compression, review, then human commit
|
|
|
91
91
|
|
|
92
92
|
| | Pi `/compact` | `pi-midcompact` |
|
|
93
93
|
| --- | --- | --- |
|
|
94
|
-
| Starts | Automatically near the context limit, or with `/compact` | At an explicit natural checkpoint with `/midcompact
|
|
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
|
|
97
|
+
| Decision gate | Generates a compaction checkpoint directly | Draft → 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.
|
|
@@ -124,13 +124,13 @@ Start a transaction at a natural breakpoint: the current work is complete enough
|
|
|
124
124
|
Run:
|
|
125
125
|
|
|
126
126
|
```text
|
|
127
|
-
/midcompact
|
|
127
|
+
/midcompact:start
|
|
128
128
|
```
|
|
129
129
|
|
|
130
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:
|
|
131
131
|
|
|
132
132
|
```text
|
|
133
|
-
/midcompact
|
|
133
|
+
/midcompact:start Compress the early repository exploration, but keep user requirements verbatim.
|
|
134
134
|
```
|
|
135
135
|
|
|
136
136
|
### 2. Discuss what to compress with the Agent
|
|
@@ -150,7 +150,7 @@ The Agent locates relevant parts of the frozen conversation, proposes one or mor
|
|
|
150
150
|
Run:
|
|
151
151
|
|
|
152
152
|
```text
|
|
153
|
-
/midcompact
|
|
153
|
+
/midcompact:review
|
|
154
154
|
```
|
|
155
155
|
|
|
156
156
|
In interactive mode, the native TUI displays the frozen conversation as a linear timeline. Each item is marked either `KEEP` or as belonging to a proposed range. Review the range boundaries and the summary that will replace each range.
|
|
@@ -158,14 +158,14 @@ In interactive mode, the native TUI displays the frozen conversation as a linear
|
|
|
158
158
|
For RPC, print, or other no-TUI modes, use the editable local browser interface instead:
|
|
159
159
|
|
|
160
160
|
```text
|
|
161
|
-
/midcompact
|
|
161
|
+
/midcompact:review-webui
|
|
162
162
|
```
|
|
163
163
|
|
|
164
164
|
Use Selection to create or change ranges and KEEP holes:
|
|
165
165
|
|
|
166
166
|
```text
|
|
167
|
-
/midcompact
|
|
168
|
-
/midcompact
|
|
167
|
+
/midcompact:select
|
|
168
|
+
/midcompact:select-webui
|
|
169
169
|
```
|
|
170
170
|
|
|
171
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.
|
|
@@ -175,7 +175,7 @@ Use the TUI or local browser Review surface to edit summaries/topics and reject
|
|
|
175
175
|
When the plan is correct, run:
|
|
176
176
|
|
|
177
177
|
```text
|
|
178
|
-
/midcompact
|
|
178
|
+
/midcompact:commit
|
|
179
179
|
```
|
|
180
180
|
|
|
181
181
|
This is deliberately a human command. The Agent cannot commit compression itself.
|
|
@@ -187,14 +187,14 @@ Pi returns to the anchor, discards the temporary planning branch, stores the rev
|
|
|
187
187
|
Keep working normally after committing. If you decide not to compress, run:
|
|
188
188
|
|
|
189
189
|
```text
|
|
190
|
-
/midcompact
|
|
190
|
+
/midcompact:abort
|
|
191
191
|
```
|
|
192
192
|
|
|
193
193
|
This returns to the anchor and discards the transaction without changing the active context.
|
|
194
194
|
|
|
195
195
|
## Native TUI Controls
|
|
196
196
|
|
|
197
|
-
Inside `/midcompact
|
|
197
|
+
Inside `/midcompact:review`:
|
|
198
198
|
|
|
199
199
|
```text
|
|
200
200
|
n/p or Left/Right select a proposed range
|
|
@@ -211,14 +211,14 @@ Enter/Esc/q close
|
|
|
211
211
|
|
|
212
212
|
| Command | Result |
|
|
213
213
|
| --- | --- |
|
|
214
|
-
| `/midcompact
|
|
215
|
-
| `/midcompact
|
|
216
|
-
| `/midcompact
|
|
217
|
-
| `/midcompact
|
|
218
|
-
| `/midcompact
|
|
219
|
-
| `/midcompact
|
|
220
|
-
| `/midcompact
|
|
221
|
-
| `/midcompact
|
|
214
|
+
| `/midcompact:start [instructions]` | Opens Agent direct / User manual / Drop, then starts a transaction at the current session-tree leaf. |
|
|
215
|
+
| `/midcompact:select` | Opens the native TUI Selection workbench for range and KEEP editing. |
|
|
216
|
+
| `/midcompact:select-webui` | Opens the local browser Selection workbench. |
|
|
217
|
+
| `/midcompact:review` | Opens summary/topic review in the native TUI. |
|
|
218
|
+
| `/midcompact:review-webui` | Opens summary/topic review in a local browser. |
|
|
219
|
+
| `/midcompact:commit` | Commits the reviewed draft. Human only. |
|
|
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. |
|
|
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
|
|
|
@@ -227,7 +227,7 @@ The extension shows planning status in Pi's footer only while a transaction is a
|
|
|
227
227
|
- **Original history is retained.** Compression changes what later model requests see, not the stored Pi messages.
|
|
228
228
|
- **Fail-open projection.** If an exact reviewed sequence no longer resolves, the extension sends the raw history unchanged rather than removing uncertain content.
|
|
229
229
|
- **State is branch-local.** Navigating with `/tree` to a point before a committed state restores raw history; returning to its descendant restores the projection.
|
|
230
|
-
- **Human review is required.** The Agent can propose a plan but cannot execute `/midcompact
|
|
230
|
+
- **Human review is required.** The Agent can propose a plan but cannot execute `/midcompact:commit`.
|
|
231
231
|
- **Tool protocol is protected.** Unknown, incomplete, or orphaned tool exchanges are not compressible.
|
|
232
232
|
- **Repeated transactions work.** Later transactions can compress newly accumulated raw context; existing summaries remain protected.
|
|
233
233
|
- **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.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-midcompact",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"description": "Branch-aware mid-context compression for the Pi coding agent",
|
|
5
5
|
"author": "frostime",
|
|
6
6
|
"repository": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
],
|
|
33
33
|
"scripts": {
|
|
34
34
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
35
|
-
"test": "rm -rf .test-dist && tsc -p tsconfig.test.json && node test/install-mocks.mjs && node --test test/core.test.mjs test/runtime-start.test.mjs test/runtime-agent.test.mjs test/runtime-user.test.mjs test/runtime-transaction.test.mjs test/runtime-lock.test.mjs",
|
|
35
|
+
"test": "rm -rf .test-dist && tsc -p tsconfig.test.json && node test/install-mocks.mjs && node --test test/schema-contract.test.mjs test/core.test.mjs test/runtime-start.test.mjs test/runtime-agent.test.mjs test/runtime-user.test.mjs test/runtime-transaction.test.mjs test/runtime-lock.test.mjs",
|
|
36
36
|
"typecheck:contract": "tsc -p tsconfig.test.json --noEmit",
|
|
37
37
|
"pack:check": "npm pack --dry-run"
|
|
38
38
|
},
|
|
@@ -12,15 +12,17 @@ This skill handles two independent tasks: planning compression and recalling com
|
|
|
12
12
|
| Signal | Immediate duty | First action |
|
|
13
13
|
|--------|----------------|--------------|
|
|
14
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 | `action
|
|
16
|
-
| A handoff reports a persisted DraftPlan and the user asks to continue | Read the shared selection and determine what help the user wants | `action
|
|
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
17
|
| The user or a projected summary needs detail from a committed block | Retrieve that history only | Follow **Recall workflow** |
|
|
18
18
|
|
|
19
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
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
|
+
|
|
21
23
|
## Plan compression
|
|
22
24
|
|
|
23
|
-
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
|
|
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`.
|
|
24
26
|
|
|
25
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.
|
|
26
28
|
|
|
@@ -32,7 +34,7 @@ Apply one invariant:
|
|
|
32
34
|
|
|
33
35
|
#### 1. Read the entry state and user intent
|
|
34
36
|
|
|
35
|
-
For a fresh Agent-direct transaction, begin with `action
|
|
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.
|
|
36
38
|
|
|
37
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.
|
|
38
40
|
|
|
@@ -69,7 +71,7 @@ When treatments involve a meaningful tradeoff, present concise alternatives and
|
|
|
69
71
|
|
|
70
72
|
#### 4. Resolve boundaries and build the DraftPlan
|
|
71
73
|
|
|
72
|
-
After the intended treatment is clear, use `action
|
|
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.
|
|
73
75
|
|
|
74
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.
|
|
75
77
|
|
|
@@ -105,9 +107,9 @@ Length follows the information that must survive, not a target ratio. Final test
|
|
|
105
107
|
|
|
106
108
|
#### 6. Verify and hand off
|
|
107
109
|
|
|
108
|
-
Call `action
|
|
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.
|
|
109
111
|
|
|
110
|
-
Describe the completed proposal with the same recognizable landmarks used during alignment. Direct the user to `/midcompact
|
|
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.
|
|
111
113
|
|
|
112
114
|
## Recall compressed content
|
|
113
115
|
|
|
@@ -117,11 +119,11 @@ Recall works with or without an active planning transaction. It reads committed
|
|
|
117
119
|
|
|
118
120
|
#### 1. Find the block
|
|
119
121
|
|
|
120
|
-
If its id is unknown, call `action
|
|
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.
|
|
121
123
|
|
|
122
124
|
#### 2. Retrieve the detail
|
|
123
125
|
|
|
124
|
-
Call `action
|
|
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.
|
|
125
127
|
|
|
126
128
|
## Tool conventions
|
|
127
129
|
|
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
Read this reference when exact call requirements, limits, rejection behavior, repeated compression, or recall truncation matters. The main workflow remains in `../SKILL.md`.
|
|
4
4
|
|
|
5
|
+
## Parameter grouping
|
|
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.
|
|
8
|
+
|
|
5
9
|
## Inspect
|
|
6
10
|
|
|
7
|
-
Without `spans`, `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.
|
|
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.
|
|
8
12
|
|
|
9
13
|
- `page_size`: default 20 groups, maximum 50.
|
|
10
14
|
- `cursor`: opaque value returned by the previous page.
|
|
@@ -15,17 +19,17 @@ Stop paging after the candidate regions are covered.
|
|
|
15
19
|
To compare explicit candidates without mutating the DraftPlan, pass one or more possibly overlapping spans:
|
|
16
20
|
|
|
17
21
|
```text
|
|
18
|
-
midcompact(action
|
|
22
|
+
midcompact(request={action:"inspect", spans=[
|
|
19
23
|
{"start":"a0006","end":"a0014"},
|
|
20
24
|
{"start":"a0006","end":"a0020"}
|
|
21
|
-
])
|
|
25
|
+
]})
|
|
22
26
|
```
|
|
23
27
|
|
|
24
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`.
|
|
25
29
|
|
|
26
30
|
## Locate
|
|
27
31
|
|
|
28
|
-
`action="locate"` returns atoms from the frozen anchor. Supply either:
|
|
32
|
+
`request.action="locate"` returns atoms from the frozen anchor. Supply either:
|
|
29
33
|
|
|
30
34
|
- `ref`: one direct atom lookup; or
|
|
31
35
|
- at least one real filter: `pattern`, `tool_name`, or `source` other than `any`.
|
|
@@ -38,7 +42,7 @@ A `g...` inventory ref is not a locate ref; use the group's `a...` start/end lan
|
|
|
38
42
|
|
|
39
43
|
## Plan
|
|
40
44
|
|
|
41
|
-
`action="plan"` uses `op="show"` by default. Agent and user mutate the same DraftPlan.
|
|
45
|
+
`request.action="plan"` uses `op="show"` by default. Agent and user mutate the same DraftPlan.
|
|
42
46
|
|
|
43
47
|
| op | Required fields |
|
|
44
48
|
|----|-----------------|
|
|
@@ -74,7 +78,7 @@ Committed blocks appear as protected atoms in later transaction snapshots and ca
|
|
|
74
78
|
|
|
75
79
|
## Recall
|
|
76
80
|
|
|
77
|
-
`action="recall"` works independently of a planning transaction and reads committed blocks active on the current branch.
|
|
81
|
+
`request.action="recall"` works independently of a planning transaction and reads committed blocks active on the current branch.
|
|
78
82
|
|
|
79
83
|
- Without `ref`, `pattern` searches block topics and summaries; `limit` defaults to 8 and has a maximum of 20.
|
|
80
84
|
- With `ref="c0001"`, the tool renders that block's stored messages.
|
package/src/SPEC.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# SPEC — midcompact extension module (`src/`)
|
|
2
|
+
|
|
3
|
+
Maintenance contract for `src/`. The model-facing usage contract lives in
|
|
4
|
+
`skills/midcompact/` (SKILL.md + references/tool-interface.md) and is
|
|
5
|
+
authoritative for observable tool/command behavior; this spec records the
|
|
6
|
+
durable invariants a future maintainer must preserve, which are not reliably
|
|
7
|
+
inferable from code at a glance. Entry point: `src/index.ts` (factory in
|
|
8
|
+
package.json → `pi.extensions`).
|
|
9
|
+
|
|
10
|
+
## Persistence
|
|
11
|
+
|
|
12
|
+
- Exactly three custom entry types are ever persisted on the session branch:
|
|
13
|
+
`midcompact-transaction`, `midcompact-draft`, `midcompact-state`
|
|
14
|
+
(`state.ts`). All are `version: 1`, shape-checked before use.
|
|
15
|
+
- Restore is *latest entry wins* over the branch. A draft restores only when
|
|
16
|
+
a transaction entry precedes it with a matching `transactionId`; the two
|
|
17
|
+
form one unit. State entries are independent of transactions.
|
|
18
|
+
- Commit appends a new state entry — never mutates old ones. Abort writes
|
|
19
|
+
nothing.
|
|
20
|
+
- Compatibility is a standing constraint: old transactions without
|
|
21
|
+
`startMode` default to `"agent"`; old ranges lacking factual char/image
|
|
22
|
+
fields are coerced (`coerceDraftRange`). Legacy `approxTokens` fields are
|
|
23
|
+
deprecated: never authoritative for decisions or UI.
|
|
24
|
+
- The planning lock is deliberately not persisted: reload clears the owner.
|
|
25
|
+
|
|
26
|
+
## Transactions
|
|
27
|
+
|
|
28
|
+
- Start freezes the current leaf as anchor, appends TXN + DRAFT, and routes
|
|
29
|
+
Agent-direct or User-manual. All planning happens on the child branch; the
|
|
30
|
+
discussion never enters the working context.
|
|
31
|
+
- Commit and abort both navigate back to the anchor (`navigateTree`); commit
|
|
32
|
+
then appends state, abort appends nothing. Both refuse to run while the
|
|
33
|
+
Agent holds the planning lock.
|
|
34
|
+
- `anchorUsage` is informational only — Pi-reported awareness, never an
|
|
35
|
+
optimization target (same rule as `projectedTokens` / estimates).
|
|
36
|
+
- Atom refs are transaction-local: re-run inspect/locate in a later
|
|
37
|
+
transaction; group refs (`g...`) are never locate refs.
|
|
38
|
+
|
|
39
|
+
## Projection
|
|
40
|
+
|
|
41
|
+
- While blocks are active, the context hook replaces committed blocks with
|
|
42
|
+
their summary wrapper. The original history is restored only by recall;
|
|
43
|
+
recall never re-projects. Committed blocks appear as protected atoms in
|
|
44
|
+
later snapshots (no double compression).
|
|
45
|
+
|
|
46
|
+
## Atoms and ranges
|
|
47
|
+
|
|
48
|
+
- The atom is the smallest selectable unit; a tool call plus its matching
|
|
49
|
+
results is one indivisible `tool_exchange` atom.
|
|
50
|
+
- Protected (never compressible): incomplete/orphaned tool protocol,
|
|
51
|
+
existing compressed blocks, unsupported message kinds, entries lacking
|
|
52
|
+
the persistent anchor entry.
|
|
53
|
+
- Draft ranges never overlap and never contain protected atoms; boundaries
|
|
54
|
+
cannot be updated in place (remove + re-add). Empty `summary` = pending;
|
|
55
|
+
commit rejects pending, reversed, overlapping, or protected-crossing
|
|
56
|
+
ranges. `KEEP` is expressed by leaving atoms outside all ranges.
|
|
57
|
+
|
|
58
|
+
## Concurrency
|
|
59
|
+
|
|
60
|
+
- One runtime mutex over DraftPlan edits: owner ∈ {agent, ui}. Agent turns
|
|
61
|
+
hold it for their whole lifetime (`agent_start` → `agent_settled`); UIs
|
|
62
|
+
hold it per session via the `midcompactPlanningLock` API object (exposed
|
|
63
|
+
for UI and tests).
|
|
64
|
+
- Blocked operations notify and return — there is no queue.
|
|
65
|
+
|
|
66
|
+
## External contracts (reference, don't duplicate)
|
|
67
|
+
|
|
68
|
+
- Tool: one `midcompact` tool whose parameters are `{ request: <union> }` —
|
|
69
|
+
a root `type: "object"` wrapping a discriminated union on `action`
|
|
70
|
+
(inspect/locate/plan/recall); each branch is closed
|
|
71
|
+
(`additionalProperties: false`), so cross-action parameters are
|
|
72
|
+
schema-rejected. The `request` wrapper exists because some providers
|
|
73
|
+
(e.g. DeepSeek) reject a root-level `anyOf` before the model sees the
|
|
74
|
+
schema.
|
|
75
|
+
Details: `skills/midcompact/references/tool-interface.md`.
|
|
76
|
+
- Commands: `midcompact:start|abort|commit|review|review-webui|select|select-webui|status`;
|
|
77
|
+
no composite `/midcompact`; native naming convention `name:sub` (Pi's
|
|
78
|
+
`skill:<name>`).
|
|
79
|
+
- The tool never starts a transaction and never commits; both are command-
|
|
80
|
+
or user-gated. Recall is the only action valid without a transaction.
|
|
81
|
+
|
|
82
|
+
## Change rules
|
|
83
|
+
|
|
84
|
+
- Adding a tool action → new request branch + handler type + tool-interface
|
|
85
|
+
section + SKILL.md routing; adding parameters to an action → its branch
|
|
86
|
+
only (the union stays nested under `request`).
|
|
87
|
+
- Changing persistence shapes → keep `version: 1` readable (coerce) or add
|
|
88
|
+
a migration; restore predicates (`state.ts`) are the compatibility gate.
|
|
89
|
+
- Renaming commands → update SKILL.md/README/tests together; the stale-name
|
|
90
|
+
failure mode is a doc-arbitrated contract violation.
|
|
91
|
+
- Test seams: `setOpenReviewWebBrowser` and the mocks in `test/` must stay
|
|
92
|
+
behavior-faithful to the real extension API; suites drive commands via
|
|
93
|
+
`pi.commands.get(...)` and the tool via plain param objects.
|
package/src/index.ts
CHANGED
|
@@ -6,8 +6,6 @@ import {
|
|
|
6
6
|
type ExtensionContext,
|
|
7
7
|
type SessionEntry,
|
|
8
8
|
} from "@earendil-works/pi-coding-agent";
|
|
9
|
-
import type { AutocompleteItem } from "@earendil-works/pi-tui";
|
|
10
|
-
|
|
11
9
|
import { buildAtoms, formatLocatedAtom, isProtectedAtom, locateAtomMatches } from "./atoms.js";
|
|
12
10
|
import { buildInventory, formatInventory, formatSpanInspection } from "./inventory.js";
|
|
13
11
|
import { messageText } from "./messages.js";
|
|
@@ -69,28 +67,79 @@ const TOOL_DESCRIPTION =
|
|
|
69
67
|
const STATUS_KEY = "midcompact";
|
|
70
68
|
const START_PROMPT_PREFIX = "A mid-compaction transaction is active on a frozen anchor snapshot.";
|
|
71
69
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
73
|
+
// string+enum, which restricted JSON-Schema subsets (e.g. DeepSeek) accept
|
|
74
|
+
// more readily than const.
|
|
75
|
+
//
|
|
76
|
+
// The union sits under a root `request` property instead of being the
|
|
77
|
+
// parameters root: some providers validate that a tool's parameters root is
|
|
78
|
+
// `type: "object"` and reject a root-level anyOf before the model ever sees
|
|
79
|
+
// the schema (observed on DeepSeek). Field descriptions stay in sync with
|
|
80
|
+
// skills/midcompact/references/tool-interface.md.
|
|
81
|
+
const InspectRequest = Type.Object(
|
|
82
|
+
{
|
|
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
|
+
cursor: Type.Optional(Type.String({ description: "Pagination cursor from the previous page." })),
|
|
87
|
+
},
|
|
88
|
+
{ additionalProperties: false },
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const LocateRequest = Type.Object(
|
|
92
|
+
{
|
|
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." })),
|
|
98
|
+
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." })),
|
|
101
|
+
},
|
|
102
|
+
{ additionalProperties: false },
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const PlanRequest = Type.Object(
|
|
106
|
+
{
|
|
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." })),
|
|
115
|
+
},
|
|
116
|
+
{ additionalProperties: false },
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const RecallRequest = Type.Object(
|
|
120
|
+
{
|
|
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." })),
|
|
126
|
+
},
|
|
127
|
+
{ additionalProperties: false },
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const Params = Type.Object(
|
|
131
|
+
{ request: Type.Union([InspectRequest, LocateRequest, PlanRequest, RecallRequest]) },
|
|
132
|
+
{
|
|
133
|
+
additionalProperties: false,
|
|
134
|
+
description: "`request.action` selects exactly one request shape; fields of the other actions are not valid.",
|
|
135
|
+
},
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
type ToolParams = Static<typeof Params>;
|
|
139
|
+
type InspectRequestType = Static<typeof InspectRequest>;
|
|
140
|
+
type LocateRequestType = Static<typeof LocateRequest>;
|
|
141
|
+
type PlanRequestType = Static<typeof PlanRequest>;
|
|
142
|
+
type RecallRequestType = Static<typeof RecallRequest>;
|
|
94
143
|
|
|
95
144
|
type RuntimeSnapshot = { atoms: Atom[]; anchorState?: CompressionState };
|
|
96
145
|
|
|
@@ -146,7 +195,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
146
195
|
content: [
|
|
147
196
|
"An active midcompact transaction exists with a persisted DraftPlan.",
|
|
148
197
|
`Draft revision ${currentDraft.revision}; ${currentDraft.ranges.length} existing range(s), which may have been created by the user.`,
|
|
149
|
-
"If the current user request asks to continue midcompact, read the `midcompact` skill first, then call midcompact(action
|
|
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.",
|
|
150
199
|
].join("\n"),
|
|
151
200
|
display: false,
|
|
152
201
|
},
|
|
@@ -172,48 +221,63 @@ export default function (pi: ExtensionAPI) {
|
|
|
172
221
|
return { messages: projectMessages(event.messages as MessageLike[], activeState) as typeof event.messages };
|
|
173
222
|
});
|
|
174
223
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const items: AutocompleteItem[] = [
|
|
181
|
-
{ value: "start", label: "start — Start a new midcompact transaction at the current anchor" },
|
|
182
|
-
{ value: "abort", label: "abort — Abort the active transaction and return to anchor" },
|
|
183
|
-
{ value: "commit", label: "commit — Commit the current draft to the branch state" },
|
|
184
|
-
{ value: "review", label: "review — Open interactive TUI to inspect and edit the draft" },
|
|
185
|
-
{ value: "review-webui", label: "review-webui — Open a local web page to inspect and edit the draft (works without TUI)" },
|
|
186
|
-
{ value: "select", label: "select — Open the TUI Selection surface to edit range boundaries" },
|
|
187
|
-
{ value: "select-webui", label: "select-webui — Open Selection in a local browser" },
|
|
188
|
-
{ value: "status", label: "status — Show current transaction and draft status" },
|
|
189
|
-
];
|
|
190
|
-
const filtered = items.filter((item) => item.value.startsWith(query));
|
|
191
|
-
return filtered.length > 0 ? filtered : null;
|
|
192
|
-
},
|
|
224
|
+
// One command per operation; the `midcompact:` prefix follows the same
|
|
225
|
+
// naming convention as Pi's own `skill:<name>` commands. The former
|
|
226
|
+
// composite `/midcompact <subcommand>` command is gone.
|
|
227
|
+
pi.registerCommand("midcompact:start", {
|
|
228
|
+
description: "Start a new midcompact transaction at the current anchor. Optional trailing text becomes the initial focus.",
|
|
193
229
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
194
230
|
await ctx.waitForIdle();
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
);
|
|
231
|
+
return startTransaction(ctx, args.trim() || undefined);
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
pi.registerCommand("midcompact:abort", {
|
|
235
|
+
description: "Abort the active transaction and return to the anchor",
|
|
236
|
+
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
237
|
+
await ctx.waitForIdle();
|
|
238
|
+
return abortTransaction(ctx);
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
pi.registerCommand("midcompact:commit", {
|
|
242
|
+
description: "Commit the current draft to the branch state",
|
|
243
|
+
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
244
|
+
await ctx.waitForIdle();
|
|
245
|
+
return commitTransaction(ctx);
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
pi.registerCommand("midcompact:review", {
|
|
249
|
+
description: "Open the interactive TUI review to inspect and edit the draft",
|
|
250
|
+
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
251
|
+
await ctx.waitForIdle();
|
|
252
|
+
return reviewTransaction(ctx, "tui");
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
pi.registerCommand("midcompact:review-webui", {
|
|
256
|
+
description: "Open a local web page to inspect and edit the draft (works without TUI)",
|
|
257
|
+
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
258
|
+
await ctx.waitForIdle();
|
|
259
|
+
return reviewTransaction(ctx, "web");
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
pi.registerCommand("midcompact:select", {
|
|
263
|
+
description: "Open the TUI Selection surface to edit range boundaries",
|
|
264
|
+
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
265
|
+
await ctx.waitForIdle();
|
|
266
|
+
return openSelectionUi(ctx, "auto");
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
pi.registerCommand("midcompact:select-webui", {
|
|
270
|
+
description: "Open Selection in a local browser",
|
|
271
|
+
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
272
|
+
await ctx.waitForIdle();
|
|
273
|
+
return openSelectionUi(ctx, "web");
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
pi.registerCommand("midcompact:status", {
|
|
277
|
+
description: "Show current transaction and draft status",
|
|
278
|
+
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
279
|
+
await ctx.waitForIdle();
|
|
280
|
+
return showStatus(ctx);
|
|
217
281
|
},
|
|
218
282
|
});
|
|
219
283
|
|
|
@@ -473,7 +537,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
473
537
|
|
|
474
538
|
if (ctx.mode !== "tui") {
|
|
475
539
|
ctx.ui.notify(
|
|
476
|
-
"Interactive TUI review is only available in interactive (tui) mode. Use /midcompact
|
|
540
|
+
"Interactive TUI review is only available in interactive (tui) mode. Use /midcompact:review-webui to open a local web page instead.",
|
|
477
541
|
"warning",
|
|
478
542
|
);
|
|
479
543
|
return;
|
|
@@ -533,12 +597,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
533
597
|
label: "Midcompact",
|
|
534
598
|
description: TOOL_DESCRIPTION,
|
|
535
599
|
parameters: Params,
|
|
536
|
-
async execute(_id: string, params:
|
|
600
|
+
async execute(_id: string, params: ToolParams, _signal: AbortSignal | undefined, _onUpdate: unknown, ctx: ExtensionContext) {
|
|
537
601
|
try {
|
|
538
|
-
|
|
602
|
+
const request = params.request;
|
|
603
|
+
if (request.action === "recall") return toolResult(handleRecall(request, ctx));
|
|
539
604
|
const restored = restoreTransaction(ctx.sessionManager.getBranch() as SessionEntry[]);
|
|
540
605
|
const currentTx = withCompatDefaults(restored.transaction ?? transaction);
|
|
541
|
-
if (!currentTx) return toolResult("No active midcompact transaction. Ask the user to run `/midcompact
|
|
606
|
+
if (!currentTx) return toolResult("No active midcompact transaction. Ask the user to run `/midcompact:start` first.");
|
|
542
607
|
transaction = currentTx;
|
|
543
608
|
draft = restored.draft ? { ...restored.draft, ranges: restored.draft.ranges.map(coerceDraftRange) } : (draft ?? emptyDraft(currentTx.id));
|
|
544
609
|
if (!requireAgentAccess(ctx)) {
|
|
@@ -546,14 +611,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
546
611
|
}
|
|
547
612
|
const snapshot = buildAnchorSnapshot(ctx.sessionManager, currentTx);
|
|
548
613
|
|
|
549
|
-
if (
|
|
550
|
-
if (
|
|
551
|
-
if (
|
|
552
|
-
const result = handlePlan(
|
|
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);
|
|
553
618
|
if (result.op === "show") {
|
|
554
619
|
return toolResult(formatDraft(draft!, draftTelemetry(transaction, draft), {
|
|
555
|
-
detail:
|
|
556
|
-
draftId:
|
|
620
|
+
detail: request.detail,
|
|
621
|
+
draftId: request.draft_id,
|
|
557
622
|
atoms: snapshot.atoms,
|
|
558
623
|
}));
|
|
559
624
|
}
|
|
@@ -569,7 +634,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
569
634
|
},
|
|
570
635
|
});
|
|
571
636
|
|
|
572
|
-
function handleInspect(params:
|
|
637
|
+
function handleInspect(params: InspectRequestType, atoms: Atom[], tx: TransactionState): string {
|
|
573
638
|
if (params.spans) {
|
|
574
639
|
if (params.page_size !== undefined || params.cursor !== undefined) {
|
|
575
640
|
throw new Error("inspect spans cannot be combined with inventory pagination.");
|
|
@@ -580,7 +645,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
580
645
|
return formatInventory(page);
|
|
581
646
|
}
|
|
582
647
|
|
|
583
|
-
function handleRecall(params:
|
|
648
|
+
function handleRecall(params: RecallRequestType, ctx: ExtensionContext): string {
|
|
584
649
|
const sm = ctx.sessionManager;
|
|
585
650
|
const branchState = restoreCompressionState(sm.getBranch() as SessionEntry[]) ?? activeState;
|
|
586
651
|
if (!branchState?.blocks.length) return "No compressed blocks are active on this branch.";
|
|
@@ -616,7 +681,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
616
681
|
|
|
617
682
|
// ---- Pure handlers ----
|
|
618
683
|
|
|
619
|
-
function handleLocate(params:
|
|
684
|
+
function handleLocate(params: LocateRequestType, atoms: Atom[]): string {
|
|
620
685
|
const hasFilter = Boolean(params.pattern || params.tool_name || (params.source && params.source !== "any"));
|
|
621
686
|
if (params.ref && hasFilter) {
|
|
622
687
|
throw new Error("locate accepts either one direct ref or search filters, not both.");
|
|
@@ -648,7 +713,7 @@ type PlanHandleResult =
|
|
|
648
713
|
| { op: "show"; draft: DraftPlan }
|
|
649
714
|
| { op: "add" | "update" | "remove"; draft: DraftPlan; changedId: string };
|
|
650
715
|
|
|
651
|
-
function handlePlan(params:
|
|
716
|
+
function handlePlan(params: PlanRequestType, current: DraftPlan, atoms: Atom[]): PlanHandleResult {
|
|
652
717
|
const op = params.op ?? "show";
|
|
653
718
|
if (op === "show") return { op, draft: current };
|
|
654
719
|
if (op === "remove") {
|
package/src/projection.ts
CHANGED
|
@@ -17,7 +17,7 @@ export function summaryMessage(block: CompressionBlock, timestamp = Date.now()):
|
|
|
17
17
|
block.summary,
|
|
18
18
|
"",
|
|
19
19
|
`Original block: ${block.id}`,
|
|
20
|
-
`Use midcompact(action
|
|
20
|
+
`Use midcompact(request={action:\"recall\", ref:\"${block.id}\"}) if exact details are needed.`,
|
|
21
21
|
].join("\n"),
|
|
22
22
|
display: true,
|
|
23
23
|
details: {
|
package/src/types.ts
CHANGED
|
@@ -178,7 +178,7 @@ export interface TransactionState {
|
|
|
178
178
|
startedAt: string;
|
|
179
179
|
/** Initial routing chosen at start: Agent-first prompt or User-first UI. */
|
|
180
180
|
startMode?: StartMode;
|
|
181
|
-
/** Frozen awareness captured when /midcompact
|
|
181
|
+
/** Frozen awareness captured when /midcompact:start runs; informational, never a target. */
|
|
182
182
|
anchorUsage?: ContextUsageSnapshot;
|
|
183
183
|
}
|
|
184
184
|
|