pi-goal-list-loop-audit 0.38.98 → 0.38.104

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +149 -0
  2. package/INSTALL.md +6 -5
  3. package/docs/DESIGN-long-running-supervision.md +3 -3
  4. package/docs/DESIGN.md +8 -4
  5. package/docs/INDEX.md +1 -1
  6. package/docs/SETTINGS.md +8 -0
  7. package/extensions/action-reminder.ts +265 -0
  8. package/extensions/audit-lifecycle.ts +450 -0
  9. package/extensions/compaction-failure.ts +67 -0
  10. package/extensions/compaction-input.ts +299 -44
  11. package/extensions/completion-summary.ts +203 -110
  12. package/extensions/faulty-objective-recovery.ts +15 -1
  13. package/extensions/finding-lead.ts +181 -0
  14. package/extensions/goal-agents-panel.ts +2 -3
  15. package/extensions/goal-commands.ts +200 -12
  16. package/extensions/goal-compactor.ts +49 -3
  17. package/extensions/goal-continuation.ts +154 -11
  18. package/extensions/goal-heartbeat.ts +180 -15
  19. package/extensions/goal-loop-auditor-process.ts +433 -24
  20. package/extensions/goal-loop-auditor.ts +62 -3
  21. package/extensions/goal-loop-core.ts +520 -25
  22. package/extensions/goal-loop-display.ts +165 -31
  23. package/extensions/goal-loop-shield.ts +162 -17
  24. package/extensions/goal-loop-subagents.ts +25 -9
  25. package/extensions/goal-loop.ts +105 -49
  26. package/extensions/goal-recovery.ts +23 -4
  27. package/extensions/goal-settings.ts +69 -2
  28. package/extensions/loops/goal-activation.ts +222 -36
  29. package/extensions/loops/goal-auditor-hooks.ts +477 -99
  30. package/extensions/loops/goal-orchestrator.ts +5 -6
  31. package/extensions/loops/goal-settings-ui.ts +78 -0
  32. package/extensions/loops/goal-tools.ts +321 -80
  33. package/extensions/loops/goal-ui.ts +28 -4
  34. package/extensions/loops/goal.ts +9 -3
  35. package/extensions/main-model-recovery.ts +99 -1
  36. package/extensions/prompt-layers.ts +30 -1
  37. package/extensions/quota-retry.ts +58 -3
  38. package/extensions/settings-menu.ts +43 -2
  39. package/media/glla-thumb.png +0 -0
  40. package/media/glla-thumb.svg +117 -0
  41. package/package.json +3 -3
  42. package/prompts/goal-loop-continuation.md +22 -9
  43. package/schemas/goal.schema.json +50 -3
  44. package/scripts/auditor-stderr-diagnostic.d.mts +8 -0
  45. package/scripts/auditor-stderr-diagnostic.mjs +39 -0
  46. package/scripts/goal-auditor-worker.mjs +189 -13
  47. package/scripts/run-tests.d.mts +8 -0
  48. package/scripts/run-tests.mjs +131 -7
  49. package/scripts/smoke.sh +1 -1
  50. package/scripts/verify-compaction-live.mjs +578 -182
