pi-goal-list-loop-audit 0.38.22 → 0.38.24
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/CHANGELOG.md +17 -0
- package/docs/DESIGN-subagent-visibility.md +26 -7
- package/docs/INDEX.md +5 -3
- package/docs/SETTINGS.md +85 -0
- package/extensions/auditor-extensions.ts +19 -6
- package/extensions/completion-summary.ts +8 -3
- package/extensions/context-checkpoint.ts +26 -42
- package/extensions/drafter-model.ts +8 -1
- package/extensions/goal-agents-panel.ts +106 -53
- package/extensions/goal-commands.ts +42 -5
- package/extensions/goal-compactor.ts +42 -2
- package/extensions/goal-continuation.ts +80 -8
- package/extensions/goal-heartbeat.ts +12 -0
- package/extensions/goal-loop-core.ts +90 -0
- package/extensions/goal-loop-display.ts +154 -52
- package/extensions/goal-loop-shield.ts +31 -0
- package/extensions/goal-recovery.ts +24 -5
- package/extensions/goal-settings.ts +74 -10
- package/extensions/loops/goal-activation.ts +31 -5
- package/extensions/loops/goal-auditor-hooks.ts +1 -0
- package/extensions/loops/goal-list-queue.ts +9 -0
- package/extensions/loops/goal-orchestrator.ts +5 -1
- package/extensions/loops/goal-runtime-globals.ts +24 -0
- package/extensions/loops/goal-session.ts +40 -5
- package/extensions/loops/goal-settings-ui.ts +36 -9
- package/extensions/loops/goal-tools.ts +6 -4
- package/extensions/loops/goal-ui.ts +20 -12
- package/extensions/model-selector.ts +13 -5
- package/extensions/multi-model-picker.ts +12 -0
- package/extensions/settings-menu.ts +6 -4
- package/extensions/state-root-owner.ts +79 -4
- package/package.json +1 -1
- package/scripts/release-pack-smoke.mjs +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.38.24 — audit pass: abort-latch send guards, ownership compare-and-swap, auditor inherit parity (2026-09-07)
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- Full display/lifecycle/settings audit pass (goal `20260907131550-12ddoy`, three parallel read-only scouts): 44 findings → 0 open. Display decisions (user-grilled): head owns liveness (status drops the `LIVE` capsule/tails, keeps `[WORKING]` + counts), exceptions-only `quiet` default (troubled rows + count line; healthy fan-out lives on the fleet panel), `active {age}` / `quiet {age}` worker labels.
|
|
7
|
+
- Lifecycle HIGH: `sendContinuation` / `sendStallEscalation` / `sendLengthContinue` refuse on `flags.abortedStandDown`; terminal-notice refusal scoped to the abort latch so transcript closure still wins; zombie retry routes on the abort-closure owner (loop abort no longer stranded by a goal started mid-delay); cycle-reset consumes budget (`attempts+1`) and honors the 24h horizon while the probe turn stays prompt per the v0.34.132 contract.
|
|
8
|
+
- Lifecycle MED/LOW: fallback `agent_start`/`turn_start` acks refuse on intervening user messages; successor absorb + self-heal share `clearDeadGenerationDispatch` (no more idle behind a "re-armed" lie); ownership refresh is compare-and-swap (live foreign claims never clobbered; dead records still refresh for heartbeat reclaim); command entry forces a fresh ownership read past the background throttle; length-continue lost its stale-classifier exemption.
|
|
9
|
+
- Settings: `stallShortWords: 0` (= off) survives normalization; auditor thinking gains the drafter's `session — inherit` row + non-reasoning picks clear stale overrides; bare `/glla fallbacks` reads on stale handles (only `clear/off/unset/none` mutates); typing `auto` in the notify editor round-trips to unset; v0.38.23 comment/doc drift swept (richness/quiet copy, reviewer write→migrate lifecycle, DESIGN one-line rows, SETTINGS wedge vs aggressive default). Full audit: `audit/AUDIT-PASS-2026-09-07.md`. Coverage: `release:check` 1979 pass / 0 fail.
|
|
10
|
+
|
|
11
|
+
## 0.38.23 — below-chat widget: forced placement, single-line rows, evidence lifesign (2026-09-07)
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- Below-chat pinning (field: GLLA card vs fleet visibly swapping slots across ticks, 2026-09-06 screenshots). The `pi-glla` widget now passes `{ placement: "belowEditor" }` — the same zone the native fleet defaults to — so both surfaces stop trading places. Forced, not a setting: one stable layout for everyone, no new settings row or drift pin.
|
|
15
|
+
- Extension-meta footers deleted. The orphan-worker `└─ /glla agents for full worker detail`, the card `└─ /goal status · /glla` / `└─ /list · /glla` hints, and the `… N more agents · /glla agents` overflow pointer all named our extension, not the user's task — pure noise once `/glla` is known. Queue depth survives as task info (`└─ N queued`); empty queue means no footer at all.
|
|
16
|
+
- Option-2 worker rows: one glyph-first line per tracked child (`▶ scout · ui-fixes · silent 2m`, `⚠ worker · art-batch · silent 31m`), age before any suffix so narrow-terminal truncation cuts the least important field first. The `→ <objective>` task-linkage header is gone (the card head already names the objective — it repeated it verbatim), ids stay in the `/glla agents` table, overflow names its count only. `/glla agents` full table untouched (`└ blocks:`, Recent hangs, abort guidance all intact).
|
|
17
|
+
- Evidence lifesign on active-clear heads: freshest-worker-evidence readout as the last head segment (`· stream 8s`), breathing glyph derived from evidence counters (`Σ toolUses + outputTokens mod 4` over `●→◉→○→◉`) — never from wall-clock, so the widget key stays stable between evidence and the v0.37.1 re-layout cannot return. Fresh (<5m) breathes, aging (5–30m) freezes the ring, stale (30m+) hollows it, a hung child triangles the head. Non-active statuses (⏸/⟡/⚠/⏳) keep their own glyph language; no rows means no readout invented.
|
|
18
|
+
- Semantic color ramp on head glyph + age text + row glyphs only (success <5m, warning to 30m, error past it; queued caps at amber — waiting is not broken; meter stays progress-colored, fleet line untouched). Color never rides alone: glyph shape (▶/◉/⚠) plus the silence number carry the same meaning unpainted, and a no-theme render contains zero ANSI. Coverage: `tests/lifesign.test.ts` (7 tests: bands incl. queued cap, empty-invents-nothing, counter-derived breath, head readout/triangle/pause/bare, row color + unpainted shape+number, 80-col fit + bucket key-stability) plus reshaped pins in `display`/`agents-panel`/`subagent-display-richness`.
|
|
19
|
+
|
|
3
20
|
## 0.38.22 — subagent display unification: richness ladder + upstream triple-render report (2026-09-05)
|
|
4
21
|
|
|
5
22
|
### Fixed
|
|
@@ -29,15 +29,29 @@ holds (no new instrumentation):
|
|
|
29
29
|
- Ledger history of `subagent_hang_detected` for the "Recent hangs" footer
|
|
30
30
|
of the panel.
|
|
31
31
|
|
|
32
|
-
Row shape:
|
|
32
|
+
Row shape (shipped one-glyph-line since v0.38.23: age-first single lines
|
|
33
|
+
replace the v0.38.22 two-line shape — footers deleted, ids live in
|
|
34
|
+
`/glla agents`; audit 2026-09-07 verified this against the doc and kept
|
|
35
|
+
the code — the doc, not the pins, was stale):
|
|
33
36
|
|
|
34
37
|
```
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
✓ explore schema-check ENDED ok 3m44s
|
|
38
|
+
▶ explore · map-model-picker · active 8s
|
|
39
|
+
▶ plan · audit-contract · quiet 26m
|
|
40
|
+
⚠ plan · stuck-render · HUNG · quiet 31m · id e5f6…
|
|
39
41
|
```
|
|
40
42
|
|
|
43
|
+
Troubled rows append the short id; healthy rows stay self-contained
|
|
44
|
+
without the fleet panel (the v0.38.22 `└ check the Agents panel` footer
|
|
45
|
+
is gone — the row itself says what is wrong). `/glla agents` keeps the
|
|
46
|
+
full detail view (recent hangs read the durable `subagent_hang_detected`
|
|
47
|
+
ledger, absent when empty — never a placeholder).
|
|
48
|
+
|
|
49
|
+
The `└ blocks:` row renders only from an OBSERVED in-flight parent
|
|
50
|
+
subagent wait (tool args carry no run id, so per-row correlation is
|
|
51
|
+
impossible — the wait NAME is named, never inferred). The "Recent hangs"
|
|
52
|
+
footer reads the durable `subagent_hang_detected` ledger (last 3, absent
|
|
53
|
+
when empty — never a placeholder).
|
|
54
|
+
|
|
41
55
|
Hung classification reuses `classifyHungSubagents` semantics (record-frozen
|
|
42
56
|
vs event-only evidence). Cap display at ~20 rows; prune ended probes per
|
|
43
57
|
the existing SUBAGENT_HANG_PRUNE_MS rule.
|
|
@@ -83,8 +97,13 @@ safety property by other means — `renderAgentsWidgetLines` buckets silence
|
|
|
83
97
|
ages exactly like the compact line, so the widget key only moves on genuine
|
|
84
98
|
state transitions — and restores rich ambient rows behind the
|
|
85
99
|
`subagentDisplayRichness` ladder (`rich` default / `compact` / `quiet`, HUNG
|
|
86
|
-
never silent), plus the task-linkage header
|
|
87
|
-
show.
|
|
100
|
+
never silent), plus — until v0.38.23 — the task-linkage header
|
|
101
|
+
(`→ <objective>`) only GLLA can show. v0.38.23 removes the header (the
|
|
102
|
+
card head already names the objective), collapses worker rows to one
|
|
103
|
+
glyph-first line each, forces the card below the chat
|
|
104
|
+
(`{ placement: "belowEditor" }`), and adds the evidence lifesign
|
|
105
|
+
(counter-derived breathing glyph + `stream {age}` readout + semantic
|
|
106
|
+
band colors on head and rows, queued capped at amber). Triplication/ordering against pi-subagents native panels is upstream
|
|
88
107
|
(nicobailon/pi-subagents#1931, read-only); GLLA owns its slot only and
|
|
89
108
|
documents the native escapes. Assembly is the pure `assembleAgentsExtras`
|
|
90
109
|
(pinned in `tests/subagent-display-richness.test.ts`).
|
package/docs/INDEX.md
CHANGED
|
@@ -18,15 +18,16 @@ For shipped docs, the relevant entry points are:
|
|
|
18
18
|
failback; v0.35.9 hardened cross-version npm tarball checks; v0.35.10
|
|
19
19
|
handles multi-entry npm dry-run reports; v0.35.11 accepts both npm report
|
|
20
20
|
shapes; v0.35.12 supports npm 12's keyed pack reports; v0.35.13 fixes stale-API recovery loops.
|
|
21
|
-
v0.35.14–v0.38.
|
|
21
|
+
v0.35.14–v0.38.24 continue through the supervisor freeze (`/glla pause`),
|
|
22
22
|
load hold, auditor picker parity, Windows launch fix, zombie-watchdog
|
|
23
23
|
subagent carve-out, due-wait backstop, the `/glla agents` visibility panel,
|
|
24
24
|
durable state-root selection, blank-until-resume auditor context, frozen
|
|
25
25
|
subagent recovery, bounded repair/replan recovery, production RPC child
|
|
26
26
|
stopping, mandatory hermetic auditor-extension validation, optional provider
|
|
27
27
|
extensions, bounded zero-stream retry containment, crash-safe persistence,
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
packed-artifact release verification, and the 2026-09-07 display/lifecycle/
|
|
29
|
+
settings audit pass (abort-latch send guards, ownership compare-and-swap,
|
|
30
|
+
auditor inherit/clear parity) — see CHANGELOG.md for the full trail.
|
|
30
31
|
- `../README.md` — what the plugin is, install, quickstart, and the
|
|
31
32
|
architectural guarantee (drafting + confirm + detached auditor).
|
|
32
33
|
- `../INSTALL.md` — source install / local development setup; the recommended
|
|
@@ -35,6 +36,7 @@ For shipped docs, the relevant entry points are:
|
|
|
35
36
|
|
|
36
37
|
## Entry points
|
|
37
38
|
- `../README.md` — what the plugin is, install, quickstart
|
|
39
|
+
- `SETTINGS.md` — canonical settings reference (files, precedence, every key)
|
|
38
40
|
- `../INSTALL.md` — source install / local development setup
|
|
39
41
|
- `../CHANGELOG.md` — user-facing changelog; the first versioned heading is
|
|
40
42
|
the current released package version (use `/glla version` to compare with
|
package/docs/SETTINGS.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# GLLA settings reference
|
|
2
|
+
|
|
3
|
+
Canonical settings reference (audit 2026-09-06 — settings truth previously
|
|
4
|
+
lived only in the menu/headless renderers and the changelog). The live
|
|
5
|
+
views remain `/glla` (headless display with provenance) and the `/glla`
|
|
6
|
+
settings menu; this file is the shipped, reviewable contract. A release-gate
|
|
7
|
+
test pins that every `SETTINGS_KEYS` entry in `extensions/goal-settings.ts`
|
|
8
|
+
appears here, so the table cannot silently drift.
|
|
9
|
+
|
|
10
|
+
## Files and precedence
|
|
11
|
+
|
|
12
|
+
- Global file: `~/.pi/agent/glla/settings.json` (machine/provider policy).
|
|
13
|
+
- Project file: `<cwd>/.pi-glla/settings.json` (project artifact).
|
|
14
|
+
- Effective value: project wins over global wins over built-in default.
|
|
15
|
+
- `/glla` shows per-key provenance (`project` / `global` / `default`).
|
|
16
|
+
- Hand-edited files are normalized on every load
|
|
17
|
+
(`normalizeLoadedSettings`): unknown enums fall back, out-of-range
|
|
18
|
+
numbers reset to unset (consumer `??` fallbacks apply), junk strings are
|
|
19
|
+
dropped, and legacy keys migrate (`reviewer` → `postaudit`,
|
|
20
|
+
`auditorModelFallback` → `auditorModelFallbacks`,
|
|
21
|
+
`hourlyQuotaProbe` → `hourlyRetryProbe`).
|
|
22
|
+
|
|
23
|
+
## Global-only keys
|
|
24
|
+
|
|
25
|
+
These describe machine/provider policy, not a project artifact. Project
|
|
26
|
+
copies are ignored (the recovery runtime reads the global file):
|
|
27
|
+
|
|
28
|
+
`stateRoot`, `mainModelFallbacks`, `mainModelRetryMinutes`,
|
|
29
|
+
`mainModelFailback`, `mainModelPrimaryProbeMinutes`, `hourlyRetryProbe`,
|
|
30
|
+
`autoResume`, `drafterModel`, `drafterThinkingLevel`,
|
|
31
|
+
`drafterModelFallbacks`, `compactorModel`, `compactorModelFallbacks`,
|
|
32
|
+
`auditorModelFallbacks`, `auditorToolTimeoutMs`, `auditorStallMs`,
|
|
33
|
+
`auditJobRetentionMs`, `auditorInspection`.
|
|
34
|
+
|
|
35
|
+
## Keys
|
|
36
|
+
|
|
37
|
+
| Key | Default | Meaning |
|
|
38
|
+
| --- | ------- | ------- |
|
|
39
|
+
| `stateRoot` | `"workingDir"` | Where durable state lives; `"sessionDir"` is explicit opt-in. Global-only. |
|
|
40
|
+
| `mainModelFallbacks` | `[]` | Ordered provider/model refs on main-model failure. Global-only. |
|
|
41
|
+
| `drafterModel` | unset (session) | Drafting-only primary model. Global-only. |
|
|
42
|
+
| `drafterThinkingLevel` | unset (inherit) | Thinking level for the drafting agent. Global-only. |
|
|
43
|
+
| `drafterModelFallbacks` | `[]` | Ordered drafting fallbacks; session model is final. Global-only. |
|
|
44
|
+
| `compactorModel` | unset (plan B) | Emergency-compactor primary; never the session model. Global-only. |
|
|
45
|
+
| `compactorModelFallbacks` | `[]` | Ordered compactor fallbacks; no session last resort. Global-only. |
|
|
46
|
+
| `mainModelRetryMinutes` | `15` | Base minutes before main-session recovery; doubles per attempt, caps 5h. Global-only. |
|
|
47
|
+
| `mainModelFailback` | `"auto"` | `"auto"` re-probes the primary; `"sticky"` keeps the fallback. Global-only. |
|
|
48
|
+
| `mainModelPrimaryProbeMinutes` | `15` | Minutes between preferred-primary health probes. Global-only. |
|
|
49
|
+
| `forbiddenModels` | `[]` | Refs that must never be selected (case-insensitive substring). |
|
|
50
|
+
| `blockForbiddenModelSwitches` | `true` | Revert forbidden switches (`false` = stand but ledger). |
|
|
51
|
+
| `visionAssist` | `true` | Continuation prompts carry the prefer-native-vision directive. |
|
|
52
|
+
| `auditorModel` | unset (session) | Detached auditor primary (`"provider/id"` or bare id). |
|
|
53
|
+
| `auditorModelFallbacks` | `[]` | Ordered auditor fallbacks; session model is final. Global-only. |
|
|
54
|
+
| `auditorAllowedExtensions` | `[]` | Extension specs the detached auditor may load; resolved fail-closed, `[]` = isolated. |
|
|
55
|
+
| `auditorSameSessionSwap` | `true` | Walk the fallback chain when the auditor is the session model. |
|
|
56
|
+
| `auditorThinkingLevel` | unset (inherit) | Detached auditor reasoning level; picked with the model. |
|
|
57
|
+
| `auditorToolTimeoutMs` | `300000` | Base budget per auditor tool call (30s–6h). Global-only. |
|
|
58
|
+
| `auditorStallMs` | `600000` | Base silence budget for the detached auditor (1m–24h). Global-only. |
|
|
59
|
+
| `auditJobRetentionMs` | `900000` | How long proven-dead audit job dirs are kept (0–7d, 0 = reap now). Global-only. |
|
|
60
|
+
| `auditorInspection` | `false` | Auditor runs as a persistent session you can tail/resume. Global-only. |
|
|
61
|
+
| `notifyCmd` | unset | Shell command on goal complete / pause / loop stop; message is `$1`. |
|
|
62
|
+
| `tokenLimit` | unset (off) | Per-goal token budget; crossing it pauses. `0` = off. |
|
|
63
|
+
| `wedgeAlertMinutes` | unset (30, or off while aggressive mode is on — the default) | Busy-but-silent minutes before the wedge alert; `0` = off. The menu shows the effective value. |
|
|
64
|
+
| `autoResume` | `false` | Restored goals/loops/lists auto-resume in fresh sessions. Global-only. |
|
|
65
|
+
| `decisionPopup` | `true` | Decision pauses pop the picker (`false` = widget card only). |
|
|
66
|
+
| `carryover` | `"pause"` | Stale carryover on new activation: `"pause"` / `"clear"` / `"resume"`. |
|
|
67
|
+
| `autoAcceptDrafts` | `false` | Drafts activate without the Confirm dialog (unattended rigs). |
|
|
68
|
+
| `auditCap` | `5` (10 aggressive) | Pause after N consecutive auditor disapprovals (`0` = unlimited). |
|
|
69
|
+
| `auditFeedbackChars` | `0` (full) | Max auditor-report chars returned after disapproval (`0` = full). |
|
|
70
|
+
| `auditorSilent` | `true` | Auditor report renders final-only, no word-by-word HUD. |
|
|
71
|
+
| `auditorProgressSignals` | `true` | Silent audits show phase label + byte counter. |
|
|
72
|
+
| `hourlyRetryProbe` | `true` | Extra blind retry at :00:30 every hour while parked. Global-only. |
|
|
73
|
+
| `subagentModelStrategy` | `"inherit-parent"` | Default subagent model policy for new sessions. |
|
|
74
|
+
| `subagentModelOverrides` | unset | Per-agent-type model pin; always wins over strategy. |
|
|
75
|
+
| `subagentFallbacks` | unset | Per-role fallback chains (first eligible ref wins). |
|
|
76
|
+
| `aggressiveMode` | `true` | Keep-going defaults (`false` = pause-first policy). |
|
|
77
|
+
| `stuckMaxInterventions` | `5` (10 aggressive) | Consecutive stuck interventions before a loop stops. |
|
|
78
|
+
| `subagentHangEscalationMinutes` | `30` | Confirmed no-progress minutes before child-specific abort (`0` = warn only). |
|
|
79
|
+
| `stallEscalationRefires` | `5` | Heartbeat refires before pause/stop (`0` = never). |
|
|
80
|
+
| `zombieRetryMaxAttempts` | `3` | Re-dispatches per busy/no-stream episode (`0` = manual; max 10). |
|
|
81
|
+
| `stallShortWords` | `15` | Tool-less turn under this many words is a nudge. |
|
|
82
|
+
| `stallSimilarityThreshold` | `0.6` | Trigram similarity above this (tool-less) is a nudge. |
|
|
83
|
+
| `postaudit` | unset | Post-completion audit config (same shape as legacy `reviewer`). |
|
|
84
|
+
| `toolOverrides` | unset | Per-tool allow/hide/per-tool-config overrides. |
|
|
85
|
+
| `reviewer` | legacy | Deprecated alias for `postaudit`; migrated on load, `postaudit` wins. |
|
|
@@ -185,7 +185,10 @@ export function resolveAuditorAllowedExtensions(
|
|
|
185
185
|
return out;
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
/** Bounded, deterministic normalization of the allowlist value.
|
|
188
|
+
/** Bounded, deterministic normalization of the allowlist value.
|
|
189
|
+
* Audit 2026-09-06: dedup is case-insensitive like the model fallback
|
|
190
|
+
* chains (npm names cannot differ by case; a same-case pair is always the
|
|
191
|
+
* same intent). The first spelling wins; the original is preserved. */
|
|
189
192
|
export function normalizeAuditorAllowedExtensions(value: unknown): string[] {
|
|
190
193
|
if (!Array.isArray(value)) return [];
|
|
191
194
|
const seen = new Set<string>();
|
|
@@ -193,9 +196,10 @@ export function normalizeAuditorAllowedExtensions(value: unknown): string[] {
|
|
|
193
196
|
for (const entry of value) {
|
|
194
197
|
if (typeof entry !== "string") continue;
|
|
195
198
|
const spec = entry.trim();
|
|
196
|
-
|
|
199
|
+
const key = spec.toLowerCase();
|
|
200
|
+
if (!spec || seen.has(key)) continue;
|
|
197
201
|
if (out.length >= 32) break; // bounded: the request hash stays sane
|
|
198
|
-
seen.add(
|
|
202
|
+
seen.add(key);
|
|
199
203
|
out.push(spec);
|
|
200
204
|
}
|
|
201
205
|
return out;
|
|
@@ -294,10 +298,19 @@ export function discoverAuditorExtensions(home: string, cwd?: string): Discovere
|
|
|
294
298
|
}
|
|
295
299
|
|
|
296
300
|
/** Expand the allowlist into `pi` CLI args: ["--extension", spec, …].
|
|
297
|
-
*
|
|
298
|
-
|
|
301
|
+
* Audit 2026-09-06: entries resolve fail-closed through
|
|
302
|
+
* resolveAuditorAllowedExtensions first — raw `npm:`/relative specs are
|
|
303
|
+
* never emitted (a raw npm: entry would trigger a fresh network install,
|
|
304
|
+
* or silently load nothing offline). Empty/absent list → [] (the worker
|
|
305
|
+
* keeps plain --no-extensions). */
|
|
306
|
+
export function auditorExtensionArgs(
|
|
307
|
+
allowed: string[] | undefined,
|
|
308
|
+
home: string,
|
|
309
|
+
cwd?: string,
|
|
310
|
+
settingsBase?: string,
|
|
311
|
+
): string[] {
|
|
299
312
|
const args: string[] = [];
|
|
300
|
-
for (const spec of
|
|
313
|
+
for (const spec of resolveAuditorAllowedExtensions(allowed, home, cwd, settingsBase)) {
|
|
301
314
|
args.push("--extension", spec);
|
|
302
315
|
}
|
|
303
316
|
return args;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Goal, Status } from "./goal-loop-core.js";
|
|
2
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* The durable, user-facing terminal recap contract. Keep this as a small
|
|
@@ -191,7 +192,7 @@ export function buildApprovalChatLines(notice: {
|
|
|
191
192
|
* word-bounded values. This is the user-facing `✓ done` block — six short
|
|
192
193
|
* facts that stay scannable in chat. The single-line projection remains
|
|
193
194
|
* for width-bound surfaces (TUI widget card, external notifies). */
|
|
194
|
-
export function completionSummaryLines(text: string | undefined, maxValueLength = 240): string[] {
|
|
195
|
+
export function completionSummaryLines(text: string | undefined, maxValueLength = 240, lineWidth?: number): string[] {
|
|
195
196
|
const source = completionSummaryBody(text ?? "").replace(/\s+/g, " ").trim();
|
|
196
197
|
const lower = source.toLowerCase();
|
|
197
198
|
const positions = COMPLETION_SUMMARY_LABELS
|
|
@@ -207,7 +208,10 @@ export function completionSummaryLines(text: string | undefined, maxValueLength
|
|
|
207
208
|
.map((entry) => entry.start)
|
|
208
209
|
.sort((a, b) => a - b)[0] ?? source.length;
|
|
209
210
|
const rawValue = source.slice(valueStart, nextStart).trim();
|
|
210
|
-
|
|
211
|
+
const line = `${name}: ${clipSummaryValue(rawValue || "not recorded", maxValueLength)}`;
|
|
212
|
+
// Audit 2026-09-06: optional width budget for width-bound surfaces —
|
|
213
|
+
// the default 240-char values previously had no width-conscious path.
|
|
214
|
+
return lineWidth && lineWidth > 0 ? truncateToWidth(line, lineWidth, "…") : line;
|
|
211
215
|
});
|
|
212
216
|
}
|
|
213
217
|
|
|
@@ -322,8 +326,9 @@ export function terminalCompletionSummaryLines(
|
|
|
322
326
|
facts: CompletionSummaryFacts,
|
|
323
327
|
candidate = facts.goal.completionSummary,
|
|
324
328
|
maxValueLength = 240,
|
|
329
|
+
lineWidth?: number,
|
|
325
330
|
): string[] {
|
|
326
|
-
return completionSummaryLines(resolveCompletionSummary(facts, candidate).summary, maxValueLength);
|
|
331
|
+
return completionSummaryLines(resolveCompletionSummary(facts, candidate).summary, maxValueLength, lineWidth);
|
|
327
332
|
}
|
|
328
333
|
|
|
329
334
|
/**
|
|
@@ -78,26 +78,28 @@ function loopNumber(value: unknown): string {
|
|
|
78
78
|
|
|
79
79
|
/**
|
|
80
80
|
* The loop is a separate work surface from Goal. Keep its durable target and
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
81
|
+
* authority in the checkpoint so a loop-only session does not lose its
|
|
82
|
+
* context when old goal-event payloads are projected out.
|
|
83
|
+
*
|
|
84
|
+
* CACHE-STABILITY INVARIANT (provider prefix-cache rule): this checkpoint is
|
|
85
|
+
* inserted into the effective per-send message list at the position of the
|
|
86
|
+
* first removed goal-event payload — i.e. EARLY in history, before every
|
|
87
|
+
* provider call. Any byte change here invalidates the cache prefix for every
|
|
88
|
+
* token after it. This block therefore renders ONLY fields that are stable
|
|
89
|
+
* for the life of the loop run (target / measure / bounds config). Live
|
|
90
|
+
* counters (iteration, best, last, stall, tokens, history, timestamps) are
|
|
91
|
+
* intentionally NOT repeated here: they already ride the newest retained
|
|
92
|
+
* `[LOOP ITERATION …]` prompt (message-list suffix) and durable
|
|
93
|
+
* `.pi-glla/active.jsonl`. Rendering them here changed the prefix on every
|
|
94
|
+
* tick, making every loop iteration a full cache miss.
|
|
84
95
|
*/
|
|
85
96
|
function loopCheckpointLines(loop: LoopState): string[] {
|
|
86
|
-
const history = Array.isArray(loop.history) ? loop.history : [];
|
|
87
|
-
const recentHistory = history.slice(-8).map((entry) =>
|
|
88
|
-
`iteration=${loopNumber(entry.iteration)}, value=${entry.value === null ? "null" : loopNumber(entry.value)}, improved=${entry.improved === true ? "true" : "false"}, at=${safeInline(entry.at, 80) || "(unknown)"}`,
|
|
89
|
-
);
|
|
90
|
-
if (history.length > recentHistory.length) {
|
|
91
|
-
recentHistory.unshift(`[…${history.length - recentHistory.length} earlier loop measurement(s) omitted]`);
|
|
92
|
-
}
|
|
93
97
|
return [
|
|
94
98
|
"Active loop authority (durable loop state; not a user request):",
|
|
95
99
|
`Loop target: ${safeInline(loop.target, 2_000) || "(missing — recover from durable loop state before proceeding)"}`,
|
|
96
100
|
`Loop measure: command=${safeInline(loop.measureCmd, 1_200) || "(metricless)"}; direction=${safeInline(loop.direction, 20) || "(none)"}`,
|
|
97
|
-
`Loop
|
|
98
|
-
|
|
99
|
-
`Loop history (latest ${Math.min(history.length, 8)} of ${history.length}):\n${recentHistory.length > 0 ? recentHistory.join("\n") : "(none yet)"}`,
|
|
100
|
-
`Loop lifecycle: startedAt=${safeInline(loop.startedAt, 100) || "(unknown)"}; lastIterationCompletedAt=${safeInline(loop.lastIterationCompletedAt, 100) || "(none)"}; specFile=${safeInline(loop.specFile, 300) || "(none)"}; refineHint=${safeInline(loop.refineHint, 600) || "(none)"}`,
|
|
101
|
+
`Loop bounds: maxIterations=${loopNumber(loop.maxIterations)}; plateauWindow=${loopNumber(loop.plateauWindow)}; timeLimitHours=${loopNumber(loop.timeLimitHours)}; tokenBudget=${loopNumber(loop.tokenBudget)}; specFile=${safeInline(loop.specFile, 300) || "(none)"}; startedAt=${safeInline(loop.startedAt, 100) || "(unknown)"}`,
|
|
102
|
+
"Loop progress: live counters (iteration/best/last/stall/history/tokens/completed-at) are intentionally omitted here — read the newest retained [LOOP ITERATION …] prompt and .pi-glla/active.jsonl. This checkpoint stays byte-stable across iterations so provider prefix caches keep hitting.",
|
|
101
103
|
];
|
|
102
104
|
}
|
|
103
105
|
|
|
@@ -146,15 +148,13 @@ const OVERFLOW_AUDIT_CHARS = 1_100;
|
|
|
146
148
|
const OVERFLOW_EMERGENCY_LINE_CHARS = 500;
|
|
147
149
|
|
|
148
150
|
function compactLoopCheckpointLines(loop: LoopState): string[] {
|
|
149
|
-
|
|
150
|
-
const latest = historyCount > 0 ? loop.history[historyCount - 1] : undefined;
|
|
151
|
+
// Same cache-stability invariant as loopCheckpointLines: stable fields only.
|
|
151
152
|
return [
|
|
152
153
|
"Active loop authority (durable loop state; not a user request):",
|
|
153
154
|
`Loop target: ${safeInline(loop.target, OVERFLOW_LOOP_TARGET_CHARS) || "(missing — recover from durable loop state before proceeding)"}`,
|
|
154
155
|
`Loop measure: command=${safeInline(loop.measureCmd, OVERFLOW_LOOP_MEASURE_CHARS) || "(metricless)"}; direction=${safeInline(loop.direction, 20) || "(none)"}`,
|
|
155
|
-
`Loop
|
|
156
|
-
|
|
157
|
-
`Loop lifecycle: startedAt=${safeInline(loop.startedAt, 70) || "(unknown)"}; lastIterationCompletedAt=${safeInline(loop.lastIterationCompletedAt, 70) || "(none)"}; specFile=${safeInline(loop.specFile, 160) || "(none)"}; historyCount=${historyCount}; latestIteration=${latest ? loopNumber(latest.iteration) : "(none)"}`,
|
|
156
|
+
`Loop bounds: maxIterations=${loopNumber(loop.maxIterations)}; plateauWindow=${loopNumber(loop.plateauWindow)}; timeLimitHours=${loopNumber(loop.timeLimitHours)}; tokenBudget=${loopNumber(loop.tokenBudget)}; specFile=${safeInline(loop.specFile, 160) || "(none)"}`,
|
|
157
|
+
"Loop progress: live counters omitted by design — see the newest retained loop prompt and .pi-glla/active.jsonl (keeps this checkpoint byte-stable for prefix-cache stability).",
|
|
158
158
|
];
|
|
159
159
|
}
|
|
160
160
|
|
|
@@ -212,7 +212,7 @@ function buildOverflowCheckpoint(
|
|
|
212
212
|
|
|
213
213
|
const optionalLines = [
|
|
214
214
|
loop
|
|
215
|
-
?
|
|
215
|
+
? "Loop history summary: omitted by design — live history rides the newest retained loop prompt (keeps this checkpoint byte-stable for prefix-cache stability)"
|
|
216
216
|
: null,
|
|
217
217
|
goal
|
|
218
218
|
? `Auto-continuation: ${goal.autoContinue === true ? "enabled" : "disabled/unknown"}; stopReason=${safeInline(goal.stopReason, 220) || "(none)"}; pauseKind=${safeInline(goal.pauseKind, 40) || "(none)"}`
|
|
@@ -312,28 +312,12 @@ function customType(message: unknown): string | null {
|
|
|
312
312
|
return typeof value === "string" ? value : null;
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
if (typeof block === "string") return block;
|
|
322
|
-
if (typeof block !== "object" || block === null) return "";
|
|
323
|
-
const text = (block as { text?: unknown }).text;
|
|
324
|
-
return typeof text === "string" ? text : "";
|
|
325
|
-
}).join("\n");
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
/** True for GLLA control-plane messages that may be safely bounded. */
|
|
329
|
-
export function isGllaControlMessage(message: unknown): boolean {
|
|
330
|
-
const type = customType(message);
|
|
331
|
-
if (type === "goal-event" || type === AUTHORITATIVE_CHECKPOINT_CUSTOM_TYPE) return true;
|
|
332
|
-
const text = messageText(message);
|
|
333
|
-
return typeof (message as { role?: unknown } | null)?.role === "string"
|
|
334
|
-
&& (text.includes("[GOAL CHECKPOINT") || text.includes("[STALL WARNING"));
|
|
335
|
-
}
|
|
336
|
-
|
|
315
|
+
// Audit 2026-09-06: a substring-based `isGllaControlMessage` predicate used
|
|
316
|
+
// to live here (any message containing "[GOAL CHECKPOINT" / "[STALL
|
|
317
|
+
// WARNING" counted as control plane). It had zero callers — the live
|
|
318
|
+
// projection keys on customType only — and any future caller would inherit
|
|
319
|
+
// the forgeable-substring hazard (crafted text projected out of context).
|
|
320
|
+
// Deleted instead of hardened: control-plane identity is customType.
|
|
337
321
|
function isGoalEventPayload(message: unknown): boolean {
|
|
338
322
|
return customType(message) === "goal-event";
|
|
339
323
|
}
|
|
@@ -98,7 +98,14 @@ export function resolveDrafterModel(ctx: ExtensionContext, settings: Pick<Settin
|
|
|
98
98
|
candidates.push({ ref: selected.ref, model: selected.model, via: "configured" });
|
|
99
99
|
}
|
|
100
100
|
if (ctx.model && currentRef && !candidates.some((candidate) => candidate.ref.toLowerCase() === currentRef.toLowerCase())) {
|
|
101
|
-
|
|
101
|
+
// Audit 2026-09-06: the session last resort must not bypass the
|
|
102
|
+
// forbiddenModels gate — a forbidden session model is refused (and
|
|
103
|
+
// ledgered) instead of being leased silently.
|
|
104
|
+
if (forbidden(currentRef)) {
|
|
105
|
+
appendLedger(ctx.cwd, "forbidden_model_switch", { scope: "drafter", ref: currentRef, via: "session-last-resort", blocked: true });
|
|
106
|
+
} else {
|
|
107
|
+
candidates.push({ ref: currentRef, model: ctx.model, via: "session-last-resort" });
|
|
108
|
+
}
|
|
102
109
|
}
|
|
103
110
|
return { configuredRefs, candidates, selected: candidates[0] };
|
|
104
111
|
}
|