pi-continual-harness 0.12.0 → 0.15.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 +118 -26
- package/package.json +1 -1
- package/src/auto-refine.ts +81 -12
- package/src/config.ts +168 -26
- package/src/corpus.ts +30 -10
- package/src/durable.ts +179 -5
- package/src/harness.ts +233 -20
- package/src/hints.ts +67 -0
- package/src/index.ts +79 -2
- package/src/inject.ts +58 -2
- package/src/outcome.ts +12 -2
- package/src/remind.ts +252 -26
- package/src/setup.ts +350 -0
- package/src/status.ts +340 -0
- package/src/store.ts +27 -5
- package/src/tools.ts +72 -1
package/README.md
CHANGED
|
@@ -70,11 +70,13 @@ Durable I/O (round-trip with pi-reflect):
|
|
|
70
70
|
/harness drop <id> # nudge importance down (−0.1)
|
|
71
71
|
/harness move <id> global|project # move an item between durable layers
|
|
72
72
|
/harness split # steer the agent to classify every item's scope
|
|
73
|
+
/harness adopt --from <provider/id> [--all] # steer the agent to copy another model's items here
|
|
73
74
|
/harness push-mem [--all|--kind <kind>|--model <provider/id|active>] # persist active items to pi-mem (save_memory)
|
|
75
|
+
/harness setup [--preset <quiet|recommended|power>] [--yes] # guided config wizard
|
|
74
76
|
```
|
|
75
77
|
|
|
76
|
-
Subcommands, flags, `--kind`/`--model` values, `move` scopes, and
|
|
77
|
-
`drop`/`move` item ids all autocomplete in the TUI as you type after
|
|
78
|
+
Subcommands, flags, `--kind`/`--model`/`--from` values, `move` scopes, and
|
|
79
|
+
`keep`/`drop`/`move` item ids all autocomplete in the TUI as you type after
|
|
78
80
|
`/harness `.
|
|
79
81
|
|
|
80
82
|
**Durable layers.** Every item carries its own durable scope: `global`
|
|
@@ -104,7 +106,10 @@ The model-facing tools:
|
|
|
104
106
|
|
|
105
107
|
- `harness_list({ kind?, model? })` — read current state. `model` defaults to
|
|
106
108
|
the active model's items (what gets injected this turn); `"*"` returns every
|
|
107
|
-
model.
|
|
109
|
+
model. Empty results are **diagnostic**: the message distinguishes a
|
|
110
|
+
genuinely empty store (with zero-state guidance) from items hidden by the
|
|
111
|
+
model or kind filter (with a tally and a `model: "*"` pointer), so an
|
|
112
|
+
empty list never reads as "broken".
|
|
108
113
|
- `harness_mutate { deltas: [...] }` — apply a batch of `create` / `update` /
|
|
109
114
|
`delete` deltas. Every `create` requires `evidence`. New items are stamped
|
|
110
115
|
automatically with the active model (see [Model binding](#model-binding-per-model-isolation)).
|
|
@@ -136,15 +141,39 @@ unchanged by a push — pi-mem gets a separate copy.
|
|
|
136
141
|
|
|
137
142
|
## Configuration
|
|
138
143
|
|
|
144
|
+
**First-run setup:** instead of hand-editing the JSON below, run
|
|
145
|
+
`/harness setup`. It is a guided wizard:
|
|
146
|
+
|
|
147
|
+
1. Shows the **current effective config** (your file merged over the shipped
|
|
148
|
+
defaults) plus the durable-layer paths.
|
|
149
|
+
2. Offers three **presets**: `quiet` (pin every opt-in to the shipped
|
|
150
|
+
defaults), `recommended` (`autoImport` + import reminders on), `power`
|
|
151
|
+
(`recommended` + `autoRefine`). Each preset is shown as a declarative diff
|
|
152
|
+
before anything is applied.
|
|
153
|
+
3. Confirms **each delta** with a one-line rationale. Autonomous switches
|
|
154
|
+
get their own explicit "this lets the agent mutate state on a cadence"
|
|
155
|
+
confirmation regardless of preset.
|
|
156
|
+
4. Writes via one reviewed path: backs up the existing file
|
|
157
|
+
(`harness.json.bak-<timestamp>`), atomically writes the merged result,
|
|
158
|
+
then reloads and prints the effective config exactly as pi will read it.
|
|
159
|
+
|
|
160
|
+
For scripted setups: `/harness setup --preset recommended --yes` (the `--yes`
|
|
161
|
+
flag is the scripted form of the confirmations, so it enables autonomous
|
|
162
|
+
features without prompts). Nothing is written on unknown input, a cancelled
|
|
163
|
+
preset menu, or a declined final write; declining an individual feature only
|
|
164
|
+
skips that feature (the accepted deltas still apply). The wizard never
|
|
165
|
+
touches harness state, only the config file.
|
|
166
|
+
|
|
139
167
|
Optional config at `~/.pi/agent/harness.json` (missing or malformed → defaults):
|
|
140
168
|
|
|
141
169
|
```json
|
|
142
170
|
{
|
|
143
171
|
"autoImport": false,
|
|
172
|
+
"importRemind": { "enabled": true, "everyTurns": 25 },
|
|
144
173
|
"proposer": "steering",
|
|
145
174
|
"dedupe": { "threshold": 0.6, "merge": true },
|
|
146
175
|
"injection": { "enabled": true, "maxTokens": 1500, "maxPerKind": 10, "charsPerToken": 4 },
|
|
147
|
-
"remindRefine": { "enabled":
|
|
176
|
+
"remindRefine": { "enabled": true, "everyTurns": 25, "detailEvery": 4 },
|
|
148
177
|
"autoRefine": { "enabled": false, "everyTurns": 100, "commit": false, "gate": false },
|
|
149
178
|
"outcomeImportance": { "enabled": false, "bump": 0.03 },
|
|
150
179
|
"classifier": { "model": "typesafe/jev-latest" },
|
|
@@ -160,7 +189,21 @@ Optional config at `~/.pi/agent/harness.json` (missing or malformed → defaults
|
|
|
160
189
|
changed since the last export. This is what makes `/refine` output survive
|
|
161
190
|
into **new** sessions without manual export/import ceremony. Both halves are
|
|
162
191
|
visible (one notify line) and use the same persisted `harness-state` entries
|
|
163
|
-
as every other mutation (`/tree` rollback covers them).
|
|
192
|
+
as every other mutation (`/tree` rollback covers them). Enabling it
|
|
193
|
+
mid-session (e.g. via `/harness setup`) is safe: the first `turn_end`
|
|
194
|
+
imports the layers *before* any export, so a late enable can never
|
|
195
|
+
overwrite the durable files with a never-imported store.
|
|
196
|
+
- **`importRemind`** — **awareness reminders** for that sync (effective only
|
|
197
|
+
when `autoImport` is on; default `{ "enabled": true, "everyTurns": 25 }`).
|
|
198
|
+
The session-start import doesn't just count — it **lists what arrived**
|
|
199
|
+
(item id, kind, first words of content), and every `everyTurns` turns a
|
|
200
|
+
one-line reminder re-surfaces the imported items so silently-injected
|
|
201
|
+
instructions never go unnoticed (full list via the agent's `harness_list`
|
|
202
|
+
tool — `/harness status` shows only counts). Bounded: at most 3 reminders
|
|
203
|
+
per imported batch, then quiet until the next session; aborted/error turns
|
|
204
|
+
are inert (completed-turn cadence). Kill-switch:
|
|
205
|
+
`{ "importRemind": { "enabled": false } }` (the start-of-session listing
|
|
206
|
+
still fires).
|
|
164
207
|
- **`injection`** — the selection policy for WHAT gets surfaced in the system
|
|
165
208
|
prompt each turn (see [Injection selection](#injection-selection-on-by-default)).
|
|
166
209
|
ON by default: items are importance-ordered, capped at `maxPerKind` (default
|
|
@@ -181,9 +224,20 @@ Optional config at `~/.pi/agent/harness.json` (missing or malformed → defaults
|
|
|
181
224
|
companion packages inject a richer comparator whose return may **abstain**
|
|
182
225
|
per pair (`{ score, abstain }` — see [`SimilarityResult`](#proposers)); an
|
|
183
226
|
abstaining pair is always kept, never merged or deleted.
|
|
184
|
-
- **`remindRefine`** —
|
|
185
|
-
|
|
186
|
-
|
|
227
|
+
- **`remindRefine`** — the `turn_end` **activation nudge**, ON by default
|
|
228
|
+
(informational only; it never mutates state). Every `everyTurns` completed
|
|
229
|
+
turns (default 25) a one-line reminder suggests `/refine`, garnished with
|
|
230
|
+
live data from the store (e.g. "12 active item(s), 4 never cited since
|
|
231
|
+
creation"; an empty store gets first-run guidance instead). Every
|
|
232
|
+
`detailEvery`-th reminder (default 4: at the default cadence, roughly
|
|
233
|
+
every 100 turns) escalates to a rotating educational guide: how the loop
|
|
234
|
+
works (with alternate descriptions so it never reads as boilerplate), the
|
|
235
|
+
`/refine` workflow, `keep`/`drop`/`prune --decay` hygiene, `push-mem`,
|
|
236
|
+
`split`, `export-corpus`, and a recommended starter config. Fresh installs
|
|
237
|
+
used to be silent; that was the bug, not the feature. Aborted/error turns
|
|
238
|
+
are skipped, so `everyTurns` counts completed turns of work. Silence it
|
|
239
|
+
with `{ "remindRefine": { "enabled": false } }`; `detailEvery: 1` makes
|
|
240
|
+
every reminder detailed.
|
|
187
241
|
- **`autoRefine`** — opt-in autonomous self-improvement (**off by default**).
|
|
188
242
|
When `enabled`, the agent runs `/refine` itself every `everyTurns` turns
|
|
189
243
|
(default 100). It is one of the package's opt-in autonomous paths: it reuses
|
|
@@ -195,14 +249,21 @@ Optional config at `~/.pi/agent/harness.json` (missing or malformed → defaults
|
|
|
195
249
|
signal, not just time (errors fall back to the plain cadence). The steering
|
|
196
250
|
prompt is delivered via the pi ≥ 0.87 actionable `turn_end` boundary (a
|
|
197
251
|
structural `harness.auto-refine-request` entry + `continue: true`), not a
|
|
198
|
-
synthetic user message.
|
|
252
|
+
synthetic user message. Aborted/error turns (pi `turn_end`
|
|
253
|
+
`BoundaryState.outcome`) are inert: they neither seed nor advance the
|
|
254
|
+
cadence window — a refine never fires over partial evidence.
|
|
199
255
|
- **`classifier`** — opt-in classifier model (pi ≥ 0.99) for the cheap gated
|
|
200
256
|
paths: `"provider/id"` or a bare id of a **classifier** catalog entry, e.g.
|
|
201
257
|
`"typesafe/jev-latest"` or a local `llama-cpp` classifier (every chat model
|
|
202
|
-
listed for llama.cpp also appears as
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
258
|
+
listed for llama.cpp also appears as one). With pi ≥ 1.1.0 the catalog also
|
|
259
|
+
offers OpenAI's GPT-6 Luna through the Decisions API (e.g.
|
|
260
|
+
`"openai/gpt-6-luna"`, needs `OPENAI_API_KEY`) and the native llama.cpp
|
|
261
|
+
decision models (Julia-1, Laya, Kev, lev, OpenJev through `/v1/systemone`,
|
|
262
|
+
llama.cpp ≥ 0.6.0 — listed *only* as classifiers, which is exactly our use:
|
|
263
|
+
a free local gate). When set and resolvable it enables the
|
|
264
|
+
classifier-confirmed dedupe (see `dedupe` above) and the auto-refine `gate`.
|
|
265
|
+
When unset or unresolvable, every classifier feature degrades to its
|
|
266
|
+
no-classifier behavior.
|
|
206
267
|
- **`virtualBinding`** — how the per-model isolation key treats pi **virtual
|
|
207
268
|
models** (router extensions, pi ≥ 0.99). Default `"virtual"`: the key is
|
|
208
269
|
the selected model's own `provider/id` — a router starts a blank harness.
|
|
@@ -215,19 +276,23 @@ Optional config at `~/.pi/agent/harness.json` (missing or malformed → defaults
|
|
|
215
276
|
- **`outcomeImportance`** — opt-in autonomous **promotion** loop (**off by
|
|
216
277
|
default**). When `enabled`, a `turn_end` hook bumps (+`bump`, default 0.03)
|
|
217
278
|
the importance of any active item the agent cited by its `[h_xxxx]` tag in the
|
|
218
|
-
turn's output.
|
|
279
|
+
turn's output. Citations are read from completed turns only — an aborted
|
|
280
|
+
turn's partial text is scanned by the next completed turn (the initial seed
|
|
281
|
+
turn itself is never scanned, as before). Referenced items
|
|
282
|
+
gain importance and get their `updatedAt`
|
|
219
283
|
touched (so they survive time-based decay); ignored items keep decaying. This
|
|
220
284
|
is the package's second opt-in autonomous path — promotion only, never
|
|
221
285
|
deletes, persisted/branchable like `harness_mutate`. Autonomous demotion from
|
|
222
286
|
outcomes is intentionally NOT done (high false-positive); use `/harness drop`,
|
|
223
287
|
`prune --decay`, or the `dedupe` proposer for that.
|
|
224
288
|
|
|
225
|
-
> **
|
|
226
|
-
> (`/harness move`), and durable I/O
|
|
227
|
-
>
|
|
228
|
-
> Migration from a `durableScope: "project"` setup: run
|
|
229
|
-
> in the project (its file's items adopt project
|
|
230
|
-
> with `/harness move` (or classify the whole
|
|
289
|
+
> **Removed:** the store-wide `durableScope` config key (deprecated 0.9.0,
|
|
290
|
+
> removed now). Items carry their own scope (`/harness move`), and durable I/O
|
|
291
|
+
> is always layered. Config files that still set the key load fine — unknown
|
|
292
|
+
> keys are ignored. Migration from a `durableScope: "project"` setup: run
|
|
293
|
+
> `/harness import` once in the project (its file's items adopt project
|
|
294
|
+
> scope), then move any strays with `/harness move` (or classify the whole
|
|
295
|
+
> store with `/harness split`).
|
|
231
296
|
|
|
232
297
|
## How it works
|
|
233
298
|
|
|
@@ -276,6 +341,29 @@ How the binding is set and respected:
|
|
|
276
341
|
- **Durable round-trip preserves owner.** `/harness export` tags each item with
|
|
277
342
|
`model: provider/id`; `/harness import` restores it. An item whose tag
|
|
278
343
|
pi-reflect stripped becomes an orphan and is adopted by the active model.
|
|
344
|
+
- **Carrying owned items across models: `/harness adopt`.** Orphan adoption
|
|
345
|
+
only covers *unowned* items. `/harness adopt --from <provider/id>` is the
|
|
346
|
+
reviewable path for owned ones: it steers the agent to classify the source
|
|
347
|
+
model's active items **generalizes** (engineering practices, user
|
|
348
|
+
preferences, project facts) vs **stays** (that model's quirks, formatting
|
|
349
|
+
preferences, failure modes), then apply the adoption as ONE `harness_mutate`
|
|
350
|
+
batch of create deltas. It is **copy, not move**: the source model's harness
|
|
351
|
+
stays untouched, content is copied verbatim (importance is listed at full
|
|
352
|
+
precision so the copy keeps its exact ranking), and evidence is annotated
|
|
353
|
+
`adopted from <provider/id>`. Because `ownerModel` is never model-settable,
|
|
354
|
+
the creates stamp to the **active** model via the normal create-stamping
|
|
355
|
+
path — audited and `/tree`-rollback-able like every mutation. `--all` skips
|
|
356
|
+
the filter; overlaps afterwards clean up with `/refine --proposer dedupe`.
|
|
357
|
+
Two store-level filters keep the copy honest (review #25): items scoped to
|
|
358
|
+
a **different project** are skipped (a copy would re-stamp them into this
|
|
359
|
+
session's project — adopt them from a session in that project instead),
|
|
360
|
+
and content the adopter already owns is skipped **before** the batch cap,
|
|
361
|
+
so re-running against a >20-item source reaches new items instead of
|
|
362
|
+
re-selecting the same top ones. The self-adopt guard compares against the
|
|
363
|
+
**resolved binding key** (`virtualBinding`-aware — under `physical` a
|
|
364
|
+
router selection adopts as the routed model), the same key
|
|
365
|
+
create-stamping uses. Explicit and manual only — there is no automatic
|
|
366
|
+
cross-model adoption.
|
|
279
367
|
|
|
280
368
|
Manual commands (`export`, `import`, `move`, `split`, `keep`, `drop`, `prune`,
|
|
281
369
|
`push-mem`, `status`) operate on the **whole store** by design — they are
|
|
@@ -410,10 +498,10 @@ Then `"my-proposer"` is selectable via `/refine --proposer my-proposer` or
|
|
|
410
498
|
auto-import + turn_end layered auto-export when the store changed —
|
|
411
499
|
`/refine` output survives into new sessions with zero ceremony. Off by
|
|
412
500
|
default; every action visible and `/tree`-rollback-able.
|
|
413
|
-
- Optional config (`~/.pi/agent/harness.json`): the durable-sync opt-in,
|
|
414
|
-
|
|
415
|
-
`turn_end` outcome-importance
|
|
416
|
-
paths
|
|
501
|
+
- Optional config (`~/.pi/agent/harness.json`): the durable-sync opt-in, a
|
|
502
|
+
default-on `turn_end` reminder (notify-only; the activation nudge), and the
|
|
503
|
+
opt-in `turn_end` auto-refine and outcome-importance loops. The package's
|
|
504
|
+
autonomous paths stay opt-in and off by default.
|
|
417
505
|
- **Pluggable delta proposers** with a registry: `steering` (default) and
|
|
418
506
|
`dedupe` (rule-based) shipped; `registerProposer()` for custom ones.
|
|
419
507
|
- **Per-model isolation**: every item is bound to a `provider/id` and injected
|
|
@@ -422,7 +510,9 @@ Then `"my-proposer"` is selectable via `/refine --proposer my-proposer` or
|
|
|
422
510
|
round-trip preserves the owner tag. With pi virtual models (routers), the
|
|
423
511
|
`"virtualBinding": "physical"` policy keys items by the routed physical
|
|
424
512
|
model instead of the router id, so a router selection does not silently
|
|
425
|
-
reset the harness.
|
|
513
|
+
reset the harness. Owned items can be carried across models explicitly with
|
|
514
|
+
`/harness adopt --from <provider/id> [--all]` (agent-filtered copy; see
|
|
515
|
+
[Model binding](#model-binding-per-model-isolation)).
|
|
426
516
|
- **Bounded injection (on by default)**: the supplemental block is
|
|
427
517
|
importance-ordered and capped per kind + by a total token budget, so a growing
|
|
428
518
|
harness never balloons the system prompt. Tunable / opt-out via the `injection`
|
|
@@ -440,7 +530,9 @@ Then `"my-proposer"` is selectable via `/refine --proposer my-proposer` or
|
|
|
440
530
|
- **Boundary delivery for auto-refine**: the steering prompt is persisted as a
|
|
441
531
|
structural entry with `{ entries, continue: true }` at `turn_end` (no
|
|
442
532
|
synthetic user message, no follow-up-scheduling side effects); manual
|
|
443
|
-
`/refine` still steers via a user message.
|
|
533
|
+
`/refine` still steers via a user message. Each new boundary also omits
|
|
534
|
+
prior auto-refine request drafts from future provider context (pi ≥ 0.87
|
|
535
|
+
context edits; `autoRefine.omitStaleDrafts: false` to keep them).
|
|
444
536
|
|
|
445
537
|
Open extension points (see `docs/ROADMAP.md`):
|
|
446
538
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-continual-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Online self-improvement layer for pi. A unified, in-trajectory harness-state store (prompt notes, memory, skill descriptions, sub-agent specs) with a manual /refine that proposes evidence-backed structured CRUD deltas. Composes with pi-reflect (offline) and pi-mem (storage); owns only the online optimizer layer. Inspired by Continual Harness (arXiv 2605.09998) and ACE (arXiv 2510.04618).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
package/src/auto-refine.ts
CHANGED
|
@@ -25,6 +25,12 @@
|
|
|
25
25
|
// sendUserMessage when the boundary cannot continue (event.context.
|
|
26
26
|
// canContinue false, e.g. after an error turn).
|
|
27
27
|
//
|
|
28
|
+
// Aborted/error turns are inert here: a turn_end whose BoundaryState.outcome
|
|
29
|
+
// is "aborted" or "error" neither seeds nor advances the cadence window, so
|
|
30
|
+
// "every N turns" counts completed turns of work and a refine never fires
|
|
31
|
+
// over partial evidence. (Durable auto-export deliberately still exports on
|
|
32
|
+
// such turns — committed entries must not be lost.)
|
|
33
|
+
//
|
|
28
34
|
// It stays visible: it notifies before firing, and the boundary draft (and the
|
|
29
35
|
// gate decision) appear in the transcript. The decision logic is factored into
|
|
30
36
|
// evaluateAutoRefine() so it is unit-testable without a live pi runtime.
|
|
@@ -34,29 +40,47 @@ import { DEFAULT_AUTO_EVERY_TURNS, type HarnessConfig, loadConfig } from "./conf
|
|
|
34
40
|
import { runRefine, gatherEvidence, DEFAULT_LOOKBACK_TURNS } from "./refine.js";
|
|
35
41
|
import { buildClassify, buildGateRequest, gateDecision } from "./classify.js";
|
|
36
42
|
|
|
37
|
-
//
|
|
38
|
-
|
|
43
|
+
// Completed turns observed since the last fire (or the seeded baseline).
|
|
44
|
+
// -1 = unseen. Aborted/error turns never touch this count — the window is
|
|
45
|
+
// completed turns of work, not raw turn indices.
|
|
46
|
+
let completedTurns = -1;
|
|
39
47
|
|
|
40
48
|
/** Test hook: reset internal counter. */
|
|
41
49
|
export function resetAutoRefine(): void {
|
|
42
|
-
|
|
50
|
+
completedTurns = -1;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* True when a turn_end's outcome marks a turn that ended badly (pi's
|
|
55
|
+
* BoundaryState.outcome: "aborted" | "error"). Undefined is treated as
|
|
56
|
+
* completed — current pi always reports the field, but synthetic events and
|
|
57
|
+
* the earliest 0.99.x may omit it.
|
|
58
|
+
*/
|
|
59
|
+
export function turnEndedBadly(outcome?: string): boolean {
|
|
60
|
+
return outcome === "aborted" || outcome === "error";
|
|
43
61
|
}
|
|
44
62
|
|
|
45
63
|
/**
|
|
46
64
|
* Decide whether to auto-refine this turn. Side-effectful: the first observed
|
|
47
|
-
* turn seeds the baseline (no refine then);
|
|
48
|
-
*
|
|
65
|
+
* completed turn seeds the baseline (no refine then); every completed turn
|
|
66
|
+
* after it advances the count, and each fired refine resets it (which also
|
|
67
|
+
* prevents the refine turn itself from immediately re-triggering).
|
|
68
|
+
* Aborted/error turns are inert: they neither seed nor advance the count —
|
|
69
|
+
* the cadence counts completed turns of work, never raw turn indices, so an
|
|
70
|
+
* aborted turn in between cannot shorten the window.
|
|
49
71
|
*/
|
|
50
|
-
export function evaluateAutoRefine(config: HarnessConfig,
|
|
72
|
+
export function evaluateAutoRefine(config: HarnessConfig, outcome?: string): boolean {
|
|
51
73
|
if (!config.autoRefine?.enabled) return false;
|
|
74
|
+
if (turnEndedBadly(outcome)) return false;
|
|
52
75
|
const every = config.autoRefine.everyTurns ?? DEFAULT_AUTO_EVERY_TURNS;
|
|
53
76
|
if (every <= 0) return false;
|
|
54
|
-
if (
|
|
55
|
-
|
|
77
|
+
if (completedTurns < 0) {
|
|
78
|
+
completedTurns = 0; // seed: the seed turn itself doesn't count
|
|
56
79
|
return false;
|
|
57
80
|
}
|
|
58
|
-
|
|
59
|
-
|
|
81
|
+
completedTurns += 1;
|
|
82
|
+
if (completedTurns >= every) {
|
|
83
|
+
completedTurns = 0;
|
|
60
84
|
return true;
|
|
61
85
|
}
|
|
62
86
|
return false;
|
|
@@ -75,11 +99,44 @@ export function buildRefineDraft(steeringMessage: string): {
|
|
|
75
99
|
return { type: "custom_message", customType: AUTO_REFINE_ENTRY, content: steeringMessage, display: true };
|
|
76
100
|
}
|
|
77
101
|
|
|
102
|
+
/** Structural slice of pi's ProjectedSessionEntry (sourceEntry + messages). */
|
|
103
|
+
export interface ProjectedLike {
|
|
104
|
+
sourceEntry?: { type?: string; customType?: string; id?: string };
|
|
105
|
+
messages?: unknown[];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** A context_edit boundary draft (pi ≥ 0.87): omit one entry from future
|
|
109
|
+
* provider context — raw history, usage, and UI history stay untouched. */
|
|
110
|
+
export interface OmissionDraft {
|
|
111
|
+
type: "context_edit";
|
|
112
|
+
targetId: string;
|
|
113
|
+
replacement: null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Context-edit drafts omitting every LIVE prior auto-refine request from
|
|
118
|
+
* future provider context (pi ≥ 0.87 ContextEditEntry). Invariant: at most
|
|
119
|
+
* ONE auto-refine request is ever model-visible — the newest. Already-omitted
|
|
120
|
+
* drafts surface in the projection with empty `messages` and are skipped, so
|
|
121
|
+
* the operation is idempotent and never stacks redundant edits. Raw history
|
|
122
|
+
* (and the HTML export) keeps every draft: /tree rollback still applies.
|
|
123
|
+
*/
|
|
124
|
+
export function buildOmissionDrafts(projected: Iterable<ProjectedLike>): OmissionDraft[] {
|
|
125
|
+
const drafts: OmissionDraft[] = [];
|
|
126
|
+
for (const p of projected) {
|
|
127
|
+
const src = p.sourceEntry;
|
|
128
|
+
if (src?.type !== "custom_message" || src.customType !== AUTO_REFINE_ENTRY) continue;
|
|
129
|
+
if ((p.messages?.length ?? 0) === 0) continue; // already omitted → skip
|
|
130
|
+
if (typeof src.id === "string") drafts.push({ type: "context_edit", targetId: src.id, replacement: null });
|
|
131
|
+
}
|
|
132
|
+
return drafts;
|
|
133
|
+
}
|
|
134
|
+
|
|
78
135
|
/** Subscribe to turn_end and run /refine on the configured cadence. */
|
|
79
136
|
export function registerAutoRefine(pi: ExtensionAPI): void {
|
|
80
137
|
pi.on("turn_end", async (event, ctx) => {
|
|
81
138
|
const config = await loadConfig();
|
|
82
|
-
if (!evaluateAutoRefine(config, event.
|
|
139
|
+
if (!evaluateAutoRefine(config, event.outcome)) return;
|
|
83
140
|
const every = config.autoRefine?.everyTurns ?? DEFAULT_AUTO_EVERY_TURNS;
|
|
84
141
|
|
|
85
142
|
// Opt-in classifier gate: cheap yes/no before spending the refine.
|
|
@@ -118,7 +175,19 @@ export function registerAutoRefine(pi: ExtensionAPI): void {
|
|
|
118
175
|
pi.sendUserMessage(result.steeringMessage);
|
|
119
176
|
return;
|
|
120
177
|
}
|
|
121
|
-
|
|
178
|
+
// Hygiene (pi ≥ 0.87 context edits): omit every LIVE prior auto-refine
|
|
179
|
+
// request from future provider context, so at most one draft (the new
|
|
180
|
+
// one) is ever model-visible. Already-omitted drafts are skipped by
|
|
181
|
+
// buildOmissionDrafts; raw history and /tree rollback are untouched.
|
|
182
|
+
// Kill-switch: autoRefine.omitStaleDrafts: false.
|
|
183
|
+
const omissions =
|
|
184
|
+
config.autoRefine?.omitStaleDrafts === false
|
|
185
|
+
? []
|
|
186
|
+
: buildOmissionDrafts(event.context?.contextEntries ?? []);
|
|
187
|
+
return {
|
|
188
|
+
entries: [...event.entries, ...omissions, buildRefineDraft(result.steeringMessage)],
|
|
189
|
+
continue: true,
|
|
190
|
+
};
|
|
122
191
|
}
|
|
123
192
|
} catch (err) {
|
|
124
193
|
ctx.ui.notify(`Auto-refine failed: ${(err as Error).message}`, "error");
|