package/CHANGELOG.md CHANGED
@@ -1,5 +1,154 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ Untagged work since 0.38.104. It stays under this heading until the release
6
+ commit renames it (docs/RELEASING.md); these are descriptive milestones, not
7
+ invented version headers.
8
+
9
+ ## 0.38.104 — prose stops grinding the audit (2026-09-29)
10
+
11
+ ### The audit cap covers every round that failed to settle the goal
12
+
13
+ - The regression shield (auditor approved, evidence contract unmet) and an
14
+ aggressive `IMPOSIBLE(partial)` verdict both re-activate the goal without
15
+ disapproving it, so the v0.38.107 hard cap — which counts `disapproved`
16
+ rounds — returned 0 for both and let them cycle forever. They now pause on
17
+ the same `auditCapHard`, offering accept / resume / tweak / cancel.
18
+ - `countTrailingUnsettledRounds` is the shared primitive: every trailing round
19
+ that failed to *settle* the goal, across all verdict classes. An approval and
20
+ an infra error break the streak; mechanical pre-audit gate rows stay
21
+ transparent.
22
+ - An alternating primary/fallback auditor ladder no longer defeats the cap by
23
+ re-baselining the comparable streak to 1 every round. The guard that detects
24
+ this (`trailingStreakGraderStable`) had no production caller; it is wired
25
+ now, and the pause reason says which count it used.
26
+
27
+ ### Honesty fixes found by the same pass
28
+
29
+ - `/goal timeline` counted objections with a raw `superseded` filter while the
30
+ runtime reads them through a migrate-on-read backfill, so it could tell the
31
+ user to re-submit work the loop had already accepted.
32
+ - The continuation self-heal budget could never be refilled: the turn-start
33
+ reset was gated on a live timer, but exhaustion nulls the timer without
34
+ clearing the counter — so the next stuck episode reported an already-spent
35
+ budget with zero re-probes.
36
+ - A blank subtask title reached `taskList` persistence and made the goal's own
37
+ state file fail the published schema. Rejected at the boundary now.
38
+ - `INSTALL.md` said the auditor runs extension-less by default; the mirror is
39
+ on by default, as `README.md` correctly said.
40
+ - The published schema now types the five completion-claim fields the terminal
41
+ render actually writes, plus the task deferral stamp, and drops
42
+ `wallDeadlineAt` — a field no GLLA code has ever written.
43
+
44
+ ### Provider walls: honest classification and bounded redaction
45
+
46
+ - An empty provider response is a transient glitch, not a quota wall, and a
47
+ real `Retry-After` reset outranks the eager first-retry path instead of being
48
+ shortened to 5s.
49
+ - The numeric `5xx` match is anchored, and the redaction cascade is bounded by
50
+ a fraction of the report's lines so sanitising cannot destroy the evidence.
51
+ - The between-tasks compaction threshold is a named token budget
52
+ (`runProactiveCompactionIfDue`), not an "emergency" that could fire on any
53
+ threshold.
54
+
55
+ ### Durable state: writes that either land or say they did not
56
+
57
+ - A retried continuation dispatch is built as a copy and adopted only after
58
+ `persistDispatchRecord` succeeds, so an unwritable `.pi-glla` can no longer
59
+ re-send an identical payload from a durable `retryCount = 0`.
60
+ - The subagent RPC registry releases its listeners instead of retaining every
61
+ rebound session's bus for the life of the process.
62
+ - A finished `result.json` classifies its job dir as dead even when the parent
63
+ PID was reused, so a proven-finished dir is reaped instead of leaking.
64
+ - `mainModelRetryMinutes`, `auditFeedbackChars` and the thinking-level pins are
65
+ normalized at load, so no surface can display a value the runtime ignores.
66
+ - Summary and widget clipping is code-point safe, and `complete_goal`'s
67
+ `newObjective` path goes through `updateGoal` with an explicit revision.
68
+
69
+ ### A park is not terminal
70
+
71
+ - The bounded-horizon and identical-failure auditor parks re-probe themselves
72
+ instead of dead-ending the goal.
73
+ - An unacknowledged continuation turn start self-heals on a doubling,
74
+ probe-bounded cadence; the loop's live event log is no longer tracked.
75
+
76
+ ### The treadmill ceiling reaches the path the field actually takes
77
+
78
+ - The disapproval hard cap (0.38.103) was wired only into `complete_goal`'s
79
+ inline settlement. Real audits settle through the detached driver minutes or
80
+ hours later, which had no cap — under aggressive mode (the default) every hit
81
+ became a TODO and the goal ground forever (hellhunter 12 rounds, junk-runner
82
+ 9, zero cap pauses). The detached path now pauses on the same
83
+ comparable-disapproval streak. See `audit/STUCK-AT-LAST-PART-2026-09-28.md`.
84
+
85
+ ### Tests: observable, hang-proof, orphan-free
86
+
87
+ - `scripts/run-tests.mjs` wraps the serialized suite with a progress
88
+ heartbeat, a bounded stall watchdog, and a teardown that takes the suite
89
+ down with it instead of leaving a runaway. The suite child is spawned
90
+ detached, so the documented process-group kill is real rather than an
91
+ ESRCH-swallowed no-op; `test:all` and both CI jobs run through the wrapper
92
+ and are bounded by `timeout-minutes`.
93
+
94
+ ### Prose stops grinding the audit (field bugs 2026-09-29)
95
+
96
+ - A verification contract with list structure now defines its checkable items
97
+ by the list: a wrapped prose preamble is commentary, not criteria. The
98
+ dracon-log contract's parenthetical preamble had split into "items" no
99
+ evidence could reference, so the shield blocked two genuine approvals
100
+ forever. Shapeless contracts keep the every-line behavior.
101
+ - Mechanical extraction no longer gates on prohibited commands: backticked
102
+ commands under negation ("no longer uses `bun run dev`") are skipped, and
103
+ never-exiting server-mode commands (`bun run dev`, `npm run serve`,
104
+ `*server.py`, …) are rejected at extraction instead of burning the round's
105
+ time budget. The AI auditor judges those items instead.
106
+ - The detached worker's stderr capture accumulates instead of last-chunk-wins
107
+ and strips control characters, so a trailing BEL flush can no longer
108
+ overwrite the real failure — the 90+ field errors recorded as `error: BEL`
109
+ will carry their true cause next time
110
+ (`scripts/auditor-stderr-diagnostic.mjs`).
111
+
112
+ ### Audit pass 2026-09-28 (findings + fixes in this section's range)
113
+
114
+ - An approved audit no longer falls through the settlement driver into the
115
+ residual-failure tail, which wrote a false "infrastructure error" warning and
116
+ a contradictory `approved: false` evidence record after a successful
117
+ completion.
118
+ - `/goal verify` checks for an owed settlement before writing its manual claim,
119
+ so an approved-but-unarchived goal keeps its approval instead of being
120
+ re-audited from scratch.
121
+ - A branch-changed loop park names the loop surface and is resumable, so the
122
+ loop's iteration, best value and history stay reachable.
123
+ - The settings menu no longer promises a 24h recovery ceiling the default
124
+ aggressive mode removes, and renders the legal retention value `0` as a value
125
+ instead of "?".
126
+
127
+ ## 0.38.103 — the treadmill gets a ceiling: inconclusive gates and a hard disapproval cap (2026-09-27)
128
+
129
+ - **A mechanical gate the host kills is no longer a disapproval.** Timeout/kill/output-limit/abort outcomes now classify `inconclusive` and enter the durable infra-retry plan instead of the rework loop — a killed gate is evidence of nothing about the work. Gate budgets also scale with host load (1× at load ≤ cpus, capped 2× past it; `mechanicalLoadScale`, default on, global-only).
130
+ - **Consecutive disapprovals hit a hard ceiling in every mode.** `auditCapHard` (default 8: soft cap 5 + three grace rounds, `0` = unlimited) pauses with a decision even under aggressiveMode keep-going. Cap pauses now offer **Accept with follow-ups** first: archives complete with residuals while the reviewer mines the disapproval reports into `/list` items (`(/goal accept)` in the decision picker).
131
+ - **Required fixes carry severity.** The brief mandates leading `[HIGH]`/`[MED]`/`[LOW]` tags per item plus a severity-honesty rule; each disapproval records a severity census on its objections ledger event. Advisory this release — automatic severity gating follows once field label distribution is measured.
132
+
133
+ ## 0.38.102 — audits converge, and the card stops burying its own gist (2026-09-27)
134
+
135
+ - **The audit brief now bounds what may block approval and defines a reachable bar.** `CHANGE SCOPE` separates blocking defects (in the changed paths, an unverified contract item, a regression this change causes) from non-blocking ones (pre-existing debt outside the change, style, anything untied to a changed path); it also states that the auditor's own prior findings are inputs to re-check, not new targets — that line breaks the self-feeding loop where each round grew the 148KB findings ledger the next round then audited. `WHAT APPROVED MEANS` gives the checkable bar. `REWORK ROUND` reframes round N as “are the PRIOR objections closed?”, with a new in-scope defect still blocking. Measured 612 attempts fleet-wide: the loop was non-terminating (8/8, 13/13, and 20× consecutive disapprovals at 63h elapsed), not slow. (audit/ENDLESS-AUDIT-CONVERGENCE-2026-09-27.md)
136
+ - **An over-budget tool no longer discards the whole audit.** A verification step that blew the 5-minute per-tool budget used to kill the worker, throwing away up to 64 completed tool calls; the goal then re-audited from zero and blew the same budget on the same loaded host. The worker now cancels that tool, resumes with a brief carrying the completed calls, and the parent holds its hard kill for `TOOL_CANCEL_GRACE_MS`. An approving verdict from a verification-incomplete attempt is refused as retryable infrastructure — never laundered into a disapproval, which would send the agent into rework for a load problem. (audit/NON-DESTRUCTIVE-AUDIT-TOOL-TIMEOUT-2026-09-26.md)
137
+ - **A title-like goal objective no longer livelocks.** `# MONSTER MINECRAFT — a first-person voxel survival game` tripped a bare markdown-heading heuristic, so it was paused as suspicious and the queued repair never rewrote the objective; every resume re-tripped it. A heading is now suspicious only when it is a bare report-section label.
138
+ - **A torn `progress.json` read no longer fails a healthy audit.** It is telemetry — the verdict arrives in `result.json` — so an unreadable snapshot is now tolerated on a time bound instead of costing the attempt.
139
+ - **The terminal card stopped burying its own gist.** A `leftOut` field carrying seven scope decisions rendered as one ~900-character bullet; `Remaining` and `Next` now expand an inline `(1) … (2) …` enumeration into an indented list. In `What Changed` the outcome is the bullet and the evidence rides indented beneath it, so the reader sees what changed before reading why.
140
+ - **New store thumbnail**: a mission-control radar with one verified target among several, replacing the generic checkmark. `aggressiveMode` semantics and the `auditCap` gate are deliberately unchanged.
141
+
142
+ ## 0.38.100 — the audit scopes itself to the recorded change set (2026-09-25)
143
+
144
+ - Execution now records the touched paths behind the `fileWrites` counter (`telemetry.files`, first-seen order, deduped, capped at 100 with an overflow count), and the detached-audit brief opens with that change set: verify THESE first instead of exploring unboundedly. The list scopes the START of the audit, never its boundary — subagent writes, shell redirections, and deletions bypass capture, so the brief says so and the auditor still follows the evidence outward. The recorded-facts fallback summary names the paths too (bounded), replacing "changed paths were not captured" for tracked goals.
145
+
146
+ ## 0.38.99 — the detached audit has one legible lifecycle and one settlement (2026-09-25)
147
+
148
+ - The isolated completion audit now names its state instead of guessing: **starting** (claim durable, worker not reporting yet), **running** with its last durable activity, **settling** (approved, archive owed), **approved** (settlement durable), and **recovery-needed** (parked, with a real `/goal resume` action). The widget, status line, goal markdown, and restart recovery all read the same projection, so a claim that a crash left mid-settlement no longer renders as “awaiting completion review” (audit/DETACHED-AUDIT-LIFECYCLE-2026-09-25.md).
149
+ - An unresolved audit can no longer look finished. The approval path persists the verdict on the claim **before** any archive is attempted, keeps that approved claim when the archive fails (so the settlement stays re-drivable), and gates every terminal surface on a durable verdict plus a landed archive. A cold session that finds an approved-but-unarchived claim finishes it without re-running the auditor.
150
+ - Last activity is durable now: the detached worker's real event stamps a throttled heartbeat on the claim, so after a restart a silent attempt is reported as no-progress instead of looking live — the in-process watchdog does not exist in a fresh session.
151
+
3
152
  ## 0.38.98 — audited lifecycle hardening and change-first completion (2026-09-24)
