pi-goal-list-loop-audit 0.38.38 → 0.38.40

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 CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.38.40 — provider overflow errors count as starvation (2026-09-09)
4
+
5
+ ### Fixed
6
+
7
+ - **PR #48 (Bjynt, merged):** `isContextStarvedLengthStop` now detects explicit provider context-overflow errors (`exceed_context_size_error`, `exceeds the available context size`, `context window exceeded`, …) arriving as `stopReason="error"`, firing the starvation ladder + emergency compactor (≥90% usage gate kept). Case 1 refactor is behavior-identical. Wiring confirmed live via the raw session message's `errorMessage`.
8
+ - Follow-up noted on the PR: OpenAI-style `maximum context length` wording still falls through the matcher set.
9
+
10
+ ### Verification
11
+
12
+ - Contributor's Case 2 tests + full `release:check` 0 failures; `tsc --noEmit` clean.
13
+
14
+ ## 0.38.39 — Codex-like terminal summary (2026-09-09)
15
+
16
+ ### Fixed
17
+
18
+ - **Terminal `[goal-event]` summary was not Codex-like (field 20260909_013733):** `•` detail bullets followed by dangling `—` trailer lines, every bullet trailing into `…`, a raw `/var/tmp/…` log path in chat, and no next action. The render is now one voice: uniform `•` bullets (approval/counts/record ride as bullets, record stays last), machine paths (`/tmp/…`, `/var/tmp/…`, `*.tgz`, receipt-only paren groups) stripped from chat bullets (archive keeps the full text), `+` joins the clause-boundary cut set, and `withoutStaleNext` is selective — audit-self-referential Next lines still drop but the first concrete next action survives as the closing bullet ahead of the non-do.
19
+ - **Claim-side guidance:** continuation prompt budgets each label value to ~90 chars, bans machine paths from the recap, and asks for one concrete `Next:` or none.
20
+
21
+ ### Verification
22
+
23
+ - New pins: uniform-voice, machine-path strip, `+`-boundary cut, next-action ordering, record-last; full `release:check` 0 failures; `tsc --noEmit` clean.
24
+ - Evidence: `audit/CODEX-SUMMARY-2026-09-09.md`. Archive format and delivery/replay mechanics unchanged.
25
+
3
26
  ## 0.38.38 — elapsed-time duplication cut + PR #47 closed (2026-09-09)
4
27
 
5
28
  ### Fixed
