pi-goal-list-loop-audit 0.35.13 → 0.35.64
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 +977 -0
- package/INSTALL.md +13 -3
- package/LIST-PHILOSOPHY.md +90 -0
- package/PLAN.md +316 -0
- package/README.md +201 -26
- package/docs/DESIGN-subagent-visibility.md +76 -0
- package/docs/DESIGN.md +59 -8
- package/docs/GLLA-POSITIONING-AND-DECOMPOSITION-2026-08-08.md +6 -1
- package/docs/INDEX.md +4 -0
- package/docs/VISION-ASSIST.md +2 -2
- package/extensions/auditor-extensions.ts +289 -0
- package/extensions/context-hygiene.ts +129 -0
- package/extensions/faulty-objective-recovery.ts +24 -0
- package/extensions/glla-state-root.ts +99 -0
- package/extensions/goal-agents-panel.ts +211 -0
- package/extensions/goal-commands.ts +226 -10
- package/extensions/goal-continuation.ts +119 -21
- package/extensions/goal-heartbeat.ts +560 -35
- package/extensions/goal-loop-auditor-process.ts +126 -4
- package/extensions/goal-loop-auditor.ts +3 -5
- package/extensions/goal-loop-backoff.ts +79 -0
- package/extensions/goal-loop-core.ts +197 -15
- package/extensions/goal-loop-dispatch.ts +3 -2
- package/extensions/goal-loop-display.ts +141 -8
- package/extensions/goal-loop-forever.ts +52 -3
- package/extensions/goal-loop-shield.ts +132 -26
- package/extensions/goal-loop-stats.ts +3 -2
- package/extensions/goal-loop.ts +101 -4
- package/extensions/goal-recovery.ts +8 -2
- package/extensions/goal-settings.ts +61 -14
- package/extensions/goal-state.ts +20 -0
- package/extensions/loops/goal-activation.ts +426 -45
- package/extensions/loops/goal-auditor-hooks.ts +39 -3
- package/extensions/loops/goal-auditor-surface.ts +32 -0
- package/extensions/loops/goal-list-queue.ts +72 -12
- package/extensions/loops/goal-orchestrator.ts +33 -1
- package/extensions/loops/goal-runtime-globals.ts +5 -0
- package/extensions/loops/goal-session.ts +68 -8
- package/extensions/loops/goal-settings-ui.ts +116 -5
- package/extensions/loops/goal-tools.ts +104 -29
- package/extensions/loops/goal-ui.ts +106 -1
- package/extensions/loops/goal.ts +7 -7
- package/extensions/main-model-recovery.ts +10 -0
- package/extensions/multi-model-picker.ts +45 -7
- package/extensions/payload-guard.ts +172 -0
- package/extensions/reviewer.ts +3 -1
- package/extensions/settings-menu.ts +39 -4
- package/package.json +9 -3
- package/prompts/goal-loop-plan-loop.md +31 -0
- package/prompts/goal-loop-plan.md +51 -0
- package/schemas/goal.schema.json +4 -1
- package/scripts/auditor-extension-fixture.mjs +19 -0
- package/scripts/goal-auditor-launch.mjs +19 -1
- package/scripts/goal-auditor-worker.mjs +15 -0
- package/scripts/verify-auditor-extensions-offline.mjs +72 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
// pi-goal-list-loop-audit — v0.35.29
|
|
2
|
+
// goal-agents-panel.ts — issue #15 subagent visibility (design:
|
|
3
|
+
// docs/DESIGN-subagent-visibility.md, scope agreed 2026-08-22).
|
|
4
|
+
//
|
|
5
|
+
// Pure rendering + data assembly for:
|
|
6
|
+
// 1. /glla agents — tracked-subagent snapshot table
|
|
7
|
+
// 2. /glla agents --tail <id> — read-only child transcript tail
|
|
8
|
+
// 3. the widget agents segment (line built here, appended by the widget)
|
|
9
|
+
//
|
|
10
|
+
// Everything here is deterministic given its inputs; fs access happens only
|
|
11
|
+
// in tailChildTranscript through an injected reader so tests stay hermetic.
|
|
12
|
+
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
import { sanitizeDisplayText } from "./goal-loop-core.js";
|
|
15
|
+
|
|
16
|
+
/** v0.35.45 (audit finding): the candidate scan reads a bounded TAIL of each
|
|
17
|
+
* transcript, not the whole file — up to 25 files were previously read in
|
|
18
|
+
* full, synchronously, on the main thread. 256 KiB is far more than any
|
|
19
|
+
* needle window needs (summary prefix / agentType / recordId recur
|
|
20
|
+
* throughout recent entries). */
|
|
21
|
+
export const TRANSCRIPT_SCAN_MAX_BYTES = 256 * 1024;
|
|
22
|
+
|
|
23
|
+
export interface AgentsPanelRow {
|
|
24
|
+
recordId: string;
|
|
25
|
+
agentType?: string;
|
|
26
|
+
summary?: string;
|
|
27
|
+
status: "running" | "hung" | "ended";
|
|
28
|
+
spawnedAt: number;
|
|
29
|
+
lastProgressAt: number;
|
|
30
|
+
toolUses: number;
|
|
31
|
+
outputTokens: number;
|
|
32
|
+
silentMs: number;
|
|
33
|
+
evidence: "record-frozen" | "event-only" | "live";
|
|
34
|
+
action?: "abort-requested" | "unavailable" | "failed";
|
|
35
|
+
endedOk?: boolean;
|
|
36
|
+
endedAt?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Human label: agentType + short summary. */
|
|
40
|
+
function rowLabel(row: AgentsPanelRow): string {
|
|
41
|
+
const summary = (row.summary ?? "").replace(/\s+/g, " ").trim();
|
|
42
|
+
return [row.agentType ?? "subagent", summary ? truncate(summary, 28) : ""].filter(Boolean).join(" · ");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function truncate(text: string, max: number): string {
|
|
46
|
+
return text.length <= max ? text : text.slice(0, Math.max(1, max - 1)) + "…";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function fmtDuration(ms: number): string {
|
|
50
|
+
const totalSec = Math.max(0, Math.round(ms / 1000));
|
|
51
|
+
const min = Math.floor(totalSec / 60);
|
|
52
|
+
const sec = totalSec % 60;
|
|
53
|
+
if (min >= 60) return `${Math.floor(min / 60)}h${String(min % 60).padStart(2, "0")}m`;
|
|
54
|
+
if (min > 0) return `${min}m${String(sec).padStart(2, "0")}s`;
|
|
55
|
+
return `${sec}s`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const PANEL_ROW_CAP = 20;
|
|
59
|
+
|
|
60
|
+
/** Render the /glla agents table. Running/hung first (hung topmost), then
|
|
61
|
+
* ended; capped at PANEL_ROW_CAP rows with an explicit truncation notice. */
|
|
62
|
+
export function renderAgentsPanel(rows: AgentsPanelRow[], now: number, managerAvailable: boolean): string[] {
|
|
63
|
+
if (rows.length === 0) {
|
|
64
|
+
return ["No subagents tracked yet — spawn one via the Agent tool and it appears here.", "(evidence: glla's event probes" + (managerAvailable ? " + pi-subagents manager records" : "") + ")"];
|
|
65
|
+
}
|
|
66
|
+
const rank = (r: AgentsPanelRow): number => (r.status === "hung" ? 0 : r.status === "running" ? 1 : 2);
|
|
67
|
+
const ordered = [...rows].sort((a, b) => rank(a) - rank(b) || a.silentMs - b.silentMs);
|
|
68
|
+
const shown = ordered.slice(0, PANEL_ROW_CAP);
|
|
69
|
+
const lines: string[] = [];
|
|
70
|
+
for (const row of shown) {
|
|
71
|
+
const glyph = row.status === "ended" ? "✓" : row.status === "hung" ? "⚠" : "●";
|
|
72
|
+
const stateWord = row.status === "ended"
|
|
73
|
+
? `ENDED ${row.endedOk === false ? "✗" : "ok"} ${fmtDuration((row.endedAt ?? now) - row.spawnedAt)}`
|
|
74
|
+
: row.action === "abort-requested"
|
|
75
|
+
? `ABORTING ${fmtDuration(now - row.spawnedAt)}`
|
|
76
|
+
: `${row.status === "hung" ? "HUNG?" : "RUNNING"} ${fmtDuration(now - row.spawnedAt)}`;
|
|
77
|
+
lines.push(`${glyph} ${rowLabel(row)} ${stateWord}`);
|
|
78
|
+
lines.push(` tools ${row.toolUses} · out ${row.outputTokens >= 1000 ? `${(row.outputTokens / 1000).toFixed(1)}k` : row.outputTokens} · silent ${fmtDuration(row.silentMs)}${row.evidence !== "live" ? ` (${row.evidence})` : ""}`);
|
|
79
|
+
if (row.action === "abort-requested") {
|
|
80
|
+
lines.push(" └ child-specific abort requested; partial output remains available while it settles");
|
|
81
|
+
} else if (row.action === "unavailable") {
|
|
82
|
+
lines.push(" └ no safe child-abort capability; inspect the child and interrupt explicitly if needed");
|
|
83
|
+
} else if (row.action === "failed") {
|
|
84
|
+
lines.push(" └ child-abort request failed; inspect the child and interrupt explicitly if needed");
|
|
85
|
+
} else if (row.status === "hung") {
|
|
86
|
+
lines.push(" └ check the Agents panel: a child whose counters stopped moving is hung, not thinking");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (ordered.length > shown.length) {
|
|
90
|
+
lines.push(`… ${ordered.length - shown.length} more (oldest ended trimmed — cap ${PANEL_ROW_CAP})`);
|
|
91
|
+
}
|
|
92
|
+
return lines;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The ambient widget segment: hidden at zero tracked children by the caller. */
|
|
96
|
+
export function renderAgentsWidgetLine(rows: AgentsPanelRow[]): string | undefined {
|
|
97
|
+
const active = rows.filter((r) => r.status !== "ended");
|
|
98
|
+
if (active.length === 0) return undefined;
|
|
99
|
+
const busiest = [...active].sort((a, b) => b.silentMs - a.silentMs)[0]!;
|
|
100
|
+
const hung = busiest.action === "abort-requested"
|
|
101
|
+
? " ⚠ aborting"
|
|
102
|
+
: busiest.status === "hung"
|
|
103
|
+
? " ⚠"
|
|
104
|
+
: "";
|
|
105
|
+
return `● ${active.length} agent${active.length === 1 ? "" : "s"} · ${(busiest.agentType ?? "subagent")} silent ${fmtDuration(busiest.silentMs)}${hung}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface TranscriptTailResult {
|
|
109
|
+
ok: boolean;
|
|
110
|
+
lines: string[];
|
|
111
|
+
/** What was searched / read — surfaced to the user on failure. */
|
|
112
|
+
detail: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Read-only tail of a child's session transcript. Candidate selection: all
|
|
116
|
+
* .jsonl files in sessionsDir whose content mentions the child's summary or
|
|
117
|
+
* agent type; most recently modified wins. NEVER resumes or attaches.
|
|
118
|
+
* readFile is injected for tests; production passes fs.readFileSync-wrapped. */
|
|
119
|
+
export function tailChildTranscript(
|
|
120
|
+
sessionsDir: string,
|
|
121
|
+
row: { recordId: string; agentType?: string; summary?: string },
|
|
122
|
+
opts: {
|
|
123
|
+
lines?: number;
|
|
124
|
+
readFile?: (file: string, maxBytes?: number) => Buffer;
|
|
125
|
+
listDir?: (dir: string) => string[];
|
|
126
|
+
statMtime?: (file: string) => number;
|
|
127
|
+
now?: number;
|
|
128
|
+
} = {},
|
|
129
|
+
): TranscriptTailResult {
|
|
130
|
+
const wantLines = Math.max(1, Math.min(200, opts.lines ?? 20));
|
|
131
|
+
const readFile = opts.readFile ?? (() => { throw new Error("no reader"); });
|
|
132
|
+
// v0.35.45: readers may honor maxBytes (production does — partial tail
|
|
133
|
+
// read); injected test readers ignore it and return the whole buffer.
|
|
134
|
+
const scanRead = (file: string): Buffer => readFile(file, TRANSCRIPT_SCAN_MAX_BYTES);
|
|
135
|
+
const listDir = opts.listDir ?? (() => []);
|
|
136
|
+
const statMtime = opts.statMtime ?? (() => 0);
|
|
137
|
+
let entries: string[];
|
|
138
|
+
try {
|
|
139
|
+
entries = listDir(sessionsDir);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
return { ok: false, lines: [], detail: `cannot list ${sessionsDir}: ${error instanceof Error ? error.message.slice(0, 120) : String(error)}` };
|
|
142
|
+
}
|
|
143
|
+
const needles = [row.summary?.slice(0, 48), row.agentType, row.recordId]
|
|
144
|
+
.filter((n): n is string => typeof n === "string" && n.trim().length >= 6)
|
|
145
|
+
.map((n) => n.toLowerCase());
|
|
146
|
+
const candidates = entries
|
|
147
|
+
.filter((f) => f.endsWith(".jsonl"))
|
|
148
|
+
.map((f) => path.join(sessionsDir, f))
|
|
149
|
+
.map((f) => ({ f, mtime: statMtime(f) }))
|
|
150
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
151
|
+
let matched: string | undefined;
|
|
152
|
+
for (const candidate of candidates.slice(0, 25)) {
|
|
153
|
+
try {
|
|
154
|
+
const content = scanRead(candidate.f).toString("utf8").toLowerCase();
|
|
155
|
+
if (needles.some((needle) => content.includes(needle))) { matched = candidate.f; break; }
|
|
156
|
+
} catch { /* unreadable file — skip */ }
|
|
157
|
+
}
|
|
158
|
+
if (!matched) {
|
|
159
|
+
return {
|
|
160
|
+
ok: false,
|
|
161
|
+
lines: [],
|
|
162
|
+
detail: `no session file in ${sessionsDir} matches this child (searched ${candidates.length} transcripts for: ${needles.map((n) => `"${truncate(n, 24)}"`).join(", ") || "any needle"}) — the child may not persist a session, or it lives under another working directory`,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
const raw = readFile(matched).toString("utf8").split("\n").filter(Boolean);
|
|
167
|
+
const formatted = raw.map(formatTranscriptEntry).filter(Boolean) as string[];
|
|
168
|
+
return { ok: true, lines: formatted.slice(-wantLines), detail: `${matched} (last ${Math.min(wantLines, formatted.length)} of ${formatted.length})` };
|
|
169
|
+
} catch (error) {
|
|
170
|
+
return { ok: false, lines: [], detail: `cannot read ${matched}: ${error instanceof Error ? error.message.slice(0, 120) : String(error)}` };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Tolerant pi-session JSONL → `[role] text` line. Unparseable lines are
|
|
175
|
+
* truncated raw so forensic value survives shape drift. v0.35.45: output is
|
|
176
|
+
* ANSI/control-char sanitized — a hostile child transcript must not be able
|
|
177
|
+
* to emit terminal escape sequences through ctx.ui.notify. */
|
|
178
|
+
export function formatTranscriptEntry(line: string): string | undefined {
|
|
179
|
+
const trimmed = line.trim();
|
|
180
|
+
if (!trimmed) return undefined;
|
|
181
|
+
try {
|
|
182
|
+
const entry = JSON.parse(trimmed) as {
|
|
183
|
+
type?: string;
|
|
184
|
+
role?: string;
|
|
185
|
+
message?: { role?: string; content?: unknown };
|
|
186
|
+
content?: unknown;
|
|
187
|
+
};
|
|
188
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return `[raw] ${truncate(sanitizeDisplayText(trimmed), 120)}`;
|
|
189
|
+
const role = entry.message?.role ?? entry.role ?? entry.type ?? "?";
|
|
190
|
+
const content = entry.message?.content ?? entry.content;
|
|
191
|
+
const text = extractText(content);
|
|
192
|
+
if (!text) return undefined;
|
|
193
|
+
return `[${role}] ${truncate(sanitizeDisplayText(text).replace(/\s+/g, " ").trim(), 160)}`;
|
|
194
|
+
} catch {
|
|
195
|
+
return `[raw] ${truncate(sanitizeDisplayText(trimmed), 120)}`;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function extractText(content: unknown): string {
|
|
200
|
+
if (typeof content === "string") return content;
|
|
201
|
+
if (Array.isArray(content)) {
|
|
202
|
+
return content
|
|
203
|
+
.map((block) => typeof block === "string"
|
|
204
|
+
? block
|
|
205
|
+
: typeof block === "object" && block !== null && typeof (block as { text?: unknown }).text === "string"
|
|
206
|
+
? (block as { text: string }).text
|
|
207
|
+
: "")
|
|
208
|
+
.join(" ");
|
|
209
|
+
}
|
|
210
|
+
return "";
|
|
211
|
+
}
|
|
@@ -8,6 +8,14 @@
|
|
|
8
8
|
import * as fs from "node:fs";
|
|
9
9
|
import * as os from "node:os";
|
|
10
10
|
import * as path from "node:path";
|
|
11
|
+
import { renderAgentsPanel, tailChildTranscript } from "./goal-agents-panel.js";
|
|
12
|
+
|
|
13
|
+
/** Child pi sessions live under the shared session store, munged by cwd
|
|
14
|
+
* (verified layout: ~/.pi/agent/sessions/--home-user-proj--/*.jsonl). */
|
|
15
|
+
function childSessionsDir(cwd: string): string {
|
|
16
|
+
const munged = "--" + cwd.replaceAll("/", "-") + "--";
|
|
17
|
+
return path.join(os.homedir(), ".pi", "agent", "sessions", munged);
|
|
18
|
+
}
|
|
11
19
|
import type { ExtensionContext, ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
20
|
import { state, replaceState } from "./goal-state.js";
|
|
13
21
|
import {
|
|
@@ -16,9 +24,11 @@ import {
|
|
|
16
24
|
formatListDepth, goalArgsNeedDrafting, ledgerPath, newGoalId, nowIso, parseListImport, parseListItemDeclaration,
|
|
17
25
|
assignQueueOrder, compareQueueItems, readAuditLog, readQueueFromDisk, routeGoalArgs, routeListText, sanitizeDisplayText, sanitizeProviderAuditReport, statusLabel,
|
|
18
26
|
writeQueueItemFile, type ModeCommand, type State, LIST_MUTATING_SUBCOMMANDS, SETTINGS_MUTATING_ACTIONS,
|
|
27
|
+
clearLoadHold, stateRootPending,
|
|
19
28
|
} from "./goal-loop-core.js";
|
|
20
29
|
import { clearDispatchRecord, dispatchRecordExists } from "./goal-loop-dispatch.js";
|
|
21
30
|
import type { AuditDisplayProgress } from "./goal-loop-display.js";
|
|
31
|
+
import { fmtElapsed } from "./goal-loop-display.js";
|
|
22
32
|
import { AUDIT_FINDINGS_REL, HELD_ON_RESTORE, LOOP_AUDIT_MARKER, listAuditCollectTarget, projectAuditTarget } from "./goal-loop-forever.js";
|
|
23
33
|
import { ProjectRollup, discoverGllaProjects, filterPremature, formatRollupJson, formatRollupTable, parseLedgerEntries, rollupProject } from "./goal-loop-stats.js";
|
|
24
34
|
import { OVERRIDABLE_AGENT_TYPES, resolveEffectiveSubagentModel } from "./goal-loop-subagents.js";
|
|
@@ -30,6 +40,7 @@ import { cmdLoop, clearLoopTimer, finishLoopGit, isLoopActive, scheduleLoopTick
|
|
|
30
40
|
import { chooseObjectiveConflict, liveObjectives } from "./goal-objective-conflict.js";
|
|
31
41
|
import { formatGllaVersion } from "./glla-version.js";
|
|
32
42
|
import { cancelDetachedGoalCompletionAuditor } from "./goal-loop-auditor-process.js";
|
|
43
|
+
import { releaseAuditorSurface } from "./loops/goal-auditor-surface.js";
|
|
33
44
|
|
|
34
45
|
export interface CommandFlags {
|
|
35
46
|
get draftingTarget(): "goal" | "list" | "loop" | null; set draftingTarget(v: "goal" | "list" | "loop" | null);
|
|
@@ -55,7 +66,7 @@ export interface CommandDeps {
|
|
|
55
66
|
setGoal: (goal: Goal, ctx: ExtensionContext, via?: string) => boolean;
|
|
56
67
|
archiveCurrentGoal: (ctx: ExtensionContext, status: Status, stopReason?: string) => boolean;
|
|
57
68
|
healGoalPolicy: (ctx: ExtensionContext) => boolean;
|
|
58
|
-
startDrafting: (ctx: ExtensionContext, target: "goal" | "list" | "loop", seed?: string) => Promise<boolean>;
|
|
69
|
+
startDrafting: (ctx: ExtensionContext, target: "goal" | "list" | "loop", seed?: string, depth?: "normal" | "plan") => Promise<boolean>;
|
|
59
70
|
warnIfStaleAtEntry: (ctx: ExtensionContext, what: string) => boolean;
|
|
60
71
|
queuePendingListOperation: (ctx: ExtensionContext, args: string) => boolean;
|
|
61
72
|
freshCtx: () => ExtensionContext | null;
|
|
@@ -87,9 +98,13 @@ export interface CommandDeps {
|
|
|
87
98
|
goalNoun: () => string;
|
|
88
99
|
displaySlice: (s: string, max: number) => string;
|
|
89
100
|
shortObj: (s: string) => string;
|
|
101
|
+
/** v0.35.29 (issue #15): read-only tracked-subagent snapshot for /glla agents.
|
|
102
|
+
* Injected because goal-heartbeat must not be imported from here (cycle). */
|
|
103
|
+
agentsSnapshot: () => { agents: import("./goal-agents-panel.js").AgentsPanelRow[]; managerAvailable: boolean };
|
|
90
104
|
}
|
|
91
105
|
|
|
92
106
|
let deps: CommandDeps;
|
|
107
|
+
let agentsSnapshot: CommandDeps["agentsSnapshot"];
|
|
93
108
|
let flags: CommandFlags;
|
|
94
109
|
let listQueue: CommandDeps["listQueue"], notifyExternal: CommandDeps["notifyExternal"], persistState: CommandDeps["persistState"], updateGoal: CommandDeps["updateGoal"], setGoal: CommandDeps["setGoal"],
|
|
95
110
|
archiveCurrentGoal: CommandDeps["archiveCurrentGoal"], healGoalPolicy: CommandDeps["healGoalPolicy"], startDrafting: CommandDeps["startDrafting"], warnIfStaleAtEntry: CommandDeps["warnIfStaleAtEntry"], queuePendingListOperation: CommandDeps["queuePendingListOperation"], freshCtx: CommandDeps["freshCtx"],
|
|
@@ -108,6 +123,7 @@ export function createGoalCommands(d: CommandDeps): void {
|
|
|
108
123
|
releaseInitialSessionLoadBarrier = d.releaseInitialSessionLoadBarrier; resolveCarryover = d.resolveCarryover; safeSteerUser = d.safeSteerUser; scheduleContinuation = d.scheduleContinuation; scheduleSessionTimeout = d.scheduleSessionTimeout;
|
|
109
124
|
createGoal = d.createGoal; fireReviewer = d.fireReviewer; openSettingsUI = d.openSettingsUI; manuallyResumeMainModelRecovery = d.manuallyResumeMainModelRecovery; activeGoalCommand = d.activeGoalCommand;
|
|
110
125
|
activeGoalStatusCommand = d.activeGoalStatusCommand; activeGoalSurfaceCommand = d.activeGoalSurfaceCommand; goalNoun = d.goalNoun; displaySlice = d.displaySlice; shortObj = d.shortObj;
|
|
126
|
+
agentsSnapshot = d.agentsSnapshot;
|
|
111
127
|
}
|
|
112
128
|
|
|
113
129
|
/* Moved bodies (bands b1 + b3 from goal.ts). */
|
|
@@ -188,6 +204,18 @@ async function cmdGoal(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
188
204
|
}
|
|
189
205
|
return cmdSet(route.rest, ctx, true);
|
|
190
206
|
}
|
|
207
|
+
// v0.35.33: /goal plan [seed] — the EXTENDED DRAFT. Same trust machinery
|
|
208
|
+
// as a regular draft (Confirm card gates activation), deeper process:
|
|
209
|
+
// research-first, multi-round interview, structured expanded objective.
|
|
210
|
+
// For greenfield/megaplan work where the standard 5–7-question interview
|
|
211
|
+
// is too shallow. Regular /goal stays the fast path.
|
|
212
|
+
if (route.name === "plan") {
|
|
213
|
+
// Same entry gate as cmdSet: a stale MAIN cannot deliver the seed; do
|
|
214
|
+
// not leave an orphaned drafting gate behind a doomed process.
|
|
215
|
+
if (warnIfStaleAtEntry(ctx, "/goal")) return;
|
|
216
|
+
await startDrafting(ctx, "goal", route.rest || undefined, "plan");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
191
219
|
}
|
|
192
220
|
return cmdSet(route.kind === "set" ? route.text : "", ctx);
|
|
193
221
|
}
|
|
@@ -331,9 +359,28 @@ async function cmdPause(ctx: ExtensionContext): Promise<void> {
|
|
|
331
359
|
|
|
332
360
|
async function cmdResume(ctx: ExtensionContext): Promise<void> {
|
|
333
361
|
releaseInitialSessionLoadBarrier();
|
|
362
|
+
// v0.35.23 (note.md Next #2): an explicit resume is exactly the decision
|
|
363
|
+
// the load hold waits for — release it before re-arming automation, or
|
|
364
|
+
// the scheduleContinuation below would be a frozen no-op.
|
|
365
|
+
if (clearLoadHold(state)) {
|
|
366
|
+
persistState(ctx);
|
|
367
|
+
appendLedger(ctx.cwd, "load_hold_released", { via: "goal-resume" });
|
|
368
|
+
ctx.ui.notify("Load hold released — automation is live again.", "info");
|
|
369
|
+
}
|
|
334
370
|
const resumeCommand = activeGoalCommand("resume");
|
|
335
|
-
|
|
336
|
-
|
|
371
|
+
// Main-model recovery returns before the normal paused-goal admission path.
|
|
372
|
+
// Probe the same stale/foreign boundary first so an admitted recovery resume
|
|
373
|
+
// can release the auditor surface, while a rejected one falls through to the
|
|
374
|
+
// existing stale-resume recovery marker without exposing old context.
|
|
375
|
+
const recoveryStaleEntry = state.mainModelRecovery
|
|
376
|
+
? warnIfStaleAtEntry(ctx, resumeCommand)
|
|
377
|
+
: false;
|
|
378
|
+
if (!recoveryStaleEntry && manuallyResumeMainModelRecovery(ctx)) {
|
|
379
|
+
releaseAuditorSurface();
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (!recoveryStaleEntry && (state.mainModelRecovery?.retryAt || state.mainModelRecovery?.pendingModelSwitch)) {
|
|
383
|
+
releaseAuditorSurface();
|
|
337
384
|
clearMainModelRecoveryTimer();
|
|
338
385
|
flags.continuationDispatchStoodDown = false;
|
|
339
386
|
// v0.34.92: the chat-prompt re-arm was removed; recovery is timer-driven.
|
|
@@ -341,7 +388,8 @@ async function cmdResume(ctx: ExtensionContext): Promise<void> {
|
|
|
341
388
|
void probeMainModelRecovery(ctx);
|
|
342
389
|
return;
|
|
343
390
|
}
|
|
344
|
-
if (state.mainModelRecovery?.primaryProbeAt || state.mainModelRecovery?.primaryProbeInFlight) {
|
|
391
|
+
if (!recoveryStaleEntry && (state.mainModelRecovery?.primaryProbeAt || state.mainModelRecovery?.primaryProbeInFlight)) {
|
|
392
|
+
releaseAuditorSurface();
|
|
345
393
|
clearMainModelRecoveryTimer();
|
|
346
394
|
flags.continuationDispatchStoodDown = false;
|
|
347
395
|
ctx.ui.notify("Probing the preferred primary now — the current fallback remains available if the primary is still unhealthy.", "info");
|
|
@@ -357,6 +405,21 @@ async function cmdResume(ctx: ExtensionContext): Promise<void> {
|
|
|
357
405
|
ctx.ui.notify("A loop is active — one active thing at a time. /loop stop it first, then resume the goal.", "warning");
|
|
358
406
|
return;
|
|
359
407
|
}
|
|
408
|
+
// An ACTIVE-but-idle goal still needs the entry admission probe: a stale
|
|
409
|
+
// command must not release the auditor surface before its schedule is
|
|
410
|
+
// rejected by the stale/foreign session boundary.
|
|
411
|
+
const staleEntry = warnIfStaleAtEntry(ctx, resumeCommand);
|
|
412
|
+
if (staleEntry) return;
|
|
413
|
+
releaseAuditorSurface();
|
|
414
|
+
if (state.goal.repairTarget?.replanPromptedAt) {
|
|
415
|
+
const target = state.goal.repairTarget;
|
|
416
|
+
updateGoal({ repairTarget: { ...target, replanPromptedAt: undefined } }, ctx);
|
|
417
|
+
appendLedger(ctx.cwd, "faulty_objective_replan_turn_reset", {
|
|
418
|
+
goalId: state.goal.id,
|
|
419
|
+
targetId: target.id,
|
|
420
|
+
via: "manual-resume",
|
|
421
|
+
});
|
|
422
|
+
}
|
|
360
423
|
appendLedger(ctx.cwd, "resume_rekick", { goalId: state.goal.id, policy: state.goal.policy, via: resumeCommand });
|
|
361
424
|
if (state.goal.interruptedAt) updateGoal({ interruptedAt: undefined, interruptedReason: undefined }, ctx); // v0.34.7: same marker law here
|
|
362
425
|
ctx.ui.notify(
|
|
@@ -381,6 +444,7 @@ async function cmdResume(ctx: ExtensionContext): Promise<void> {
|
|
|
381
444
|
}
|
|
382
445
|
const staleEntry = warnIfStaleAtEntry(ctx, resumeCommand);
|
|
383
446
|
if (staleEntry) return;
|
|
447
|
+
releaseAuditorSurface();
|
|
384
448
|
markCompletionAuditRecoveryPending(ctx, "manual-resume");
|
|
385
449
|
flags.completionAuditRecoveryArmed = true;
|
|
386
450
|
ctx.ui.notify("Resuming the stored completion claim — starting a detached auditor (no agent turn needed).", "info");
|
|
@@ -437,8 +501,9 @@ async function cmdResume(ctx: ExtensionContext): Promise<void> {
|
|
|
437
501
|
// manual resume exactly as by an automatic one. (staleEntry still re-marks
|
|
438
502
|
// below — a resume inside a stale session is a NEW interrupt.)
|
|
439
503
|
const storedCompletion = state.goal.pendingCompletion;
|
|
440
|
-
updateGoal({ status: "active", pauseReason: undefined, pauseSuggestedAction: undefined, pauseKind: undefined, pauseOptions: undefined, pauseRecommended: undefined, pauseResumeAt: undefined, interruptedAt: undefined, interruptedReason: undefined, ...(staleEntry ? { interruptedAt: nowIso(), interruptedReason: "resumed in a stale session" } : {}), ...(usage ? { usage } : {}) }, ctx);
|
|
504
|
+
updateGoal({ status: "active", pauseReason: undefined, pauseSuggestedAction: undefined, pauseKind: undefined, pauseOptions: undefined, pauseRecommended: undefined, pauseResumeAt: undefined, interruptedAt: undefined, interruptedReason: undefined, autoResumedAt: undefined, autoResumedEvent: undefined, ...(staleEntry ? { interruptedAt: nowIso(), interruptedReason: "resumed in a stale session" } : {}), ...(usage ? { usage } : {}) }, ctx);
|
|
441
505
|
if (staleEntry) return;
|
|
506
|
+
releaseAuditorSurface();
|
|
442
507
|
// A stored completion claim is a direct-audit resume, not an agent turn.
|
|
443
508
|
// Keeping the claim while merely scheduling a continuation left manual
|
|
444
509
|
// pause/resume with an ACTIVE goal that no timer would ever consume.
|
|
@@ -462,6 +527,10 @@ async function cmdResume(ctx: ExtensionContext): Promise<void> {
|
|
|
462
527
|
}
|
|
463
528
|
|
|
464
529
|
async function cmdCancel(ctx: ExtensionContext): Promise<void> {
|
|
530
|
+
if (stateRootPending()) {
|
|
531
|
+
ctx.ui.notify("Cancel deferred — the selected sessionDir is not resolved yet, so no live state was changed. Reload the host session and retry.", "warning");
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
465
534
|
if (!state.goal) {
|
|
466
535
|
// v0.28.14: users reach for /goal cancel to kill a LOOP (no goal
|
|
467
536
|
// active) — point at the right verb instead of doing nothing silently.
|
|
@@ -1074,7 +1143,7 @@ async function cmdList(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
1074
1143
|
return;
|
|
1075
1144
|
}
|
|
1076
1145
|
|
|
1077
|
-
if (sub === "pause") {
|
|
1146
|
+
if (sub === "pause" && !rest) {
|
|
1078
1147
|
if (!state.goal || state.goal.policy !== "list") {
|
|
1079
1148
|
ctx.ui.notify(
|
|
1080
1149
|
state.goal ? "The active work is a standalone goal — /goal pause to pause it." : "No active list item to pause. /list show to see the list.",
|
|
@@ -1086,7 +1155,7 @@ async function cmdList(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
1086
1155
|
return;
|
|
1087
1156
|
}
|
|
1088
1157
|
|
|
1089
|
-
if (sub === "resume") {
|
|
1158
|
+
if (sub === "resume" && !rest) {
|
|
1090
1159
|
// Resume the list's head. The head activates AS the active goal, so this
|
|
1091
1160
|
// is the same motion as /goal resume — named for the surface the user is
|
|
1092
1161
|
// looking at (v0.22.7: "we would just unpause, and that is next").
|
|
@@ -1219,7 +1288,22 @@ async function cmdList(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
1219
1288
|
return;
|
|
1220
1289
|
}
|
|
1221
1290
|
|
|
1222
|
-
|
|
1291
|
+
// v0.35.33: /list plan [seed] — the extended draft for LIST items: deep
|
|
1292
|
+
// research + multi-round interview, then the agent proposes items[] through
|
|
1293
|
+
// the SAME one-Confirm batch path (never per-item activation). Intended for
|
|
1294
|
+
// greenfield/megaplan decomposition where the standard interview is too
|
|
1295
|
+
// shallow. Plain /list add keeps its fast behavior unchanged.
|
|
1296
|
+
if (sub === "plan") {
|
|
1297
|
+
// staleEntry was already gated by LIST_MUTATING_SUBCOMMANDS above.
|
|
1298
|
+
await startDrafting(ctx, "list", rest || undefined, "plan");
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
if (sub === "clear" && !rest) {
|
|
1303
|
+
if (stateRootPending()) {
|
|
1304
|
+
ctx.ui.notify("List clear deferred — the selected sessionDir is not resolved yet, so no live state was changed. Reload the host session and retry.", "warning");
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1223
1307
|
// v0.34.61: delete the sidecars of every removed item BEFORE clearing
|
|
1224
1308
|
// state. The /list disk-recovery fallback scans .pi-glla/goals/*.queue.json
|
|
1225
1309
|
// when memQueue is empty; without this, a /list clear followed by a
|
|
@@ -1237,7 +1321,11 @@ async function cmdList(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
1237
1321
|
// v0.24.1: ONE verb for "stop this whole list" — aborts the active item
|
|
1238
1322
|
// when it's list-sourced AND drops the waiting items. Before this the user
|
|
1239
1323
|
// had to know to combine /goal cancel + /list clear.
|
|
1240
|
-
if (sub === "cancel") {
|
|
1324
|
+
if (sub === "cancel" && !rest) {
|
|
1325
|
+
if (stateRootPending()) {
|
|
1326
|
+
ctx.ui.notify("List cancel deferred — the selected sessionDir is not resolved yet, so no live state was changed. Reload the host session and retry.", "warning");
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1241
1329
|
const waiting = listQueue().length;
|
|
1242
1330
|
const activeIsListItem = state.goal?.policy === "list" && (state.goal.status === "active" || state.goal.status === "paused" || state.goal.status === "auditing");
|
|
1243
1331
|
if (waiting === 0 && !activeIsListItem) {
|
|
@@ -1297,6 +1385,10 @@ async function cmdList(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
1297
1385
|
}
|
|
1298
1386
|
if (!activateNextListItem(ctx, n, { explicit: true })) {
|
|
1299
1387
|
ctx.ui.notify(listQueue().length === 0 ? "List is empty — nothing to activate." : `No item #${n} (list has ${listQueue().length}).`, "info");
|
|
1388
|
+
} else {
|
|
1389
|
+
// Explicit list activation is also continuation consent, even when the
|
|
1390
|
+
// activated item is a fresh queue head rather than the parked goal.
|
|
1391
|
+
releaseAuditorSurface();
|
|
1300
1392
|
}
|
|
1301
1393
|
return;
|
|
1302
1394
|
}
|
|
@@ -1737,6 +1829,10 @@ function cmdSwitchlog(args: string, ctx: ExtensionContext): void {
|
|
|
1737
1829
|
*/
|
|
1738
1830
|
async function cmdGllaWipe(ctx: ExtensionContext, entryChecked = false): Promise<void> {
|
|
1739
1831
|
if (!entryChecked && warnIfStaleAtEntry(ctx, "/glla wipe")) return;
|
|
1832
|
+
if (stateRootPending()) {
|
|
1833
|
+
ctx.ui.notify("Wipe deferred — the selected sessionDir is not resolved yet, so no live state was changed. Reload the host session and retry.", "warning");
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1740
1836
|
const g = state.goal;
|
|
1741
1837
|
const live = g && (g.status === "active" || g.status === "paused" || g.status === "auditing");
|
|
1742
1838
|
const memoryQueue = listQueue();
|
|
@@ -1784,8 +1880,11 @@ async function cmdGllaWipe(ctx: ExtensionContext, entryChecked = false): Promise
|
|
|
1784
1880
|
// Do not abort here: wipe still has to clear queue sidecars and loop
|
|
1785
1881
|
// state. The old early abort made a second /glla wipe appear necessary.
|
|
1786
1882
|
if (!archiveCurrentGoal(ctx, "aborted", "user wipe (/glla wipe)")) return;
|
|
1883
|
+
// v0.35.30: the archive's closeArchivedSlot just recorded lastOutcome;
|
|
1884
|
+
// a wipe means CLEAN SLATE — the ✓/▪ line must not survive it.
|
|
1885
|
+
replaceState({ ...state, lastOutcome: undefined });
|
|
1787
1886
|
} else if (g) {
|
|
1788
|
-
replaceState({ ...state, goal: null });
|
|
1887
|
+
replaceState({ ...state, goal: null, lastOutcome: undefined });
|
|
1789
1888
|
}
|
|
1790
1889
|
// Clear provider-recovery and continuation artifacts only after a live-goal
|
|
1791
1890
|
// archive succeeds. An archive failure must leave all resumable work intact.
|
|
@@ -1837,12 +1936,56 @@ async function cmdGllaWipe(ctx: ExtensionContext, entryChecked = false): Promise
|
|
|
1837
1936
|
* today) → the v0.28.23 decision-picker pattern. Verbs whose semantics
|
|
1838
1937
|
* genuinely differ per type (tweak/finish/next/decide/refine) stay typed.
|
|
1839
1938
|
*/
|
|
1939
|
+
/**
|
|
1940
|
+
* v0.35.15: `/glla pause` — freeze the SUPERVISOR, not the work.
|
|
1941
|
+
*
|
|
1942
|
+
* Broad by design (user-confirmed 2026-08-21): every automatic side-effect
|
|
1943
|
+
* stops — heartbeat re-arms/stale probes/zombie cleanup, recovery probes,
|
|
1944
|
+
* auto-resume, auto-continuation dispatch, loop ticks, and the proactive
|
|
1945
|
+
* auditor quiet-phase notification. The active goal/list item/loop state is
|
|
1946
|
+
* untouched and any detached worker keeps running; only the automatic
|
|
1947
|
+
* machinery around it freezes. Manual user commands still work. Persisted
|
|
1948
|
+
* via `supervisorPausedAt`, so a session restart cannot silently re-arm
|
|
1949
|
+
* machinery the user explicitly stopped. Pausing while a recovery claim is
|
|
1950
|
+
* parked keeps the claim durable on disk — nothing is discarded.
|
|
1951
|
+
*/
|
|
1952
|
+
async function cmdGllaPause(ctx: ExtensionContext): Promise<void> {
|
|
1953
|
+
if (warnIfStaleAtEntry(ctx, "/glla pause")) return;
|
|
1954
|
+
releaseInitialSessionLoadBarrier();
|
|
1955
|
+
const already = typeof state.supervisorPausedAt === "number";
|
|
1956
|
+
const pausedAtMs = Date.now();
|
|
1957
|
+
replaceState({ ...state, supervisorPausedAt: pausedAtMs });
|
|
1958
|
+
persistState(ctx);
|
|
1959
|
+
appendLedger(ctx.cwd, "supervisor_pause", already ? { repeat: true } : {});
|
|
1960
|
+
// An armed continuation/recovery timer may already be scheduled; every
|
|
1961
|
+
// dispatch point checks supervisorPaused() so stale timers become no-ops.
|
|
1962
|
+
ctx.ui.notify(already
|
|
1963
|
+
? "Supervisor is already paused — all automatic machinery stays frozen (active work untouched). /glla resume to unfreeze."
|
|
1964
|
+
: "Supervisor PAUSED — heartbeat re-arms, recovery probes, auto-resume, continuation dispatch, loop ticks, and auditor quiet notifies are frozen. The active goal/list item/loop and detached workers keep running. /glla resume to unfreeze.",
|
|
1965
|
+
"warning");
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1840
1968
|
async function cmdGllaResume(ctx: ExtensionContext): Promise<void> {
|
|
1841
1969
|
// v0.29.12: a zombie instance (handle dead after session replacement)
|
|
1842
1970
|
// used to answer "Nothing to resume" — the resume path must name the
|
|
1843
1971
|
// real recovery (/reload rebuilds extensions in place), not mislead.
|
|
1844
1972
|
if (warnIfStaleAtEntry(ctx, "/glla resume")) return;
|
|
1973
|
+
// /glla resume is the broad resume surface and therefore carries the same
|
|
1974
|
+
// consent semantics as /goal resume or /list resume.
|
|
1975
|
+
releaseAuditorSurface();
|
|
1845
1976
|
releaseInitialSessionLoadBarrier();
|
|
1977
|
+
// v0.35.15: clearing a supervisor pause is resume's first job — the flag
|
|
1978
|
+
// outlives sessions, so an explicit /glla resume must always unfreeze the
|
|
1979
|
+
// machinery even when there is nothing else to resume afterwards.
|
|
1980
|
+
let clearedSupervisorPause = false;
|
|
1981
|
+
if (typeof state.supervisorPausedAt === "number") {
|
|
1982
|
+
const frozenMs = Date.now() - state.supervisorPausedAt;
|
|
1983
|
+
delete (state as { supervisorPausedAt?: number }).supervisorPausedAt;
|
|
1984
|
+
persistState(ctx);
|
|
1985
|
+
clearedSupervisorPause = true;
|
|
1986
|
+
appendLedger(ctx.cwd, "supervisor_resume", { frozenMs });
|
|
1987
|
+
ctx.ui.notify(`Supervisor RESUMED after ${fmtElapsed(frozenMs)} — heartbeat re-arms, recovery probes, auto-resume, continuation dispatch, and auditor quiet notifies are live again.`, "info");
|
|
1988
|
+
}
|
|
1846
1989
|
if (manuallyResumeMainModelRecovery(ctx)) return;
|
|
1847
1990
|
if (state.mainModelRecovery?.retryAt || state.mainModelRecovery?.pendingModelSwitch) {
|
|
1848
1991
|
clearMainModelRecoveryTimer();
|
|
@@ -1929,6 +2072,11 @@ async function cmdGllaResume(ctx: ExtensionContext): Promise<void> {
|
|
|
1929
2072
|
scheduleLoopTick(ctx);
|
|
1930
2073
|
return;
|
|
1931
2074
|
}
|
|
2075
|
+
if (clearedSupervisorPause) {
|
|
2076
|
+
// The pause was the only thing being resumed — do not follow it with a
|
|
2077
|
+
// misleading "Nothing to resume".
|
|
2078
|
+
return;
|
|
2079
|
+
}
|
|
1932
2080
|
ctx.ui.notify("Nothing to resume — no paused goal/list-item, no held loop. /goal, /list, or /loop to start something.", "info");
|
|
1933
2081
|
}
|
|
1934
2082
|
|
|
@@ -1945,6 +2093,10 @@ async function cmdGllaResume(ctx: ExtensionContext): Promise<void> {
|
|
|
1945
2093
|
* queue after aborting only its current item.
|
|
1946
2094
|
*/
|
|
1947
2095
|
async function cmdGllaCancel(ctx: ExtensionContext): Promise<void> {
|
|
2096
|
+
if (stateRootPending()) {
|
|
2097
|
+
ctx.ui.notify("Cancel deferred — the selected sessionDir is not resolved yet, so no live state was changed. Reload the host session and retry.", "warning");
|
|
2098
|
+
return;
|
|
2099
|
+
}
|
|
1948
2100
|
// A running loop is itself the active objective. Stop it before looking at
|
|
1949
2101
|
// an unrelated waiting list; otherwise `/glla cancel` can silently drop the
|
|
1950
2102
|
// queue while leaving the loop running.
|
|
@@ -2013,6 +2165,55 @@ function cmdGllaVersion(ctx: ExtensionContext): void {
|
|
|
2013
2165
|
ctx.ui.notify(formatGllaVersion(), "info");
|
|
2014
2166
|
}
|
|
2015
2167
|
|
|
2168
|
+
// v0.35.29 (issue #15): tracked-subagent visibility — snapshot panel plus a
|
|
2169
|
+
// read-only child transcript tail (design: docs/DESIGN-subagent-visibility.md).
|
|
2170
|
+
function cmdAgents(args: string, ctx: ExtensionContext): void {
|
|
2171
|
+
const { agents, managerAvailable } = agentsSnapshot();
|
|
2172
|
+
const tailMatch = args.match(/--tail\s+(\S+)/);
|
|
2173
|
+
if (tailMatch) {
|
|
2174
|
+
const id = tailMatch[1]!;
|
|
2175
|
+
const row = agents.find((a) => a.recordId === id || a.recordId.startsWith(id));
|
|
2176
|
+
if (!row) {
|
|
2177
|
+
ctx.ui.notify(`No tracked subagent matches "${id}". /glla agents lists the current ids.`, "warning");
|
|
2178
|
+
return;
|
|
2179
|
+
}
|
|
2180
|
+
const linesMatch = args.match(/--lines\s+(\d+)/);
|
|
2181
|
+
// v0.35.45 (audit finding): the candidate scan reads a bounded TAIL of
|
|
2182
|
+
// each transcript instead of up to 25 FULL files synchronously on the
|
|
2183
|
+
// main thread. The single matched file still gets a full read so the
|
|
2184
|
+
// "last N of M" detail stays honest.
|
|
2185
|
+
const readTailAware = (file: string, maxBytes?: number): Buffer => {
|
|
2186
|
+
if (maxBytes === undefined) return fs.readFileSync(file);
|
|
2187
|
+
try {
|
|
2188
|
+
const size = fs.statSync(file).size;
|
|
2189
|
+
if (size <= maxBytes) return fs.readFileSync(file);
|
|
2190
|
+
const fd = fs.openSync(file, "r");
|
|
2191
|
+
try {
|
|
2192
|
+
const buf = Buffer.alloc(maxBytes);
|
|
2193
|
+
const bytesRead = fs.readSync(fd, buf, 0, maxBytes, size - maxBytes);
|
|
2194
|
+
return buf.subarray(0, bytesRead);
|
|
2195
|
+
} finally {
|
|
2196
|
+
fs.closeSync(fd);
|
|
2197
|
+
}
|
|
2198
|
+
} catch {
|
|
2199
|
+
return fs.readFileSync(file);
|
|
2200
|
+
}
|
|
2201
|
+
};
|
|
2202
|
+
const result = tailChildTranscript(childSessionsDir(ctx.cwd), row, {
|
|
2203
|
+
lines: linesMatch ? Number(linesMatch[1]) : undefined,
|
|
2204
|
+
readFile: readTailAware,
|
|
2205
|
+
listDir: (dir) => fs.readdirSync(dir),
|
|
2206
|
+
statMtime: (file) => { try { return fs.statSync(file).mtimeMs; } catch { return 0; } },
|
|
2207
|
+
});
|
|
2208
|
+
const header = result.ok
|
|
2209
|
+
? `transcript tail — ${row.agentType ?? row.recordId} (${result.detail})`
|
|
2210
|
+
: `no transcript tail for ${row.agentType ?? row.recordId}`;
|
|
2211
|
+
ctx.ui.notify([header, ...result.lines, ...(result.ok ? [] : [result.detail])].join("\n"), result.ok ? "info" : "warning");
|
|
2212
|
+
return;
|
|
2213
|
+
}
|
|
2214
|
+
ctx.ui.notify(`glla agents${managerAvailable ? "" : " (pi-subagents manager registry not present — event evidence only)"}\n${renderAgentsPanel(agents, Date.now(), managerAvailable).join("\n")}`, "info");
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2016
2217
|
// v0.29.8: /glla status — the unified "what's running" surface (user: "we
|
|
2017
2218
|
// need to type goal status [to check], so that command at least is missing
|
|
2018
2219
|
// for checking on whatever active process we have"). Read-only aggregate of
|
|
@@ -2020,6 +2221,11 @@ function cmdGllaVersion(ctx: ExtensionContext): void {
|
|
|
2020
2221
|
// to the deep surfaces.
|
|
2021
2222
|
function cmdGllaStatus(ctx: ExtensionContext): void {
|
|
2022
2223
|
const lines: string[] = [];
|
|
2224
|
+
// v0.35.15: name a frozen supervisor FIRST — it changes how every other
|
|
2225
|
+
// line reads (nothing automatic will fire while this is set).
|
|
2226
|
+
if (typeof state.supervisorPausedAt === "number") {
|
|
2227
|
+
lines.push(`supervisor: ⏸ PAUSED since ${new Date(state.supervisorPausedAt).toISOString()} — re-arms/recovery/dispatch frozen — /glla resume`);
|
|
2228
|
+
}
|
|
2023
2229
|
const g = state.goal;
|
|
2024
2230
|
if (g) {
|
|
2025
2231
|
const tok = (g.usage?.tokensUsed ?? 0) > 0 ? ` · ${g.usage!.tokensUsed} tok` : "";
|
|
@@ -2078,6 +2284,11 @@ async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
2078
2284
|
cmdAudits(trimmed.slice("audits".length).trim(), ctx);
|
|
2079
2285
|
return;
|
|
2080
2286
|
}
|
|
2287
|
+
// v0.35.29 (issue #15): tracked-subagent panel — read-only, stale-safe.
|
|
2288
|
+
if (/^agents(?:\s|$)/.test(trimmed)) {
|
|
2289
|
+
cmdAgents(trimmed.slice("agents".length).trim(), ctx);
|
|
2290
|
+
return;
|
|
2291
|
+
}
|
|
2081
2292
|
if (/^status(?:\s|$)/.test(trimmed)) {
|
|
2082
2293
|
cmdGllaStatus(ctx);
|
|
2083
2294
|
return;
|
|
@@ -2127,6 +2338,10 @@ async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
2127
2338
|
await cmdGllaResume(ctx);
|
|
2128
2339
|
return;
|
|
2129
2340
|
}
|
|
2341
|
+
if (/^pause(?:\s|$)/.test(trimmed)) {
|
|
2342
|
+
await cmdGllaPause(ctx);
|
|
2343
|
+
return;
|
|
2344
|
+
}
|
|
2130
2345
|
if (/^cancel(?:\s|$)/.test(trimmed)) {
|
|
2131
2346
|
await cmdGllaCancel(ctx);
|
|
2132
2347
|
return;
|
|
@@ -2198,6 +2413,7 @@ async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
2198
2413
|
fmt("aggressiveMode", "aggressiveMode"),
|
|
2199
2414
|
fmt("stuckMaxInterventions", "stuckMaxInterventions"),
|
|
2200
2415
|
fmt("stallEscalationRefires", "stallEscalation"),
|
|
2416
|
+
fmt("subagentHangEscalationMinutes", "subagentHangActionMinutes"),
|
|
2201
2417
|
fmt("wedgeAlertMinutes", "wedgeAlert"),
|
|
2202
2418
|
fmt("stallShortWords", "stallShortWords"),
|
|
2203
2419
|
fmt("stallSimilarityThreshold", "stallSimilarityThreshold"),
|