4
153
 
5
154
  - Fresh audit repairs make goal/list/loop state durable across persistence failures, session replacement, compaction failure, repair promotion, and branch-mode completion. Confirmed loop refinements now apply atomically instead of mutating live state before the spec is safely written.
package/INSTALL.md CHANGED
@@ -193,11 +193,12 @@ re-fired behind your back.
193
193
 
194
194
  ## Auditor model requirement
195
195
 
196
- The completion auditor runs in a detached fresh pi RPC process with no
197
- extensions, skills, prompt templates, themes, or context files by default. Its
198
- model therefore needs to work with a built-in pi provider in an extension-less
199
- session. If your normal session model comes from an extension provider, select
200
- a compatible model in `/glla` under the Auditor settings.
196
+ The completion auditor runs in a detached fresh pi RPC process. By default it
197
+ mirrors your session's extension packages (so an extension-provided model
198
+ works in the auditor too), but it loads no skills, prompt templates, themes, or
199
+ context files. If you would rather the auditor run against only built-in pi
200
+ providers, set **Auditor mirror session extensions** to off in `/glla` and then
201
+ select a compatible model under the Auditor settings.
201
202
 
202
203
  The worker resolves `pi` from `PATH` and inherits normal provider configuration.
203
204
  If required, point it at a specific binary:
