pi-goal-list-loop-audit 0.38.62 → 0.38.67
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 +98 -0
- package/README.md +8 -5
- package/docs/INDEX.md +1 -1
- package/docs/SETTINGS.md +2 -1
- package/extensions/auditor-extensions.ts +90 -0
- package/extensions/auditor-thinking.ts +16 -0
- package/extensions/drafting-handoff.ts +34 -9
- package/extensions/faulty-objective-recovery.ts +19 -0
- package/extensions/goal-commands.ts +70 -3
- package/extensions/goal-continuation.ts +44 -10
- package/extensions/goal-heartbeat.ts +6 -0
- package/extensions/goal-loop-auditor-process.ts +33 -1
- package/extensions/goal-loop-core.ts +260 -3
- package/extensions/goal-loop-display.ts +204 -107
- package/extensions/goal-loop.ts +8 -1
- package/extensions/goal-recovery.ts +25 -2
- package/extensions/goal-settings.ts +18 -0
- package/extensions/length-continue.ts +14 -0
- package/extensions/loops/goal-activation.ts +42 -8
- package/extensions/loops/goal-auditor-hooks.ts +164 -32
- package/extensions/loops/goal-orchestrator.ts +27 -0
- package/extensions/loops/goal-runtime-globals.ts +9 -0
- package/extensions/loops/goal-session.ts +9 -2
- package/extensions/loops/goal-settings-ui.ts +78 -3
- package/extensions/loops/goal-tools.ts +205 -27
- package/extensions/loops/goal-ui.ts +45 -0
- package/extensions/loops/goal.ts +5 -1
- package/extensions/main-model-recovery.ts +13 -0
- package/extensions/settings-menu.ts +8 -0
- package/package.json +1 -1
- package/prompts/goal-loop-draft.md +12 -0
- package/prompts/goal-loop-plan.md +7 -0
- package/schemas/goal.schema.json +7 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,103 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.38.67 — Stop clobbering other extensions' compactions (2026-09-19)
|
|
4
|
+
|
|
5
|
+
### session_before_compact returns nothing (issue #56, ezoushen)
|
|
6
|
+
|
|
7
|
+
The `session_before_compact` handler ended with `return {}`, which under
|
|
8
|
+
pi's last-truthy-wins handler semantics silently discarded a
|
|
9
|
+
previously-run extension's `SessionBeforeCompactResult.compaction` — pi
|
|
10
|
+
fell back to default compaction after the other extension had already paid
|
|
11
|
+
for summarization. The handler only prunes the shared preparation by
|
|
12
|
+
reference and arms the in-flight marker, so it now returns nothing, with a
|
|
13
|
+
regression pin asserting no truthy object return.
|
|
14
|
+
|
|
15
|
+
## 0.38.66 — Auditor reliability: eviction, mirrors, bounded waits (2026-09-19)
|
|
16
|
+
|
|
17
|
+
### Dead-auditor eviction and identical-failure parking (audit-stuck batch)
|
|
18
|
+
|
|
19
|
+
Detached-auditor failures that prove a model ref unresolvable (`Model not
|
|
20
|
+
found` and synonyms) evict the ref from the candidate chain instead of
|
|
21
|
+
retrying it forever. `resume_goal` runs the recovery probe inline rather
|
|
22
|
+
than refusing while main-model recovery is pending. Three consecutive
|
|
23
|
+
identical infra failures park the claim `blocked` with a named chain and a
|
|
24
|
+
structured fingerprint instead of spinning. Parked-queue duplicates coalesce
|
|
25
|
+
instead of piling up, and queued retries drain rather than jam the list head.
|
|
26
|
+
|
|
27
|
+
### Session-identity refusal fix (field 195237)
|
|
28
|
+
|
|
29
|
+
Subagent-session confusion is gone: foreign-context detection compares
|
|
30
|
+
session identity, not object identity, so a worker in the same session is
|
|
31
|
+
no longer refused as foreign.
|
|
32
|
+
|
|
33
|
+
### Bounded agent load waits (field 153404)
|
|
34
|
+
|
|
35
|
+
Far-future agent `pause_goal kind="wait"` requests are clamped to one hour
|
|
36
|
+
with a visible notice instead of parking for 4+ hours unchecked.
|
|
37
|
+
|
|
38
|
+
### Tweak keeps provenance raw (field 184316)
|
|
39
|
+
|
|
40
|
+
`/goal tweak` strips the procedural tail before extracting the verification
|
|
41
|
+
contract while keeping the raw user seed, so filings say what was done
|
|
42
|
+
without laundering what was asked.
|
|
43
|
+
|
|
44
|
+
### Relentless provider retry within bounds (fields 200751/200754)
|
|
45
|
+
|
|
46
|
+
Provider failures keep hammering within the recovery envelope: both
|
|
47
|
+
error-brake callbacks anchor on the recovery episode key instead of matching
|
|
48
|
+
reason strings, so a self-park no longer disarms an armed retry. Genuine
|
|
49
|
+
user pauses and decision-picker stand-downs still stand down.
|
|
50
|
+
|
|
51
|
+
### No mid-objective parking while recovery is armed
|
|
52
|
+
|
|
53
|
+
Agent `error`/`blocked` park requests are refused with routing text while
|
|
54
|
+
provider recovery is armed, and ledgered — the run must not stop
|
|
55
|
+
mid-objective unless it must. Decision waits, genuine blockers, and user
|
|
56
|
+
pauses are untouched.
|
|
57
|
+
|
|
58
|
+
### Suspicious-goal close (field 032245)
|
|
59
|
+
|
|
60
|
+
A goal paused only by the suspicious-objective dispatch shield can now close
|
|
61
|
+
via `complete_goal`: the accept path verifies artifacts through the isolated
|
|
62
|
+
auditor and voids the queued repair on approval. User pauses and repair
|
|
63
|
+
cards (`repairTarget`) still refuse as before.
|
|
64
|
+
|
|
65
|
+
### Standby pause for background agents (field 021655)
|
|
66
|
+
|
|
67
|
+
New structured `standby` pause kind for waits on background agents — the
|
|
68
|
+
card and status line say waiting (not blocked), and completion wakes
|
|
69
|
+
automatically. `blocked`/`error` rendering is unchanged.
|
|
70
|
+
|
|
71
|
+
### Post-drafting stuck paths fixed
|
|
72
|
+
|
|
73
|
+
Persist-failed completion claims report error instead of fake pending;
|
|
74
|
+
auditor-retry waits are capped at 15 minutes; recovery waits name the agent
|
|
75
|
+
path (`resume_goal` retries now). After drafting, the loop no longer gets
|
|
76
|
+
stuck short of completion.
|
|
77
|
+
|
|
78
|
+
### Auditor mirrors session extensions (field 151158)
|
|
79
|
+
|
|
80
|
+
The detached auditor inherits the session's extension packages by default
|
|
81
|
+
(tri-state `auditorMirrorSessionExtensions`, on unless explicitly off), so a
|
|
82
|
+
session-resolved auditor model ref no longer dies as `Model not found` in
|
|
83
|
+
the isolated worker. GLLA never mirrors itself; unresolvable specs drop
|
|
84
|
+
fail-closed; the read-only tool fence is unchanged.
|
|
85
|
+
|
|
86
|
+
### Fallback chains offer the thinking pick (field 152226)
|
|
87
|
+
|
|
88
|
+
Saving a non-empty auditor fallback chain now offers the thinking-level pick
|
|
89
|
+
(the agnes-flash selection sticks), and typing the session model into the
|
|
90
|
+
fallback input names the slot-0 exclusion instead of silently clearing.
|
|
91
|
+
Clearing the chain skips the thinking prompt.
|
|
92
|
+
|
|
93
|
+
### Goal-aware length continuation (PR #55, FOF11)
|
|
94
|
+
|
|
95
|
+
When an active goal's response is truncated at the output-token limit, the
|
|
96
|
+
continuation text first asks whether the goal is already satisfied — calling
|
|
97
|
+
`complete_goal` if so — instead of starting another blind work pass. Plain
|
|
98
|
+
sessions keep the generic text; marker and retry payload follow the text
|
|
99
|
+
actually sent.
|
|
100
|
+
|
|
3
101
|
## 0.38.62 — Whole-note verification aggregation (2026-09-17)
|
|
4
102
|
|
|
5
103
|
### Conservative result parsing
|
package/README.md
CHANGED
|
@@ -421,7 +421,8 @@ Open `/glla` for the settings table. The most important choices are:
|
|
|
421
421
|
unset, auditor thinking inherits the parent session dial (including `max`);
|
|
422
422
|
- **Main-agent and auditor fallback models:** both roles use the same ordered,
|
|
423
423
|
deselectable, bounded fallback-chain picker for provider failures; the
|
|
424
|
-
auditor's session model remains the final last resort
|
|
424
|
+
auditor's session model remains the final last resort. Saving a non-empty
|
|
425
|
+
auditor chain also offers the thinking-level pick so the selection sticks;
|
|
425
426
|
- **Auto-resume:** whether persisted work may restart automatically after a
|
|
426
427
|
session loads; explicit resume commands are always available;
|
|
427
428
|
- **State root:** `workingDir` by default, opt-in `sessionDir`;
|
|
@@ -442,10 +443,12 @@ supervisor.
|
|
|
442
443
|
## Model and auditor requirements
|
|
443
444
|
|
|
444
445
|
The main agent may use the model/provider you normally use in pi. The detached
|
|
445
|
-
auditor starts a fresh
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
extension.
|
|
446
|
+
auditor starts a fresh pi process that mirrors your session's extension
|
|
447
|
+
packages by default, so session-resolved models (including
|
|
448
|
+
provider-extension models) work there too; disable the mirror in `/glla`
|
|
449
|
+
settings if you want the fully isolated extension-less auditor. Choose an
|
|
450
|
+
auditor model in `/glla` if the session model depends on something the
|
|
451
|
+
worker cannot reach.
|
|
449
452
|
|
|
450
453
|
The worker inherits normal pi provider configuration and resolves `pi` from
|
|
451
454
|
`PATH`. If needed, set:
|
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.
|
|
19
|
+
v0.35.14–v0.38.67 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
|
@@ -50,8 +50,9 @@ copies are ignored (the recovery runtime reads the global file):
|
|
|
50
50
|
| `blockForbiddenModelSwitches` | `true` | Revert forbidden switches (`false` = stand but ledger). |
|
|
51
51
|
| `visionAssist` | `true` | Continuation prompts carry the prefer-native-vision directive. |
|
|
52
52
|
| `auditorModel` | unset (session) | Detached auditor primary (`"provider/id"` or bare id). |
|
|
53
|
-
| `auditorModelFallbacks` | `[]` | Ordered auditor fallbacks; session model is final. Global-only. |
|
|
53
|
+
| `auditorModelFallbacks` | `[]` | Ordered auditor fallbacks; session model is final. Saving a non-empty chain offers the thinking pick. Global-only. |
|
|
54
54
|
| `auditorAllowedExtensions` | `[]` | Extension specs the detached auditor may load; resolved fail-closed, `[]` = isolated. |
|
|
55
|
+
| `auditorMirrorSessionExtensions` | `true` | Also load the session's own packages in the worker (GLLA itself never mirrored); `false` = curated allow-list only. |
|
|
55
56
|
| `auditorSameSessionSwap` | `true` | Walk the fallback chain when the auditor is the session model. |
|
|
56
57
|
| `auditorThinkingLevel` | unset (inherit) | Detached auditor reasoning level; picked with the model. |
|
|
57
58
|
| `auditorToolTimeoutMs` | `300000` | Base budget per auditor tool call (30s–6h). Global-only. |
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
|
|
36
36
|
import * as fs from "node:fs";
|
|
37
37
|
import * as path from "node:path";
|
|
38
|
+
import { fileURLToPath } from "node:url";
|
|
38
39
|
|
|
39
40
|
/** One extension the user could allow-list for the auditor. */
|
|
40
41
|
export interface DiscoveredPiExtension {
|
|
@@ -185,6 +186,95 @@ export function resolveAuditorAllowedExtensions(
|
|
|
185
186
|
return out;
|
|
186
187
|
}
|
|
187
188
|
|
|
189
|
+
/** v0.38.65 (field 151158): repo root of THIS extension — the session
|
|
190
|
+
* mirror must never feed GLLA back into its own detached worker (an
|
|
191
|
+
* auditor loading the goal toolkit could re-dispatch goals). Exported
|
|
192
|
+
* for tests; the mirror filter is the real consumer. */
|
|
193
|
+
export function gllaRepoRoot(): string {
|
|
194
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** v0.38.65: true when a mirror candidate is GLLA itself — by package
|
|
198
|
+
* name (no filesystem touch; covers npm: specs and relative paths on
|
|
199
|
+
* machines where the install lives under a different path) or by
|
|
200
|
+
* resolved path inside this repo (covers relative-path specs pointing
|
|
201
|
+
* here under an unfamiliar spelling). */
|
|
202
|
+
export function isSelfMirrorSpec(raw: string, opts: { home: string; cwd?: string }): boolean {
|
|
203
|
+
const trimmed = raw.trim();
|
|
204
|
+
if (!trimmed) return false;
|
|
205
|
+
if (/pi-goal-list-loop-audit/i.test(labelForSpec(trimmed))) return true;
|
|
206
|
+
const root = gllaRepoRoot();
|
|
207
|
+
const bases = [path.join(opts.home, ".pi", "agent"), ...(opts.cwd ? [path.join(opts.cwd, ".pi")] : [])];
|
|
208
|
+
for (const base of bases) {
|
|
209
|
+
const resolved = resolveAuditorExtensionSpec(trimmed, { home: opts.home, cwd: opts.cwd, base });
|
|
210
|
+
if (resolved && (resolved === root || resolved.startsWith(root + path.sep))) return true;
|
|
211
|
+
}
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** v0.38.65 (field 151158): raw session package specs to mirror into the
|
|
216
|
+
* detached auditor — user settings packages[]/extensions[] first, then
|
|
217
|
+
* project settings. Raw strings, not resolved paths: the process layer
|
|
218
|
+
* resolves them fail-closed at dispatch, so an entry that vanishes
|
|
219
|
+
* between mirror and spawn simply drops out. GLLA itself is excluded.
|
|
220
|
+
* Read-only and best-effort like the rest of discovery: unreadable
|
|
221
|
+
* settings files contribute nothing. */
|
|
222
|
+
export function sessionMirrorExtensionSpecs(home: string, cwd?: string): string[] {
|
|
223
|
+
const out: string[] = [];
|
|
224
|
+
const seen = new Set<string>();
|
|
225
|
+
const push = (raw: string): void => {
|
|
226
|
+
const spec = raw.trim();
|
|
227
|
+
const key = spec.toLowerCase();
|
|
228
|
+
if (!spec || seen.has(key)) return;
|
|
229
|
+
if (isSelfMirrorSpec(spec, { home, cwd })) return;
|
|
230
|
+
seen.add(key);
|
|
231
|
+
out.push(spec);
|
|
232
|
+
};
|
|
233
|
+
for (const raw of readJsonArray(path.join(home, ".pi", "agent", "settings.json"))) push(raw);
|
|
234
|
+
if (cwd) {
|
|
235
|
+
for (const raw of readJsonArray(path.join(cwd, ".pi", "settings.json"))) push(raw);
|
|
236
|
+
}
|
|
237
|
+
return out;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** v0.38.65: union the curated allowlist with the session mirror — user
|
|
241
|
+
* entries first (first spelling wins, case-insensitive), then mirrored
|
|
242
|
+
* session specs. Mirror off restores the pre-mirror behavior exactly:
|
|
243
|
+
* the curated allowlist alone, possibly empty (fully isolated). Bounded
|
|
244
|
+
* like the allowlist itself. */
|
|
245
|
+
export function mergeAuditorAllowedExtensions(
|
|
246
|
+
user: string[] | undefined,
|
|
247
|
+
mirror: string[],
|
|
248
|
+
mirrorEnabled: boolean,
|
|
249
|
+
): string[] {
|
|
250
|
+
const base = normalizeAuditorAllowedExtensions(user);
|
|
251
|
+
if (!mirrorEnabled) return base;
|
|
252
|
+
const seen = new Set(base.map((entry) => entry.toLowerCase()));
|
|
253
|
+
const out = [...base];
|
|
254
|
+
for (const entry of normalizeAuditorAllowedExtensions(mirror)) {
|
|
255
|
+
if (seen.has(entry.toLowerCase())) continue;
|
|
256
|
+
seen.add(entry.toLowerCase());
|
|
257
|
+
out.push(entry);
|
|
258
|
+
}
|
|
259
|
+
return out;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** v0.38.65 (field 151158): dispatch-time allowlist — the curated
|
|
263
|
+
* setting union the session package mirror, unless the user explicitly
|
|
264
|
+
* opted out (auditorMirrorSessionExtensions === false). Both dispatch
|
|
265
|
+
* paths (complete_goal launch + provider-retry re-seed) call this so
|
|
266
|
+
* provider parity between session and worker stays identical. */
|
|
267
|
+
export function dispatchAuditorAllowedExtensions(
|
|
268
|
+
user: string[] | undefined,
|
|
269
|
+
mirrorSetting: boolean | undefined,
|
|
270
|
+
home: string,
|
|
271
|
+
cwd?: string,
|
|
272
|
+
): string[] {
|
|
273
|
+
const enabled = mirrorSetting !== false;
|
|
274
|
+
const mirror = enabled ? sessionMirrorExtensionSpecs(home, cwd) : [];
|
|
275
|
+
return mergeAuditorAllowedExtensions(user, mirror, enabled);
|
|
276
|
+
}
|
|
277
|
+
|
|
188
278
|
/** Bounded, deterministic normalization of the allowlist value.
|
|
189
279
|
* Audit 2026-09-06: dedup is case-insensitive like the model fallback
|
|
190
280
|
* chains (npm names cannot differ by case; a same-case pair is always the
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Resolve the auditor dial against each selected candidate, not just the primary.
|
|
2
|
+
* Old settings keep their meaning: unset inherits the session dial. Unsupported
|
|
3
|
+
* levels fall downwards; a non-reasoning model always gets off. */
|
|
4
|
+
export function resolveAuditorThinkingLevel(model: { reasoning?: boolean; thinkingLevelMap?: Record<string, string | null> } | undefined, requested: string): string {
|
|
5
|
+
const levels = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
|
|
6
|
+
if (!model?.reasoning) return 'off';
|
|
7
|
+
const ceiling = levels.indexOf(requested);
|
|
8
|
+
const start = ceiling < 0 ? levels.indexOf('high') : ceiling;
|
|
9
|
+
for (let index = start; index >= 0; index--) {
|
|
10
|
+
const level = levels[index]!;
|
|
11
|
+
const mapped = model.thinkingLevelMap?.[level];
|
|
12
|
+
if (mapped === null || ((level === 'xhigh' || level === 'max') && mapped === undefined)) continue;
|
|
13
|
+
return level;
|
|
14
|
+
}
|
|
15
|
+
return 'off';
|
|
16
|
+
}
|
|
@@ -1,7 +1,25 @@
|
|
|
1
|
-
/** Observes
|
|
2
|
-
*
|
|
3
|
-
*
|
|
1
|
+
/** Observes an unfinished drafting handoff: a list/goal/loop drafting
|
|
2
|
+
* turn that ends with commentary alone. The grill guidance is in effect
|
|
3
|
+
* for the whole episode, so every drafting turn must end with a presented
|
|
4
|
+
* picker, a stated next dependent question, or a concrete proposal — a
|
|
5
|
+
* stop turn with substantive question-less commentary strands the user
|
|
6
|
+
* (field 125335) exactly like the old dangling-intro shape it subsumes.
|
|
7
|
+
* Silence, punctuation, and short acks alone are never evidence that a
|
|
8
|
+
* user wait is broken. The caller owns lifecycle-fenced, once-only dispatch.
|
|
4
9
|
*/
|
|
10
|
+
const DRAFT_COMMENTARY_FLOOR = 40;
|
|
11
|
+
// A stated next dependent question is a legal drafting end — only
|
|
12
|
+
// commentary ALONE corrects. Keep tight: will-ask, next-question, and
|
|
13
|
+
// after-you-answer phrasing, never a bare "ask"/"question" mention
|
|
14
|
+
// (promising questions without stating the next one is the bug).
|
|
15
|
+
const STATED_NEXT_QUESTION_RE = new RegExp(
|
|
16
|
+
[
|
|
17
|
+
String.raw`\b(i[''’]ll|i will|let me)\b[^.!?]{0,100}\bask\b`,
|
|
18
|
+
String.raw`\bnext\b[^.!?]{0,60}\bquestions?\b`,
|
|
19
|
+
String.raw`\b(after|once)\b[^.!?]{0,60}\b(you (answer|reply)|your answer)\b`,
|
|
20
|
+
].join("|"),
|
|
21
|
+
"i",
|
|
22
|
+
);
|
|
5
23
|
export class DraftingHandoffObserver {
|
|
6
24
|
private answered = false;
|
|
7
25
|
private notified = false;
|
|
@@ -11,7 +29,12 @@ export class DraftingHandoffObserver {
|
|
|
11
29
|
|
|
12
30
|
reset(): void {
|
|
13
31
|
this.invalidate();
|
|
14
|
-
|
|
32
|
+
// A fresh drafting episode opens armed: the grill guidance is in
|
|
33
|
+
// effect from turn one, so a commentary-only end strands the user
|
|
34
|
+
// with or without prior Q&A (field 125335 had none to rely on).
|
|
35
|
+
// Picker-outstanding, proposal-presented, and cancelled-picker turns
|
|
36
|
+
// still disarm via noteToolResult below.
|
|
37
|
+
this.answered = true;
|
|
15
38
|
this.notified = false;
|
|
16
39
|
}
|
|
17
40
|
|
|
@@ -27,11 +50,13 @@ export class DraftingHandoffObserver {
|
|
|
27
50
|
|
|
28
51
|
observe(text: string, stopReason: string | undefined): boolean {
|
|
29
52
|
if (!this.answered || this.notified || stopReason !== "stop") return false;
|
|
30
|
-
const
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if (
|
|
53
|
+
const trimmed = text.trim();
|
|
54
|
+
// A real prose question waits without an autonomous retry.
|
|
55
|
+
if (trimmed.includes("?")) return false;
|
|
56
|
+
// Short acks are not substantive commentary.
|
|
57
|
+
if (trimmed.length < DRAFT_COMMENTARY_FLOOR) return false;
|
|
58
|
+
// A stated next dependent question ends the turn legally.
|
|
59
|
+
if (STATED_NEXT_QUESTION_RE.test(trimmed)) return false;
|
|
35
60
|
this.notified = true;
|
|
36
61
|
this.answered = false;
|
|
37
62
|
return true;
|
|
@@ -8,6 +8,25 @@
|
|
|
8
8
|
|
|
9
9
|
import type { Goal, ObjectiveRepairRecord } from "./goal-loop-core.js";
|
|
10
10
|
|
|
11
|
+
/** Prefix written by the dispatch guard when it parks a goal/list item
|
|
12
|
+
* whose objective prose tripped the fragment heuristics. Exported so the
|
|
13
|
+
* complete_goal close path can recognize exactly this pause shape without
|
|
14
|
+
* re-parsing prose. */
|
|
15
|
+
export const SUSPICIOUS_PAUSE_PREFIX = "Suspicious objective detected (";
|
|
16
|
+
|
|
17
|
+
/** A suspicious-objective pause is a dispatch shield, not a work stop:
|
|
18
|
+
* the objective prose looked like report garbage, so the worker model must
|
|
19
|
+
* not be dispatched — but a completion claim carries evidence to the
|
|
20
|
+
* isolated auditor, which verifies real artifacts. Callers use this to keep
|
|
21
|
+
* the close path open on exactly this pause shape. */
|
|
22
|
+
export function isSuspiciousObjectivePause(
|
|
23
|
+
goal: Pick<Goal, "pauseKind" | "pauseReason">,
|
|
24
|
+
): boolean {
|
|
25
|
+
return goal.pauseKind === "blocked"
|
|
26
|
+
&& typeof goal.pauseReason === "string"
|
|
27
|
+
&& goal.pauseReason.startsWith(SUSPICIOUS_PAUSE_PREFIX);
|
|
28
|
+
}
|
|
29
|
+
|
|
11
30
|
export type SuspiciousObjectiveReason =
|
|
12
31
|
| "empty"
|
|
13
32
|
| "archive-metadata"
|
|
@@ -14,7 +14,7 @@ import { renderAgentsPanel, tailChildTranscript, TRANSCRIPT_HEADER_SCAN_MAX_BYTE
|
|
|
14
14
|
import { state, replaceState } from "./goal-state.js";
|
|
15
15
|
import {
|
|
16
16
|
DEFAULT_TOKEN_LIMIT, Goal, ListItem, Status, appendLedger, archiveDir, archivedGoalPath, bumpGoalRevision, sanitizeProviderDisplayText,
|
|
17
|
-
computeListDepthFromLedger, clearQueueItemFiles, deleteQueueItemFile, deleteQueueItemFileResult, extractVerificationContract, formatAuditLog, formatGoalAuditHistory, formatMainModelRecoveryStatus, queueItemSidecarCount,
|
|
17
|
+
computeListDepthFromLedger, clearQueueItemFiles, deleteQueueItemFile, deleteQueueItemFileResult, extractVerificationContract, stripTweakProceduralTail, formatAuditLog, formatGoalAuditHistory, formatMainModelRecoveryStatus, queueItemSidecarCount,
|
|
18
18
|
formatListDepth, goalArgsNeedDrafting, ledgerPath, newGoalId, nowIso, parseListImport, parseListItemDeclaration, readLedgerTail,
|
|
19
19
|
assignQueueOrder, compareQueueItems, readAuditLog, readQueueFromDisk, routeGoalArgs, routeListText, sanitizeDisplayText, sanitizeProviderAuditReport, statusLabel,
|
|
20
20
|
visibleListPosition, visibleListPositions,
|
|
@@ -231,6 +231,14 @@ async function cmdGoal(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
231
231
|
ctx.ui.notify("An audit is already running…", "info");
|
|
232
232
|
return;
|
|
233
233
|
}
|
|
234
|
+
// Pending-claim lifecycle: a stored claim awaiting its verdict owns
|
|
235
|
+
// the goal surface. Verify reports the awaiting state instead of
|
|
236
|
+
// destroying the healthy claim and relaunching blindly — the
|
|
237
|
+
// in-process flag above only covers audits owned by this process.
|
|
238
|
+
if (state.goal.status === "auditing" && state.goal.pendingCompletion) {
|
|
239
|
+
ctx.ui.notify(`A completion audit is already awaiting its verdict — wait for it or ${activeGoalSurfaceCommand("cancel")} to discard the pending claim and start over.`, "info");
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
234
242
|
updateGoal({
|
|
235
243
|
pendingCompletion: {
|
|
236
244
|
completionSummary: "Manual audit requested by the user via /goal verify (no agent completion claim). Verify the objective against the repo directly.",
|
|
@@ -825,7 +833,12 @@ export async function cmdTweak(
|
|
|
825
833
|
ctx.ui.notify(guidance, "info");
|
|
826
834
|
return false;
|
|
827
835
|
}
|
|
828
|
-
|
|
836
|
+
// Now 184316: strip the trailing procedural tail BEFORE contract
|
|
837
|
+
// extraction — a replacement ending in filing instructions plus an
|
|
838
|
+
// embedded command ("Ensure both objective and verification contract
|
|
839
|
+
// are updated, then /goal resume.") must file the said work, not the
|
|
840
|
+
// instructions about the filing. userSeeds below keeps the said words.
|
|
841
|
+
const proposed = extractVerificationContract(stripTweakProceduralTail(adopted));
|
|
829
842
|
const newObjective = proposed.objective;
|
|
830
843
|
if (!newObjective) {
|
|
831
844
|
// (a bare input of "Done when: ..." only yields this — the replacement
|
|
@@ -1060,6 +1073,32 @@ export function hydrateListQueueFromDisk(ctx: ExtensionContext): number {
|
|
|
1060
1073
|
return recovered.length;
|
|
1061
1074
|
}
|
|
1062
1075
|
|
|
1076
|
+
/** v0.38.63 (audit-stuck batch, field 124536): while the head goal is
|
|
1077
|
+
* parked, re-adding an objective that is already queued — or duplicated
|
|
1078
|
+
* inside the same batch — coalesces instead of piling up. Pure; both add
|
|
1079
|
+
* funnels (direct + batch) call it with the live queue. */
|
|
1080
|
+
export function splitParkedQueueDuplicates(texts: string[], queuedObjectives: Array<string | undefined>): { fresh: string[]; coalesced: string[] } {
|
|
1081
|
+
const seen = new Set<string>();
|
|
1082
|
+
for (const objective of queuedObjectives) {
|
|
1083
|
+
if (typeof objective === "string" && objective.trim()) seen.add(normalizeObjective(objective));
|
|
1084
|
+
}
|
|
1085
|
+
const fresh: string[] = [];
|
|
1086
|
+
const coalesced: string[] = [];
|
|
1087
|
+
for (const text of texts) {
|
|
1088
|
+
let key = "";
|
|
1089
|
+
try {
|
|
1090
|
+
key = normalizeObjective(parseListItemDeclaration(text).objective);
|
|
1091
|
+
} catch { /* unparseable — the funnel below refuses it with its own copy */ }
|
|
1092
|
+
if (key && seen.has(key)) {
|
|
1093
|
+
coalesced.push(text);
|
|
1094
|
+
continue;
|
|
1095
|
+
}
|
|
1096
|
+
if (key) seen.add(key);
|
|
1097
|
+
fresh.push(text);
|
|
1098
|
+
}
|
|
1099
|
+
return { fresh, coalesced };
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1063
1102
|
function enqueueItems(ctx: ExtensionContext, texts: string[], source: string, opts?: { autoActivate?: boolean }): number {
|
|
1064
1103
|
hydrateListQueueFromDisk(ctx);
|
|
1065
1104
|
const recentlyDone = recentlyCompletedObjectives(ctx.cwd);
|
|
@@ -1071,7 +1110,19 @@ function enqueueItems(ctx: ExtensionContext, texts: string[], source: string, op
|
|
|
1071
1110
|
ctx.ui.notify(`Skipped ${skipped} item(s) duplicating work COMPLETED in the last 24h (zombie-twin guard): ${first.slice(0, 90)}`, "warning");
|
|
1072
1111
|
}
|
|
1073
1112
|
if (fresh.length === 0) return 0;
|
|
1074
|
-
|
|
1113
|
+
// v0.38.63 (audit-stuck batch, field 124536): while parked, duplicates
|
|
1114
|
+
// of waiting work coalesce — the queue must not grow without progress.
|
|
1115
|
+
let toQueue = fresh;
|
|
1116
|
+
if (state.goal?.status === "paused") {
|
|
1117
|
+
const split = splitParkedQueueDuplicates(fresh, listQueue().map((item) => item.objective));
|
|
1118
|
+
if (split.coalesced.length > 0) {
|
|
1119
|
+
appendLedger(ctx.cwd, "list_parked_duplicate_coalesced", { source, count: split.coalesced.length, objective: split.coalesced[0]!.slice(0, 200), queueDepth: listQueue().length });
|
|
1120
|
+
ctx.ui.notify(`Coalesced ${split.coalesced.length} item(s) already waiting behind the paused head — queue depth holds at ${listQueue().length}.`, "info");
|
|
1121
|
+
}
|
|
1122
|
+
toQueue = split.fresh;
|
|
1123
|
+
if (toQueue.length === 0) return 0;
|
|
1124
|
+
}
|
|
1125
|
+
const items = toQueue.map((text) => {
|
|
1075
1126
|
const extracted = parseListItemDeclaration(text);
|
|
1076
1127
|
return {
|
|
1077
1128
|
id: newGoalId(),
|
|
@@ -1146,6 +1197,11 @@ function enqueueItems(ctx: ExtensionContext, texts: string[], source: string, op
|
|
|
1146
1197
|
appendLedger(ctx.cwd, "list_queue_disk_first", { source, count: itemsToWrite.length, diskFirst });
|
|
1147
1198
|
persistState(ctx);
|
|
1148
1199
|
appendLedger(ctx.cwd, "list_imported", { source, count: itemsToWrite.length });
|
|
1200
|
+
if (state.goal?.status === "paused") {
|
|
1201
|
+
// v0.38.63 (audit-stuck batch, field 124536): a parked head queues
|
|
1202
|
+
// silently today — the pile-up grows invisibly. Name the jam + depth.
|
|
1203
|
+
ctx.ui.notify(`Queued ${itemsToWrite.length} item(s) behind the paused head (${listQueue().length} waiting) — ${state.goal.pauseSuggestedAction ?? "resume the head to continue"}.`, "info");
|
|
1204
|
+
}
|
|
1149
1205
|
if (!state.goal || state.goal.status === "complete" || state.goal.status === "aborted") {
|
|
1150
1206
|
// v0.28.28: unsolicited sources (the reviewer) do NOT auto-start the
|
|
1151
1207
|
// head unless autoResume is on — "I cancelled a goal and the next one
|
|
@@ -1663,8 +1719,19 @@ async function cmdList(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
1663
1719
|
}
|
|
1664
1720
|
|
|
1665
1721
|
/** Append one objective to the list; activate immediately when idle. */
|
|
1722
|
+
/** v0.38.63 (audit-stuck batch, field 124536): parked-coalesce early-out
|
|
1723
|
+
* for the direct funnel — true when the add merged into waiting work. */
|
|
1724
|
+
function coalesceParkedDuplicate(ctx: ExtensionContext, raw: string): boolean {
|
|
1725
|
+
if (state.goal?.status !== "paused") return false;
|
|
1726
|
+
if (splitParkedQueueDuplicates([raw], listQueue().map((item) => item.objective)).coalesced.length === 0) return false;
|
|
1727
|
+
appendLedger(ctx.cwd, "list_parked_duplicate_coalesced", { source: "direct", count: 1, objective: raw.slice(0, 200), queueDepth: listQueue().length });
|
|
1728
|
+
ctx.ui.notify(`Already waiting behind the paused head — coalesced, queue depth holds at ${listQueue().length}.`, "info");
|
|
1729
|
+
return true;
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1666
1732
|
function addSingleItem(ctx: ExtensionContext, raw: string): void {
|
|
1667
1733
|
hydrateListQueueFromDisk(ctx);
|
|
1734
|
+
if (coalesceParkedDuplicate(ctx, raw)) return;
|
|
1668
1735
|
const extracted = parseListItemDeclaration(raw);
|
|
1669
1736
|
const item = assignQueueOrder([{
|
|
1670
1737
|
id: newGoalId(),
|
|
@@ -67,7 +67,7 @@ import {
|
|
|
67
67
|
type ContinuationDispatch,
|
|
68
68
|
} from "./goal-loop-dispatch.js";
|
|
69
69
|
import { BACKOFF_IDLE_RETRY_MS, HEARTBEAT_MAX_NUDGES } from "./goal-loop-backoff.js";
|
|
70
|
-
import { LENGTH_CONTINUE_MAX, LENGTH_CONTINUE_TEXT } from "./length-continue.js";
|
|
70
|
+
import { GOAL_LENGTH_CONTINUE_TEXT, LENGTH_CONTINUE_MAX, LENGTH_CONTINUE_TEXT } from "./length-continue.js";
|
|
71
71
|
|
|
72
72
|
// v0.38.0: GLLA_MONITOR_INTERVAL_MS throttling is deprecated — scheduling is
|
|
73
73
|
// event-driven (250ms→15s adaptive fallback) for every plane, including
|
|
@@ -86,7 +86,7 @@ import { readHandoffBriefExcerpt } from "./goal-compactor.js";
|
|
|
86
86
|
import { loadSettings } from "./goal-settings.js";
|
|
87
87
|
import { clearLoopTimer, isLoopActive } from "./goal-loop.js";
|
|
88
88
|
import { attemptFreshSessionRecovery, mainModelRecoveryActive, recoverMainModelFromSendStorm } from "./goal-recovery.js";
|
|
89
|
-
import { sendStormEscalateMs } from "./main-model-recovery.js";
|
|
89
|
+
import { isCompactionInFlightSince, sendStormEscalateMs } from "./main-model-recovery.js";
|
|
90
90
|
import {
|
|
91
91
|
appendObjectiveRepairRecord,
|
|
92
92
|
applyObjectiveRepair,
|
|
@@ -95,6 +95,7 @@ import {
|
|
|
95
95
|
buildRepairTaskObjective,
|
|
96
96
|
deriveObjectiveRepair,
|
|
97
97
|
hasQueuedObjectiveRepair,
|
|
98
|
+
SUSPICIOUS_PAUSE_PREFIX,
|
|
98
99
|
} from "./faulty-objective-recovery.js";
|
|
99
100
|
|
|
100
101
|
/** goal.ts-owned module lets the continuation cluster reads/writes through
|
|
@@ -130,8 +131,8 @@ export interface ContinuationFlags {
|
|
|
130
131
|
get loopRearmMilestone(): number;
|
|
131
132
|
set loopRearmMilestone(v: number);
|
|
132
133
|
get completionAuditInFlight(): boolean;
|
|
134
|
+
get compactionInFlightSince(): number | null;
|
|
133
135
|
}
|
|
134
|
-
|
|
135
136
|
/** goal.ts functions/consts the continuation cluster calls. Wired by goal.ts
|
|
136
137
|
* at factory time. */
|
|
137
138
|
export interface ContinuationDeps {
|
|
@@ -410,8 +411,15 @@ export function accountSendRearm(ctx: ExtensionContext, kind: "continuation" | "
|
|
|
410
411
|
}
|
|
411
412
|
}
|
|
412
413
|
if (elapsed >= sendStormEscalateMs() && Date.now() - flags.lastActivityAt >= SEND_REARM_ESCALATE_SILENT_MS) {
|
|
413
|
-
|
|
414
|
-
|
|
414
|
+
// In-flight auto-compaction is legitimate busy time (field 083546) —
|
|
415
|
+
// its silence must not read as a stuck send queue. Suppress the
|
|
416
|
+
// escalation (and its recovery park) until the post-compact settle.
|
|
417
|
+
if (isCompactionInFlightSince(flags.compactionInFlightSince)) {
|
|
418
|
+
appendLedger(ctx.cwd, "send_rearm_escalated_suppressed", { reason: "compaction-in-flight", kind, streak, elapsedMinutes: Math.round(elapsed / 60000) });
|
|
419
|
+
} else {
|
|
420
|
+
if (kind === "continuation") { continuationRearmStreak = 0; continuationRearmSince = 0; } else { flags.loopRearmStreak = 0; flags.loopRearmSince = 0; }
|
|
421
|
+
escalateSendRearmStorm(ctx, kind);
|
|
422
|
+
}
|
|
415
423
|
}
|
|
416
424
|
}
|
|
417
425
|
|
|
@@ -946,9 +954,14 @@ export function guardGoalBeforeContinuation(
|
|
|
946
954
|
ctx: ExtensionContext,
|
|
947
955
|
where: string,
|
|
948
956
|
expectedGoalId?: string,
|
|
949
|
-
options: { allowAuditing?: boolean } = {},
|
|
957
|
+
options: { allowAuditing?: boolean; allowSuspiciousClose?: boolean } = {},
|
|
950
958
|
): boolean {
|
|
951
959
|
const allowAuditing = options.allowAuditing === true;
|
|
960
|
+
// v0.38.63 (field 032245): a completion-claim settlement never dispatches
|
|
961
|
+
// the objective prose to a worker — the isolated auditor verifies real
|
|
962
|
+
// artifacts. allowSuspiciousClose skips ONLY the suspicious-objective
|
|
963
|
+
// pause branch below; every other fence (terminal, archive, stale,
|
|
964
|
+
// replan) still applies.
|
|
952
965
|
const goal = state.goal;
|
|
953
966
|
if (!goal) return false;
|
|
954
967
|
if (expectedGoalId && goal.id !== expectedGoalId) {
|
|
@@ -1024,6 +1037,15 @@ export function guardGoalBeforeContinuation(
|
|
|
1024
1037
|
|
|
1025
1038
|
const assessment = assessSuspiciousObjective(goal.objective, goal.verificationContract);
|
|
1026
1039
|
if (!assessment.suspicious) return true;
|
|
1040
|
+
if (options.allowSuspiciousClose === true) {
|
|
1041
|
+
appendLedger(ctx.cwd, "faulty_objective_suspicious_close_allowed", {
|
|
1042
|
+
goalId: goal.id,
|
|
1043
|
+
where,
|
|
1044
|
+
reasons: [...assessment.reasons],
|
|
1045
|
+
note: "completion-claim settlement verifies artifacts, never dispatches the objective; suspicious-objective pause skipped",
|
|
1046
|
+
});
|
|
1047
|
+
return true;
|
|
1048
|
+
}
|
|
1027
1049
|
// v0.35.31 (field: Screenshot_20260822_193744): an EXPLICIT `/goal start
|
|
1028
1050
|
// <text>` is user intent. The fragment heuristics (dangling-fragment,
|
|
1029
1051
|
// lowercase-fragment, …) exist to stop AGENT-authored report garbage from
|
|
@@ -1080,7 +1102,7 @@ export function guardGoalBeforeContinuation(
|
|
|
1080
1102
|
}
|
|
1081
1103
|
goal.status = "paused";
|
|
1082
1104
|
goal.pauseKind = "blocked";
|
|
1083
|
-
goal.pauseReason =
|
|
1105
|
+
goal.pauseReason = `${SUSPICIOUS_PAUSE_PREFIX}${assessment.reasons.join(", ")}).`;
|
|
1084
1106
|
goal.pauseSuggestedAction = `${activeGoalSurfaceCommand("tweak")} the objective if needed; /list next starts the preserved repair/replan task, then resume the original target after its confirmed task list is accepted.`;
|
|
1085
1107
|
goal.pauseResumeAt = undefined;
|
|
1086
1108
|
goal.updatedAt = nowIso();
|
|
@@ -1099,6 +1121,14 @@ export function scheduleContinuation(ctx: ExtensionContext, force = false, delay
|
|
|
1099
1121
|
if (mainModelRecoveryActive()) return;
|
|
1100
1122
|
if (flags.sessionHandoffPending || flags.initialSessionLoadPending || flags.extensionApiStale || flags.staleTerminalDone || flags.zombieStoodDown) return;
|
|
1101
1123
|
if (pendingContinuationDispatch) return;
|
|
1124
|
+
// In-flight auto-compaction: never dispatch into a compacting host
|
|
1125
|
+
// (field 083546). force (explicit user intent) still bypasses — the
|
|
1126
|
+
// user is alive and the host answered. The post-compact settle owns
|
|
1127
|
+
// the single bounded resume probe.
|
|
1128
|
+
if (!force && isCompactionInFlightSince(flags.compactionInFlightSince)) {
|
|
1129
|
+
appendLedger(ctx.cwd, "continuation_dispatch_deferred_compaction", { goalId: state.goal?.id ?? null });
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1102
1132
|
if (continuationDispatchStoodDown && !force) return;
|
|
1103
1133
|
if (force) releaseContinuationDispatchStandDown();
|
|
1104
1134
|
if (!isActionableGoal()) return;
|
|
@@ -1436,21 +1466,25 @@ export function sendLengthContinue(ctx: ExtensionContext, consecutive: number):
|
|
|
1436
1466
|
// chain — same abort-latch reasoning as sendContinuation.
|
|
1437
1467
|
if (flags.sessionHandoffPending || flags.initialSessionLoadPending || !flags.extensionApi || flags.extensionApiStale || continuationDispatchStoodDown || pendingContinuationDispatch || flags.abortedStandDown) return;
|
|
1438
1468
|
if (state.goal && !guardGoalBeforeContinuation(ctx, "length-continuation")) return;
|
|
1469
|
+
// v0.38.66 (PR #55, FOF11): active goals get completion-aware recovery
|
|
1470
|
+
// text — a truncated turn must offer closure, not another blind work
|
|
1471
|
+
// pass. Plain sessions keep the generic text.
|
|
1472
|
+
const content = state.goal?.status === "active" ? GOAL_LENGTH_CONTINUE_TEXT : LENGTH_CONTINUE_TEXT;
|
|
1439
1473
|
const attempt = dispatchPrepare(ctx, {
|
|
1440
1474
|
generation: flags.sessionGeneration,
|
|
1441
1475
|
ownerSessionId: sessionManagerId(ctx),
|
|
1442
1476
|
kind: "length",
|
|
1443
|
-
marker:
|
|
1477
|
+
marker: content.slice(0, 80),
|
|
1444
1478
|
resync: false,
|
|
1445
1479
|
});
|
|
1446
1480
|
if (!attempt) return;
|
|
1447
1481
|
try {
|
|
1448
1482
|
flags.extensionApi.sendMessage({
|
|
1449
1483
|
customType: GOAL_EVENT_ENTRY,
|
|
1450
|
-
content
|
|
1484
|
+
content,
|
|
1451
1485
|
display: true,
|
|
1452
1486
|
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
1453
|
-
lastContinuationSentPayload = { content
|
|
1487
|
+
lastContinuationSentPayload = { content, display: true }; // v0.34.88: verbatim retry payload
|
|
1454
1488
|
if (!dispatchAccepted(ctx, attempt)) return;
|
|
1455
1489
|
appendLedger(ctx.cwd, "length_continue_sent", { consecutive, attemptId: attempt.id });
|
|
1456
1490
|
ctx.ui.notify(`Response hit the output-token cap — auto-continuing (${consecutive}/${LENGTH_CONTINUE_MAX})`, "warning");
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
type Goal,
|
|
34
34
|
} from "./goal-loop-core.js";
|
|
35
35
|
import { loadSettings } from "./goal-settings.js";
|
|
36
|
+
import { isCompactionInFlightSince } from "./main-model-recovery.js";
|
|
36
37
|
import {
|
|
37
38
|
HEARTBEAT_STALL_MS,
|
|
38
39
|
PENDING_LATCH_STUCK_MS,
|
|
@@ -67,6 +68,7 @@ export interface HeartbeatFlags {
|
|
|
67
68
|
get sessionHandoffPending(): boolean;
|
|
68
69
|
set sessionHandoffPending(v: boolean);
|
|
69
70
|
get compactionGraceUntil(): number;
|
|
71
|
+
get compactionInFlightSince(): number | null;
|
|
70
72
|
get continuationDispatchStoodDown(): boolean;
|
|
71
73
|
get pendingContinuationDispatch(): ContinuationDispatch | null;
|
|
72
74
|
get postCompactResumeOwed(): boolean;
|
|
@@ -1317,6 +1319,10 @@ function heartbeatTick(): void {
|
|
|
1317
1319
|
// machinery below stays quiet for 3 minutes while the replaced session
|
|
1318
1320
|
// settles (latch watchdog, wedge alert, refire counting all resume after).
|
|
1319
1321
|
if (Date.now() < flags.compactionGraceUntil) return;
|
|
1322
|
+
// In-flight auto-compaction is legitimate busy time (field 083546) —
|
|
1323
|
+
// the whole stall/refire/watchdog machinery below stays quiet while
|
|
1324
|
+
// the marker is armed, same as the post-compact grace above.
|
|
1325
|
+
if (isCompactionInFlightSince(flags.compactionInFlightSince)) return;
|
|
1320
1326
|
// v0.34.24: an accepted dispatch with no start proof owns the watchdog
|
|
1321
1327
|
// until its bounded timeout. Do not let the generic heartbeat create a
|
|
1322
1328
|
// second blind send underneath it; explicit resume or a fresh session
|