package/docs/INDEX.md CHANGED
@@ -18,7 +18,7 @@ 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.38 continue through the supervisor freeze (`/glla pause`),
21
+ v0.35.14–v0.38.40 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
@@ -86,7 +86,7 @@ export function isUsefulCompletionSummary(text: string | undefined): boolean {
86
86
  * back to the word break, then to a hard cut only when the head holds
87
87
  * no space past the halfway mark — a long token such as a commit hash
88
88
  * must not eviscerate the whole value. Trailing punctuation is stripped
89
- * before the ellipsis. Short values pass through untouched (no `…`). */
89
+ * `npm version…+ latest` cut inside a `+`-joined list). */
90
90
  export function clipSummaryValue(value: string, limit: number): string {
91
91
  const clean = value.replace(/\s+/g, " ").trim();
92
92
  const capped = Number.isFinite(limit) ? Math.max(8, Math.floor(limit)) : 72;
@@ -95,16 +95,16 @@ export function clipSummaryValue(value: string, limit: number): string {
95
95
  const floor = Math.max(16, Math.floor((capped - 1) * 0.4));
96
96
  let boundary = -1;
97
97
  for (let i = 0; i < head.length; i++) {
98
- if (/[,;:·—–(\[]/.test(head[i]!)) boundary = i;
98
+ if (/[,;:·—–(+[\[]/.test(head[i]!)) boundary = i;
99
99
  }
100
100
  if (boundary >= floor) {
101
- const cut = head.slice(0, boundary).replace(/[,;:·—–(\[\s]+$/u, "");
101
+ const cut = head.slice(0, boundary).replace(/[,;:·—–(+[\[\s]+$/u, "");
102
102
  if (cut.length >= Math.min(floor, 16)) return `${cut}…`;
103
103
  }
104
104
  const space = head.lastIndexOf(" ");
105
105
  const kept = (space > capped / 2 ? head.slice(0, space) : head)
106
106
  .trimEnd()
107
- .replace(/[,;:·—–(\[]$/u, "");
107
+ .replace(/[,;:·—–(+\[]$/u, "");
108
108
  return `${kept}…`;
109
109
  }
110
110
 
@@ -152,6 +152,34 @@ export interface HumanCompletionBrief {
152
152
  details: string[];
153
153
  }
154
154
 
155
+ /** v0.38.39 (field 20260909_013733): absolute machine paths read as a
156
+ * machine receipt in user chat (`Tests: … (/var/tmp/glla-….log…`). The
157
+ * chat brief strips `/tmp/…`, `/var/tmp/…`, and `*.tgz` tokens so bullets
158
+ * carry human-readable proof (counts, versions, repo-relative paths);
159
+ * the durable archive keeps the full text. Falls back to the original
160
+ * when stripping would empty the value. */
161
+ export function chatSafeDetailValue(value: string): string {
162
+ const withoutGroups = value.replace(/\([^()]*\)/g, (group) => {
163
+ // A parenthesized group that carries nothing but stripped tokens and
164
+ // receipt words is machine packaging — drop the whole group so no
165
+ // `( tarball )` husk survives. Groups with real words stay verbatim.
166
+ const inner = group
167
+ .slice(1, -1)
168
+ .replace(/(?:\/var)?\/tmp\/\S+/g, "")
169
+ .replace(/\S+\.tgz\b/g, "")
170
+ .replace(/\btarballs?\b|\blogs?\b/gi, "");
171
+ return /[a-z]/i.test(inner) ? group : "";
172
+ });
173
+ const stripped = withoutGroups
174
+ .replace(/(?:\/var)?\/tmp\/\S+/g, "")
175
+ .replace(/\btarballs?\s+\S+\.tgz\b/gi, "")
176
+ .replace(/\S+\.tgz\b/g, "")
177
+ .replace(/\(\s*\)/g, "")
178
+ .replace(/\s{2,}/g, " ")
179
+ .trim();
180
+ return stripped || value;
181
+ }
182
+
155
183
  /** The human briefing: outcome first in its own words, then only the
156
184
  * labels that carry real content (filler like `Unresolved: none` or
157
185
  * `Changed: not recorded` is dropped, never shown). The durable archive
@@ -170,7 +198,9 @@ export function humanCompletionBrief(
170
198
  const separator = line.indexOf(":");
171
199
  if (separator < 0) continue;
172
200
  const content = briefValueContent(line.slice(separator + 1));
173
- if (content) details.push(`${line.slice(0, separator)}: ${clipSummaryValue(content, valueBudget)}`);
201
+ // v0.38.39: machine paths are archive evidence, not chat evidence —
202
+ // strip before clipping so the budget applies to the human text.
203
+ if (content) details.push(`${line.slice(0, separator)}: ${clipSummaryValue(chatSafeDetailValue(content), valueBudget)}`);
174
204
  }
175
205
  return { outcome, details };
176
206
  }
@@ -178,10 +208,27 @@ export function humanCompletionBrief(
178
208
  /** v0.38.20: the agent's `Next:` recap line goes stale the moment the
179
209
  * verdict lands — reprinting it next to the approval trailer reads as
180
210
  * complete-before-verify (field 2026-09-04: `Next: detached auditor
181
- * verdict decides.` printed above `— auditor … approved.`). Every approval
182
- * surface strips it; the full six-label record stays in the archive. */
211
+ * verdict decides.` printed above `— auditor … approved.`).
212
+ * v0.38.39 (field 20260909_013733): the Codex close wants at most ONE
213
+ * concrete next action, so the filter is now selective instead of total:
214
+ * self-referential audit/process lines (`verdict decides`, `awaiting
215
+ * approval`, …) still drop, but the first concrete action survives as
216
+ * the closing bullet. The full six-label record stays in the archive. */
217
+ const STALE_NEXT_PATTERN = /auditor|verdict|approv|audit|settl|review/i;
183
218
  export function withoutStaleNext(details: string[] | undefined): string[] {
184
- return (details ?? []).filter((d) => !/^\s*Next\s*:/i.test(d));
219
+ const kept: string[] = [];
220
+ let actionKept = false;
221
+ for (const detail of details ?? []) {
222
+ if (!/^\s*Next\s*:/i.test(detail)) {
223
+ kept.push(detail);
224
+ continue;
225
+ }
226
+ if (STALE_NEXT_PATTERN.test(detail)) continue;
227
+ if (actionKept) continue;
228
+ actionKept = true;
229
+ kept.push(detail);
230
+ }
231
+ return kept;
185
232
  }
186
233
 
187
234
  /** v0.38.20: the approval chat notify — outcome first, all bounded
@@ -202,13 +249,24 @@ export function buildApprovalChatLines(notice: {
202
249
  `✓ done — ${notice.outcome}`,
203
250
  // v0.38.37 (audit 2026-09-08): one verifiable-result bullet per
204
251
  // informing detail — the Codex closing shape.
252
+ // v0.38.39: the approval/counts/record trailer rides as bullets too
253
+ // (field 20260909_013733 — the `—` tail read as a second voice); the
254
+ // record pointer stays last.
205
255
  ...withoutStaleNext(notice.details).map((detail) => `• ${detail}`),
206
- notice.approval,
207
- ...(notice.counts ? [notice.counts] : []),
208
- notice.record,
256
+ trailerBullet(notice.approval),
257
+ ...(notice.counts ? [trailerBullet(notice.counts)] : []),
258
+ trailerBullet(notice.record),
209
259
  ];
210
260
  }
211
261
 
262
+ /** v0.38.39 (field 20260909_013733): the trailer rides as bullets too —
263
+ * the Codex close is uniform `•` lines (`• Changed:` … `• record:`),
264
+ * never a `•` block followed by dangling `—` lines. The `— ` sigil is
265
+ * stripped at render; the input strings keep it for non-chat surfaces. */
266
+ function trailerBullet(line: string): string {
267
+ return `• ${line.replace(/^—\s*/, "")}`;
268
+ }
269
+
212
270
  /** v0.38.25: the audit-goal counts line — verdict proof ONLY, built from
213
271
  * durable goal state. Raw run stats (turns/file-writes/bash calls) read as
214
272
  * a machine receipt in user chat (field 2026-09-08 223522/223523); the
@@ -297,7 +355,7 @@ export function buildTerminalApprovalRender(input: TerminalApprovalRenderInput):
297
355
  ...(input.extras ?? []),
298
356
  ],
299
357
  recap,
300
- transcriptLines: [...withoutStaleNext(brief.details).map((detail) => `• ${detail}`), input.approval],
358
+ transcriptLines: [...withoutStaleNext(brief.details).map((detail) => `• ${detail}`), trailerBullet(input.approval)],
301
359
  countsLine,
302
360
  outcome: brief.outcome,
303
361
  approval: input.approval,
@@ -46,6 +46,10 @@ export interface AssistantLengthMessageLike {
46
46
  usage?: {
47
47
  output?: number;
48
48
  };
49
+ /** Provider error message, when stopReason is "error". */
50
+ errorMessage?: string;
51
+ /** Error type from provider (e.g., "exceed_context_size_error"). */
52
+ errorType?: string;
49
53
  }
50
54
 
51
55
  export const LENGTH_CONTINUE_CONTEXT_STARVED_PERCENT = 90;
@@ -60,21 +64,49 @@ export const LENGTH_CONTINUE_CONTEXT_STARVED_MAX_OUTPUT = 8;
60
64
  * here queues another 1-token request before pi's post-agent_end compaction
61
65
  * check and delays the actual cure (field: darklord 2026-08-02, 198,116 /
62
66
  * 198,179 total tokens of a 200,000 window, output=1 twice).
67
+ *
68
+ * v0.38.36: also detect explicit context-overflow provider errors
69
+ * (e.g., "exceed_context_size_error", "exceeds the available context size")
70
+ * which arrive as stopReason="error" with an errorMessage, not as
71
+ * stopReason="length". These are also context starvation — the request
72
+ * exceeded the model's context window before generation could start.
63
73
  */
64
74
  export function isContextStarvedLengthStop(
65
75
  message: AssistantLengthMessageLike | null | undefined,
66
76
  contextUsage: ContextUsageLike | null | undefined,
67
77
  ): boolean {
68
- if (message?.stopReason !== "length") return false;
69
- const output = message.usage?.output;
70
- if (typeof output !== "number" || !Number.isFinite(output)) return false;
71
- if (output > LENGTH_CONTINUE_CONTEXT_STARVED_MAX_OUTPUT) return false;
72
- const percent = typeof contextUsage?.percent === "number"
73
- ? contextUsage.percent
74
- : typeof contextUsage?.tokens === "number" && typeof contextUsage?.contextWindow === "number" && contextUsage.contextWindow > 0
75
- ? (contextUsage.tokens / contextUsage.contextWindow) * 100
76
- : null;
77
- return percent !== null && Number.isFinite(percent) && percent >= LENGTH_CONTINUE_CONTEXT_STARVED_PERCENT;
78
+ // Case 1: pi's context-safety clamp (stopReason="length" with tiny output)
79
+ if (message?.stopReason === "length") {
80
+ const output = message.usage?.output;
81
+ if (typeof output === "number" && Number.isFinite(output) && output <= LENGTH_CONTINUE_CONTEXT_STARVED_MAX_OUTPUT) {
82
+ const percent = typeof contextUsage?.percent === "number"
83
+ ? contextUsage.percent
84
+ : typeof contextUsage?.tokens === "number" && typeof contextUsage?.contextWindow === "number" && contextUsage.contextWindow > 0
85
+ ? (contextUsage.tokens / contextUsage.contextWindow) * 100
86
+ : null;
87
+ return percent !== null && Number.isFinite(percent) && percent >= LENGTH_CONTINUE_CONTEXT_STARVED_PERCENT;
88
+ }
89
+ return false; // Real overlong response, not context starvation
90
+ }
91
+ // Case 2: explicit provider context-overflow error (stopReason="error")
92
+ if (message?.stopReason === "error") {
93
+ const errMsg = (message.errorMessage ?? message.errorType ?? "").toLowerCase();
94
+ if (
95
+ errMsg.includes("exceed_context_size") ||
96
+ errMsg.includes("exceeds the available context") ||
97
+ errMsg.includes("exceeds the context window") ||
98
+ (errMsg.includes("exceeds the model") && errMsg.includes("context")) ||
99
+ (errMsg.includes("context window") && errMsg.includes("exceed"))
100
+ ) {
101
+ const percent = typeof contextUsage?.percent === "number"
102
+ ? contextUsage.percent
103
+ : typeof contextUsage?.tokens === "number" && typeof contextUsage?.contextWindow === "number" && contextUsage.contextWindow > 0
104
+ ? (contextUsage.tokens / contextUsage.contextWindow) * 100
105
+ : null;
106
+ return percent !== null && Number.isFinite(percent) && percent >= LENGTH_CONTINUE_CONTEXT_STARVED_PERCENT;
107
+ }
108
+ }
109
+ return false;
78
110
  }
79
111
 
80
112
  export function makeLengthContinueTracker(max: number = LENGTH_CONTINUE_MAX) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.38.38",
3
+ "version": "0.38.40",
4
4
  "description": "Mission control for autonomous pi: interview-drafted goals, an audited task queue, and forever-loops (metric, spec, project-audit) that run for hours. A detached extension-less auditor process re-verifies every completion with raw evidence without holding the main pi turn; confirmed drafts, decision pauses and consent gates keep you in charge.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "author": "dracon",
@@ -197,4 +197,4 @@ The orchestrator's backstop is the stall watchdog: three consecutive turns with
197
197
 
198
198
  `complete_goal` submits a nonterminal claim, not a completion verdict. While the detached audit is pending, do not say the goal is done/complete/approved or present a final success summary. Do not wait or poll. If a response is needed, give one brief pending-status sentence. GLLA posts the concrete outcome-first summary into chat after verified approval and durable archive, without needing another user prompt. Do not duplicate that summary. A rejected claim remains unfinished and repair work continues.
199
199
 
200
- The agent-written completionSummary is user-facing evidence, not a step log: open with the outcome in plain words, then one verifiable result per label with its proof inline (test counts, file paths, commit refs) — never raw run stats. Cut values at clause boundaries, never mid-word; pass what was deliberately left out as the complete_goal leftOut parameter — it renders as the closing bullet, and is omitted when absent.
200
+ The agent-written completionSummary is user-facing evidence, not a step log: open with the outcome in plain words, then one verifiable result per label with its proof inline (test counts, file paths, commit refs) — never raw run stats. Keep each label value to one line of ~90 chars so every chat bullet fits without a trailing …; cite human-readable proof (counts, versions, repo-relative paths, PR numbers) and never machine paths (/var/tmp logs, tarballs — those live in the archive only). Cut values at clause boundaries, never mid-word; write Next: as one concrete follow-up action or none (audit-self-referential lines like "verdict decides" are stripped at render); pass what was deliberately left out as the complete_goal leftOut parameter — it renders as the closing bullet, and is omitted when absent.