@@ -125,14 +125,14 @@ and the status/history surfaces. The human layer follows two different purposes:
125
125
  Verification Summary table (Quality Gate | Command | Scope | Status | Notes
126
126
  when supplied), and Final Repository State.
127
127
 
128
- Both surfaces retain the independent auditor verdict and the full six-label
128
+ Both surfaces retain the independent completion review and the full six-label
129
129
  machine recap. A lone approval may be promoted to the chat headline because
130
- it is a durable verdict fact, not because tests passed. Width-bound and
130
+ it is a durable review fact, not because tests passed. Width-bound and
131
131
  external surfaces use the compact single-line projection of all six labels.
132
132
  Every terminal goal notification, including version-bearing already-shipped
133
133
  claims and explicit cancellation, carries the change-first human view or the
134
134
  compact projection; loop notifications do the same. The executor recap and
135
- independent auditor verdict stay separate: an approval is not manufactured from
135
+ independent completion review stay separate: an approval is not manufactured from
136
136
  the presence of a summary.
137
137
 
138
138
  Metric-loop stops use the same six-label contract in their durable loop state
package/docs/DESIGN.md CHANGED
@@ -499,20 +499,24 @@ This is a **clean break** by decision of the user. We do not interop with `pi-go
499
499
  > **Superseded by consolidation (v0.8.0).** The planned per-loop files below
500
500
  > never shipped: loops 1+2 live together in `extensions/loops/goal.ts`
501
501
  > (one state machine, one loop driver), loop 3's helpers in
502
- > `extensions/loops/forever.ts`, rendering in `goal-loop-display.ts`,
503
- > drafting inline in `goal.ts` + `prompts/`. Kept for history.
502
+ > `extensions/goal-loop-forever.ts`, rendering in `goal-loop-display.ts`,
503
+ > drafting inline in `goal.ts` + `prompts/`. Kept for history. v0.38.108: the
504
+ > "Lines" column below is a snapshot of that plan, not a claim about today's
505
+ > tree — where the plan and the shipped path disagree, the shipped path is
506
+ > `extensions/goal-loop-forever.ts` and the auditor prompt is built in
507
+ > `extensions/goal-loop-auditor.ts` (there is no prompt file).
504
508
 
505
509
  | File | Purpose | Lines |
506
510
  |---|---|---|
507
511
  | `extensions/loops/goal.ts` | Loops 1+2 (single goal + list of goals) | shipped |
508
- | `extensions/loops/forever.ts` | Loop 3 (metric loop helpers) | shipped |
512
+ | `extensions/loops/forever.ts` | Loop 3 (metric loop helpers) — plan name; shipped as `extensions/goal-loop-forever.ts` | planned |
509
513
  | `extensions/goal-loop-core.ts` | Shared state machine, types, JSONL | shipped |
510
514
  | `extensions/goal-loop-auditor.ts` | Auditor prompt + compatibility helper | shipped |
511
515
  | `extensions/goal-loop-auditor-process.ts` | Detached worker protocol, IPC, and shield revalidation | shipped |
512
516
  | `scripts/goal-auditor-worker.mjs` | Extension-less RPC auditor child | shipped |
513
517
  | `extensions/goal-loop-display.ts` | Status line + /goal status rendering | shipped |
514
518
  | `prompts/goal-loop-continuation.md` | Templated continuation prompt | ~80 |
515
- | `prompts/goal-loop-auditor.md` | Templated auditor prompt | ~80 |
519
+ | `prompts/goal-loop-auditor.md` | Templated auditor prompt — never shipped; the prompt is built in `extensions/goal-loop-auditor.ts` | planned |
516
520
  | `prompts/goal-loop-draft.md` | Templated drafting prompt | v0.2.0 |
517
521
  | `schemas/goal.schema.json` | JSON Schema for goal state | ~50 |
518
522
 
package/docs/INDEX.md CHANGED
@@ -16,7 +16,7 @@ Policy contracts and recent changes live in the `audit/` directory of the
16
16
  failback; v0.35.9 hardened cross-version npm tarball checks; v0.35.10
17
17
  handles multi-entry npm dry-run reports; v0.35.11 accepts both npm report
18
18
  shapes; v0.35.12 supports npm 12's keyed pack reports; v0.35.13 fixes stale-API recovery loops.
19
- v0.35.14–v0.38.98 continue through the supervisor freeze (`/glla pause`),
19
+ v0.35.14–v0.38.104 continue through the supervisor freeze (`/glla pause`),
20
20
  load hold, auditor picker parity, Windows launch fix, zombie-watchdog
21
21
  subagent carve-out, due-wait backstop, the `/glla agents` visibility panel,
22
22
  durable state-root selection, blank-until-resume auditor context, frozen
package/docs/SETTINGS.md CHANGED
@@ -32,6 +32,11 @@ copies are ignored (the recovery runtime reads the global file):
32
32
  `auditorModelFallbacks`, `auditorToolTimeoutMs`, `auditorStallMs`,
33
33
  `auditJobRetentionMs`, `auditSpotCheckRate`, `auditorInspection`.
34
34
 
35
+ This list is `GLOBAL_ONLY_KEYS` in `extensions/goal-settings.ts`, and a
36
+ project-scope write to any of them is stripped on read. Every other key —
37
+ including `auditCapHard` and `mechanicalLoadScale` — is project-settable and
38
+ a project file wins over the global one.
39
+
35
40
  ## Keys
36
41
 
37
42
  | Key | Default | Meaning |
@@ -69,12 +74,15 @@ copies are ignored (the recovery runtime reads the global file):
69
74
  | `carryover` | `"pause"` | Stale carryover on new activation: `"pause"` / `"clear"` / `"resume"`. |
70
75
  | `autoAcceptDrafts` | `false` | Drafts activate without the Confirm dialog (unattended rigs). |
71
76
  | `auditCap` | `5` (10 aggressive) | Pause after N consecutive auditor disapprovals (`0` = unlimited). |
77
+ | `auditCapHard` | `8` | Hard ceiling on consecutive disapprovals — pauses with a decision even in aggressive mode (`0` = unlimited). Project-settable. |
78
+ | `mechanicalLoadScale` | `true` | Scale mechanical gate budgets with host load up to 2× (`false` = fixed budgets). Project-settable. |
72
79
  | `auditFeedbackChars` | `0` (full) | Max auditor-report chars returned after disapproval (`0` = full). |
73
80
  | `auditorSilent` | `true` | Auditor report renders final-only, no word-by-word HUD. |
74
81
  | `auditorProgressSignals` | `true` | Silent audits show phase label + byte counter. |
75
82
  | `hourlyRetryProbe` | `true` | Extra blind retry at :00:30 every hour while parked. Global-only. |
76
83
  | `subagentModelStrategy` | `"inherit-parent"` | Default subagent model policy for new sessions. |
77
84
  | `subagentModelOverrides` | unset | Per-agent-type model pin; always wins over strategy. |
85
+ | `subagentThinkingOverrides` | unset | Per-agent-type thinking pin (`off`–`max`); unset per type = session inherit. |
78
86
  | `subagentFallbacks` | unset | Per-role fallback chains (first eligible ref wins). |
79
87
  | `subagentDisplayRichness` | `"quiet"` | Ambient worker UI: `"quiet"` (default, troubled workers + count line) / `"compact"` / `"rich"`. |
80
88
  | `aggressiveMode` | `true` | Keep-going defaults (`false` = pause-first policy). |
@@ -0,0 +1,265 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { Box, Text } from "@earendil-works/pi-tui";
3
+
4
+ export const ACTION_REMINDER_CUSTOM_TYPE = "glla-action-reminder";
5
+
6
+ export type ActionReminderKind = "decision" | "blocked" | "error" | "wait" | "standby";
7
+
8
+ export type ActionReminderParkState = "stopped" | "automatic" | "redirect";
9
+
10
+ export interface ActionReminderDetails {
11
+ kind: ActionReminderKind;
12
+ reason: string;
13
+ action: string;
14
+ resumeCommand: string;
15
+ resumeAt?: string;
16
+ diagnostic?: string;
17
+ parkState: ActionReminderParkState;
18
+ /** The persisted goal is safe in every pause state. `parkState` separately
19
+ * distinguishes a stopped turn, automatic wait, or redirect continuation. */
20
+ safelyParked: true;
21
+ }
22
+
23
+ export interface ActionReminderPresentation {
24
+ heading: string;
25
+ why: string;
26
+ next: string;
27
+ resumeCommand?: string;
28
+ resumeAt?: string;
29
+ parkState: ActionReminderParkState;
30
+ }
31
+
32
+ export interface ActionReminderCopy {
33
+ content: string;
34
+ details: ActionReminderDetails;
35
+ presentation: ActionReminderPresentation;
36
+ }
37
+
38
+ export interface PauseAbortMarker {
39
+ ownerSession: unknown;
40
+ goalId: string;
41
+ kind: ActionReminderKind;
42
+ reason: string;
43
+ action?: string;
44
+ resumeCommand: string;
45
+ resumeAt?: string;
46
+ }
47
+
48
+ let pauseAbortMarker: PauseAbortMarker | null = null;
49
+ let pauseTurnStartedAt = 0;
50
+ let pauseTurnGeneration = 0;
51
+ let pauseMarkerGeneration: number | null = null;
52
+
53
+ /** Bind the exact pause state and host owner to the turn pause_goal is about
54
+ * to abort. A session/goal mismatch expires the marker instead of letting it
55
+ * rewrite some later abort. */
56
+ export function markPauseAbort(marker: PauseAbortMarker): void {
57
+ pauseAbortMarker = { ...marker };
58
+ // Keep the public marker contract focused on the pause payload. A monotonic
59
+ // generation captured at agent_start is the fence: consuming requires the
60
+ // marker and its owner/goal to still belong to this exact turn.
61
+ pauseMarkerGeneration = pauseTurnStartedAt === 0 ? null : pauseTurnGeneration;
62
+ }
63
+
64
+ export function markActionReminderTurnStart(): void {
65
+ pauseTurnStartedAt = Date.now();
66
+ pauseTurnGeneration++;
67
+ pauseAbortMarker = null;
68
+ pauseMarkerGeneration = null;
69
+ }
70
+
71
+ export function clearPauseAbort(): void {
72
+ pauseAbortMarker = null;
73
+ pauseTurnStartedAt = 0;
74
+ pauseMarkerGeneration = null;
75
+ }
76
+
77
+ /** Consume a pending pause marker only when this exact owner, goal, and turn
78
+ * still match. `aborted` distinguishes the expected assistant abort from any
79
+ * other message_end that happens to arrive first. */
80
+ export function consumePauseAbort(input: {
81
+ ownerSession: unknown;
82
+ goalId?: string;
83
+ aborted: boolean;
84
+ }): PauseAbortMarker | null {
85
+ const marker = pauseAbortMarker;
86
+ if (!marker) return null;
87
+ if (
88
+ !input.aborted
89
+ || marker.ownerSession !== input.ownerSession
90
+ || marker.goalId !== input.goalId
91
+ || pauseTurnStartedAt === 0
92
+ || pauseMarkerGeneration === null
93
+ || pauseMarkerGeneration !== pauseTurnGeneration
94
+ ) {
95
+ return null;
96
+ }
97
+ pauseAbortMarker = null;
98
+ pauseMarkerGeneration = null;
99
+ return marker;
100
+ }
101
+
102
+ export function buildAbortedAssistantNotice(input: {
103
+ kind: ActionReminderKind;
104
+ reason: string;
105
+ action?: string;
106
+ resumeCommand: string;
107
+ resumeAt?: string;
108
+ }): string {
109
+ const reminder = buildActionReminder(input);
110
+ return `GLLA paused this turn safely — ${reminder.presentation.why} ${reminder.presentation.next}`;
111
+ }
112
+
113
+ /** Build the durable, user-facing explanation for a GLLA pause. Pi's core
114
+ * abort renderer may still print its generic transport line; this message is
115
+ * the GLLA-owned card that tells the user what happened and what to do next. */
116
+ export function buildActionReminder(input: {
117
+ kind: ActionReminderKind;
118
+ reason: string;
119
+ action?: string;
120
+ resumeCommand: string;
121
+ resumeAt?: string;
122
+ diagnostic?: string;
123
+ parkState?: ActionReminderParkState;
124
+ }): ActionReminderCopy {
125
+ const reason = input.reason.trim() || "The current turn cannot continue safely.";
126
+ const suppliedAction = input.action?.trim();
127
+ const action = suppliedAction || input.resumeCommand;
128
+ const parkState = input.parkState ?? (
129
+ input.kind === "wait" || input.kind === "standby" ? "automatic" : "stopped"
130
+ );
131
+ const presentation = buildPausePresentation({
132
+ kind: input.kind,
133
+ reason,
134
+ action: input.kind === "decision" && !suppliedAction ? "" : action,
135
+ resumeCommand: input.resumeCommand,
136
+ resumeAt: input.resumeAt,
137
+ parkState,
138
+ });
139
+ const diagnostic = input.diagnostic?.trim();
140
+ const content = [
141
+ presentation.heading,
142
+ `Why: ${presentation.why}`,
143
+ parkState === "redirect"
144
+ ? "The saved goal is held while the current turn handles the new request."
145
+ : parkState === "automatic"
146
+ ? "Your saved work is intact; no manual action is needed."
147
+ : "Your saved work is intact; this turn stopped before more work could be lost.",
148
+ presentation.next,
149
+ ...(diagnostic ? [`Diagnostic: ${diagnostic}`] : []),
150
+ ].join("\n");
151
+ return {
152
+ content,
153
+ details: {
154
+ kind: input.kind,
155
+ reason,
156
+ action,
157
+ resumeCommand: input.resumeCommand,
158
+ resumeAt: input.resumeAt,
159
+ diagnostic,
160
+ parkState,
161
+ safelyParked: true,
162
+ },
163
+ presentation,
164
+ };
165
+ }
166
+
167
+ /** Normalize all reminder states into one hierarchy used by both transcript
168
+ * copy and the TUI renderer. Raw transport/provider diagnostics stay outside
169
+ * this model; callers may append them only after the actionable lines. */
170
+ export function buildPausePresentation(input: {
171
+ kind: ActionReminderKind;
172
+ reason: string;
173
+ action: string;
174
+ resumeCommand: string;
175
+ resumeAt?: string;
176
+ parkState: ActionReminderParkState;
177
+ }): ActionReminderPresentation {
178
+ const why = input.reason.trim() || "The current turn cannot continue safely.";
179
+ if (input.parkState === "redirect") {
180
+ return {
181
+ heading: "Goal safely parked — handling your new request now",
182
+ why,
183
+ next: `Next: ${input.action || input.resumeCommand}`,
184
+ resumeCommand: input.resumeCommand,
185
+ parkState: input.parkState,
186
+ };
187
+ }
188
+ if (input.kind === "standby") {
189
+ return {
190
+ heading: "Waiting — work is safely parked",
191
+ why,
192
+ next: "No action is needed; the background completion will wake this work automatically.",
193
+ parkState: input.parkState,
194
+ };
195
+ }
196
+ if (input.kind === "wait") {
197
+ return {
198
+ heading: "Waiting — work is safely parked",
199
+ why,
200
+ next: input.resumeAt
201
+ ? `The wait resumes automatically at ${input.resumeAt}.`
202
+ : "The wait resumes automatically when its condition clears.",
203
+ parkState: input.parkState,
204
+ };
205
+ }
206
+ if (input.kind === "decision") {
207
+ return {
208
+ heading: "Decision needed — work is safely parked",
209
+ why,
210
+ next: input.action
211
+ ? `Next: ${input.action}`
212
+ : "Choose an option in the decision card, then resume.",
213
+ resumeCommand: input.resumeCommand,
214
+ parkState: input.parkState,
215
+ };
216
+ }
217
+ return {
218
+ heading: "Action needed — work is safely parked",
219
+ why,
220
+ next: `Next: ${input.action || input.resumeCommand}`,
221
+ resumeCommand: input.resumeCommand,
222
+ parkState: input.parkState,
223
+ };
224
+ }
225
+
226
+ export function registerActionReminderRenderer(pi: ExtensionAPI): void {
227
+ pi.registerMessageRenderer(ACTION_REMINDER_CUSTOM_TYPE, (message, { outputPad }, theme) => {
228
+ const details = message.details as Partial<ActionReminderDetails> | undefined;
229
+ const diagnostics = details?.diagnostic?.trim();
230
+ const kind = details?.kind ?? "blocked";
231
+ const color = kind === "error" ? "error" : kind === "decision" ? "accent" : "warning";
232
+ const parkState = details?.parkState ?? (kind === "wait" || kind === "standby" ? "automatic" : "stopped");
233
+ const presentation = buildPausePresentation({
234
+ kind,
235
+ reason: details?.reason ?? "",
236
+ action: details?.action ?? details?.resumeCommand ?? "",
237
+ resumeCommand: details?.resumeCommand ?? "",
238
+ resumeAt: details?.resumeAt,
239
+ parkState,
240
+ });
241
+ const heading = parkState === "redirect"
242
+ ? `↪ GLLA ${presentation.heading}`
243
+ : kind === "wait" || kind === "standby"
244
+ ? `⏳ GLLA ${presentation.heading}`
245
+ : `⏸ GLLA ${presentation.heading}`;
246
+ const box = new Box(outputPad, 1, (t) => theme.bg("customMessageBg", t));
247
+ box.addChild(new Text(theme.fg(color, heading), 0, 0));
248
+ box.addChild(new Text(theme.fg("dim", `Why: ${presentation.why}`), 0, 0));
249
+ if (presentation.next) box.addChild(new Text(theme.fg("accent", presentation.next), 0, 0));
250
+ if (presentation.parkState === "redirect") {
251
+ box.addChild(new Text(theme.fg("dim", "The saved goal is held while the current turn handles the new request."), 0, 0));
252
+ } else if (presentation.parkState === "automatic") {
253
+ box.addChild(new Text(theme.fg("dim", "Your saved work is intact; no manual action is needed."), 0, 0));
254
+ } else {
255
+ box.addChild(new Text(theme.fg("dim", "Your saved work is intact; this turn stopped before more work could be lost."), 0, 0));
256
+ }
257
+ if (presentation.resumeCommand && presentation.parkState !== "automatic") {
258
+ box.addChild(new Text(theme.fg("dim", `Resume: ${presentation.resumeCommand}`), 0, 0));
259
+ }
260
+ if (diagnostics) {
261
+ box.addChild(new Text(theme.fg("dim", `Diagnostic: ${diagnostics}`), 0, 0));
262
+ }
263
+ return box;
264
+ });
265
+ }