@siuver/omp-debug-mode 0.1.3 → 0.1.5
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 +20 -0
- package/README.md +154 -94
- package/package.json +3 -3
- package/src/debug-mode.ts +350 -334
- package/src/evidence.ts +169 -0
- package/src/gate.ts +72 -25
- package/src/machine.ts +334 -0
- package/src/methodology.ts +106 -32
- package/src/probes.ts +9 -9
- package/src/state.ts +288 -36
- package/src/tools.ts +80 -6
- package/src/ui.ts +73 -24
- package/src/review-actions.ts +0 -22
package/src/debug-mode.ts
CHANGED
|
@@ -2,7 +2,8 @@ import { Text } from "@oh-my-pi/pi-coding-agent";
|
|
|
2
2
|
import type { ExtensionAPI, ExtensionContext, MessageRenderer } from "@oh-my-pi/pi-coding-agent";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
-
import {
|
|
5
|
+
import { describeEvidence, validateEvidenceArtifact } from "./evidence";
|
|
6
|
+
import { describeOpenReason } from "./gate";
|
|
6
7
|
import {
|
|
7
8
|
ACTIVE_LOG_FILE,
|
|
8
9
|
JsonlLineCounter,
|
|
@@ -11,46 +12,43 @@ import {
|
|
|
11
12
|
readJsonlLines,
|
|
12
13
|
summarizeHypotheses,
|
|
13
14
|
} from "./log-files";
|
|
15
|
+
import { type DebugEvent, type Effect, PROMPT_FIXED, PROMPT_PROCEED, PROMPT_START, reduce } from "./machine";
|
|
16
|
+
import { CLEANUP_CONTRACT, METHODOLOGY, extractAssistantText } from "./methodology";
|
|
17
|
+
import { type LedgerScan, describeLedger, probesInInput, scanLedger, survivingProbes } from "./probes";
|
|
14
18
|
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
PROCEED_REMINDER,
|
|
18
|
-
buildFixedMessage,
|
|
19
|
-
buildProceedMessage,
|
|
20
|
-
buildStartMessage,
|
|
21
|
-
extractAssistantText,
|
|
22
|
-
extractReproductionSteps,
|
|
23
|
-
} from "./methodology";
|
|
24
|
-
import { describeLedger, recordProbes, syncLedger } from "./probes";
|
|
25
|
-
import { REVIEW_ABORT, REVIEW_ADD_DETAILS, REVIEW_MARK_FIXED, REVIEW_PROCEED, reviewMenuOptions } from "./review-actions";
|
|
26
|
-
import {
|
|
19
|
+
type DebugSession,
|
|
20
|
+
type DebugState,
|
|
27
21
|
DEBUG_CONTEXT_TYPE,
|
|
28
22
|
DEBUG_ENTRY,
|
|
29
|
-
|
|
23
|
+
INACTIVE,
|
|
24
|
+
activeRunId,
|
|
30
25
|
blackboard,
|
|
31
26
|
compareRunIds,
|
|
32
|
-
|
|
27
|
+
currentRound,
|
|
28
|
+
evidenceSummary,
|
|
29
|
+
evidenceView,
|
|
30
|
+
hasNonProbeEvidence,
|
|
33
31
|
keepLatestCustomType,
|
|
34
32
|
logFileFor,
|
|
33
|
+
pendingRequests,
|
|
34
|
+
reviveState,
|
|
35
35
|
} from "./state";
|
|
36
36
|
import { registerDebugTools } from "./tools";
|
|
37
|
-
import { applyUi
|
|
37
|
+
import { applyUi } from "./ui";
|
|
38
38
|
import { debugDirFor, excludeDebugLogsFromGit, pruneDebugRoot } from "./workspace";
|
|
39
39
|
|
|
40
40
|
const COMMAND_MODE = "debug-mode";
|
|
41
|
-
const COMMAND_MENU = "debug-menu";
|
|
42
41
|
const COMMAND_DONE = "debug-done";
|
|
43
42
|
const COMMAND_PROCEED = "debug-proceed";
|
|
44
|
-
const
|
|
43
|
+
const COMMAND_EVIDENCE = "debug-evidence";
|
|
45
44
|
const COMMAND_ABORT = "debug-abort";
|
|
46
45
|
const COMMAND_STATUS = "debug-status";
|
|
47
46
|
|
|
48
47
|
/** Compact transcript lines for the prompts this extension injects. */
|
|
49
48
|
const MESSAGE_SUMMARIES: Record<string, string> = {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"debug-mode-fixed": "marked fixed — removing probes and summarizing",
|
|
49
|
+
[PROMPT_START]: "debug mode started — hypotheses and instrumentation",
|
|
50
|
+
[PROMPT_PROCEED]: "proceed — analyzing captured logs",
|
|
51
|
+
[PROMPT_FIXED]: "marked fixed — removing probes and summarizing",
|
|
54
52
|
};
|
|
55
53
|
|
|
56
54
|
interface DebugMessageDetails {
|
|
@@ -58,8 +56,7 @@ interface DebugMessageDetails {
|
|
|
58
56
|
}
|
|
59
57
|
|
|
60
58
|
export function registerDebugMode(pi: ExtensionAPI): void {
|
|
61
|
-
|
|
62
|
-
let reviewMenuOpen = false;
|
|
59
|
+
let state: DebugState = INACTIVE;
|
|
63
60
|
let uiCtx: ExtensionContext | null = null;
|
|
64
61
|
let watchedLogFile: string | null = null;
|
|
65
62
|
const lineCounter = new JsonlLineCounter();
|
|
@@ -72,79 +69,142 @@ export function registerDebugMode(pi: ExtensionAPI): void {
|
|
|
72
69
|
pi.registerMessageRenderer<DebugMessageDetails>(customType, renderer);
|
|
73
70
|
}
|
|
74
71
|
|
|
75
|
-
// ==============================
|
|
72
|
+
// ============================== state plumbing ==============================
|
|
76
73
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Cache-only events carry no effects, so they may run inside a render pass
|
|
76
|
+
* without re-entering the UI refresh that asked for them.
|
|
77
|
+
*/
|
|
78
|
+
function absorbCache(event: Extract<DebugEvent, { t: "runs_observed" | "ledger_synced" }>): void {
|
|
79
|
+
state = reduce(state, event).state;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Run one event through the machine, then realise whatever it asked for. */
|
|
83
|
+
function dispatch(event: DebugEvent, ctx: ExtensionContext): string | null {
|
|
84
|
+
const transition = reduce(state, event);
|
|
85
|
+
// The reducer returns the previous object for a genuine no-op, so a turn
|
|
86
|
+
// full of tool calls does not append a state snapshot per message.
|
|
87
|
+
const changed = transition.state !== state;
|
|
88
|
+
state = transition.state;
|
|
89
|
+
if (changed) pi.appendEntry(DEBUG_ENTRY, { ...state });
|
|
90
|
+
refreshUi();
|
|
91
|
+
watchLogFile();
|
|
92
|
+
let continueContext: string | null = null;
|
|
93
|
+
for (const effect of transition.effects) {
|
|
94
|
+
const context = applyEffect(effect, ctx);
|
|
95
|
+
if (context) continueContext = context;
|
|
96
|
+
}
|
|
97
|
+
return continueContext;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function applyEffect(effect: Effect, ctx: ExtensionContext): string | null {
|
|
101
|
+
switch (effect.kind) {
|
|
102
|
+
case "notify":
|
|
103
|
+
ctx.ui.notify(effect.text, effect.level);
|
|
104
|
+
return null;
|
|
105
|
+
case "prompt":
|
|
106
|
+
pi.sendMessage(
|
|
107
|
+
{
|
|
108
|
+
customType: effect.customType,
|
|
109
|
+
content: effect.content,
|
|
110
|
+
display: true,
|
|
111
|
+
details: { summary: effect.summary } satisfies DebugMessageDetails,
|
|
112
|
+
},
|
|
113
|
+
{ triggerTurn: true },
|
|
114
|
+
);
|
|
115
|
+
return null;
|
|
116
|
+
case "continue":
|
|
117
|
+
return effect.context;
|
|
118
|
+
case "teardown": {
|
|
119
|
+
unwatchLogFile();
|
|
120
|
+
lineCounter.clear();
|
|
121
|
+
if (effect.debugDir) {
|
|
122
|
+
try {
|
|
123
|
+
fs.rmSync(effect.debugDir, { recursive: true, force: true });
|
|
124
|
+
pruneDebugRoot(ctx.cwd);
|
|
125
|
+
} catch (err) {
|
|
126
|
+
pi.logger.warn("debug-mode: failed to remove debug dir", { err });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const label = effect.outcome === "finished" ? "finished" : "aborted";
|
|
130
|
+
if (effect.probesLeft.length > 0) {
|
|
131
|
+
ctx.ui.notify(
|
|
132
|
+
`Debug mode ${label}, but ${effect.probesLeft.length} probe(s) remain in code: ${effect.probesLeft.map(p => p.id).join(", ")} — remove manually. Applied fixes remain in the working diff.`,
|
|
133
|
+
"warning",
|
|
134
|
+
);
|
|
135
|
+
} else {
|
|
136
|
+
ctx.ui.notify(
|
|
137
|
+
`Debug mode ${label}. Log files removed; applied fixes remain in the working diff for review.`,
|
|
138
|
+
"info",
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
88
143
|
}
|
|
89
144
|
}
|
|
90
145
|
|
|
146
|
+
function refreshUi(): void {
|
|
147
|
+
applyUi(uiCtx, state, currentLogCount);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ============================== log files ==============================
|
|
151
|
+
|
|
91
152
|
function readRunLines(run: string): string[] {
|
|
92
|
-
return readJsonlLines(logFileFor(state, run));
|
|
153
|
+
return state.active ? readJsonlLines(logFileFor(state, run)) : [];
|
|
93
154
|
}
|
|
94
155
|
|
|
156
|
+
/** Re-count every run on disk. Pure cache maintenance — never a decision. */
|
|
95
157
|
function refreshLogCounts(): void {
|
|
96
|
-
|
|
97
|
-
|
|
158
|
+
if (!state.active) return;
|
|
159
|
+
const session: DebugSession = state;
|
|
160
|
+
const active = activeRunId(session);
|
|
161
|
+
const runs = new Set(Object.keys(session.logCounts));
|
|
162
|
+
if (session.debugDir) {
|
|
98
163
|
try {
|
|
99
|
-
for (const file of fs.readdirSync(
|
|
164
|
+
for (const file of fs.readdirSync(session.debugDir)) {
|
|
100
165
|
if (!file.endsWith(".jsonl")) continue;
|
|
101
166
|
if (file === ACTIVE_LOG_FILE) {
|
|
102
|
-
if (
|
|
167
|
+
if (active) runs.add(active);
|
|
103
168
|
} else {
|
|
104
169
|
runs.add(file.replace(/\.jsonl$/, ""));
|
|
105
170
|
}
|
|
106
171
|
}
|
|
107
172
|
} catch {}
|
|
108
173
|
}
|
|
109
|
-
|
|
110
|
-
|
|
174
|
+
const logCounts: Record<string, number> = {};
|
|
175
|
+
for (const run of runs) logCounts[run] = lineCounter.count(logFileFor(session, run));
|
|
176
|
+
absorbCache({ t: "runs_observed", runHistory: orderedHistory(session, runs), logCounts });
|
|
111
177
|
}
|
|
112
178
|
|
|
113
179
|
/** Keep the ordered history complete when runs are discovered from disk. */
|
|
114
|
-
function
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const completed =
|
|
180
|
+
function orderedHistory(session: DebugSession, runs: Iterable<string>): string[] {
|
|
181
|
+
const active = activeRunId(session);
|
|
182
|
+
const missing = [...runs].filter(run => run !== active && !session.runHistory.includes(run));
|
|
183
|
+
if (missing.length === 0) return session.runHistory;
|
|
184
|
+
const completed = session.runHistory.filter(run => run !== active).concat(missing);
|
|
119
185
|
completed.sort(compareRunIds);
|
|
120
|
-
|
|
186
|
+
return active ? [...completed, active] : completed;
|
|
121
187
|
}
|
|
122
188
|
|
|
123
189
|
function currentLogCount(): number {
|
|
124
190
|
refreshLogCounts();
|
|
125
|
-
|
|
191
|
+
if (!state.active) return 0;
|
|
192
|
+
const run = activeRunId(state);
|
|
193
|
+
return run ? (state.logCounts[run] ?? 0) : 0;
|
|
126
194
|
}
|
|
127
195
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const run = `run${state.round}-${Date.now().toString(36)}`;
|
|
196
|
+
/** Archive the active log, truncate it for the next reproduction, and name the run. */
|
|
197
|
+
function createRun(debugDir: string, round: number, previousRun: string | null): string | null {
|
|
131
198
|
try {
|
|
132
|
-
prepareRunLog(
|
|
199
|
+
prepareRunLog(debugDir, previousRun);
|
|
133
200
|
} catch (err) {
|
|
134
201
|
pi.logger.error("debug-mode: cannot initialize run log", {
|
|
135
|
-
file: path.join(
|
|
202
|
+
file: path.join(debugDir, ACTIVE_LOG_FILE),
|
|
136
203
|
err,
|
|
137
204
|
});
|
|
138
205
|
return null;
|
|
139
206
|
}
|
|
140
|
-
|
|
141
|
-
state.runHistory.push(run);
|
|
142
|
-
state.logCounts[run] = 0;
|
|
143
|
-
return run;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function refreshUi(): void {
|
|
147
|
-
applyUi(uiCtx, state, currentLogCount);
|
|
207
|
+
return `run${round}-${Date.now().toString(36)}`;
|
|
148
208
|
}
|
|
149
209
|
|
|
150
210
|
/**
|
|
@@ -153,13 +213,13 @@ export function registerDebugMode(pi: ExtensionAPI): void {
|
|
|
153
213
|
* refresh the widget as observations land.
|
|
154
214
|
*/
|
|
155
215
|
function watchLogFile(): void {
|
|
156
|
-
const file = state.
|
|
216
|
+
const file = state.active && state.stage === "awaiting_evidence" ? logFileFor(state) : null;
|
|
157
217
|
if (file === watchedLogFile) return;
|
|
158
218
|
unwatchLogFile();
|
|
159
219
|
if (!file || !uiCtx?.hasUI) return;
|
|
160
220
|
try {
|
|
161
221
|
fs.watchFile(file, { interval: 1000 }, (curr, prev) => {
|
|
162
|
-
if (state.
|
|
222
|
+
if (!state.active || state.stage !== "awaiting_evidence") return;
|
|
163
223
|
if (curr.mtimeMs === prev.mtimeMs && curr.size === prev.size) return;
|
|
164
224
|
try {
|
|
165
225
|
refreshUi();
|
|
@@ -185,25 +245,34 @@ export function registerDebugMode(pi: ExtensionAPI): void {
|
|
|
185
245
|
|
|
186
246
|
// ============================== probe ledger ==============================
|
|
187
247
|
|
|
248
|
+
/** Rescan the recorded files so the ledger matches the code on disk. */
|
|
249
|
+
async function syncLedger(): Promise<LedgerScan> {
|
|
250
|
+
const scan = await scanLedger(state.active ? state.probes : []);
|
|
251
|
+
if (state.active) absorbCache({ t: "ledger_synced", probes: survivingProbes(scan) });
|
|
252
|
+
return scan;
|
|
253
|
+
}
|
|
254
|
+
|
|
188
255
|
pi.on("tool_call", async (event, ctx) => {
|
|
189
256
|
if (!state.active) return;
|
|
190
257
|
if (event.toolName !== "edit" && event.toolName !== "write") return;
|
|
191
|
-
|
|
258
|
+
const probes = probesInInput(event.input as Record<string, unknown>, ctx.cwd, currentRound(state).index);
|
|
259
|
+
if (probes.length > 0) dispatch({ t: "probes_found", probes }, ctx);
|
|
192
260
|
});
|
|
193
261
|
|
|
194
262
|
// ============================== prompt injection ==============================
|
|
195
263
|
|
|
196
264
|
pi.on("before_agent_start", async () => {
|
|
197
|
-
if (!state.active ||
|
|
265
|
+
if (!state.active || state.stage === "awaiting_evidence") return;
|
|
198
266
|
// The blackboard claims to be ground truth, so reconcile it with disk first.
|
|
199
|
-
await syncLedger(
|
|
267
|
+
await syncLedger();
|
|
268
|
+
if (!state.active) return;
|
|
200
269
|
// Cleanup has no hypotheses left to form; the full methodology would only
|
|
201
270
|
// invite another round.
|
|
202
|
-
const contract = state.
|
|
271
|
+
const contract = state.stage === "cleaning_up" ? CLEANUP_CONTRACT : METHODOLOGY;
|
|
203
272
|
return {
|
|
204
273
|
message: {
|
|
205
274
|
customType: DEBUG_CONTEXT_TYPE,
|
|
206
|
-
content: `${blackboard(state)}\n\n${contract}`,
|
|
275
|
+
content: `${blackboard(state, describeEvidence(evidenceView(state)))}\n\n${contract}`,
|
|
207
276
|
display: false,
|
|
208
277
|
},
|
|
209
278
|
};
|
|
@@ -217,229 +286,141 @@ export function registerDebugMode(pi: ExtensionAPI): void {
|
|
|
217
286
|
if (filtered.length !== event.messages.length) return { messages: filtered };
|
|
218
287
|
});
|
|
219
288
|
|
|
220
|
-
// ==============================
|
|
289
|
+
// ============================== turn lifecycle ==============================
|
|
221
290
|
|
|
222
|
-
pi.on("agent_start", async () => {
|
|
223
|
-
if (state.active)
|
|
291
|
+
pi.on("agent_start", async (_event, ctx) => {
|
|
292
|
+
if (state.active) dispatch({ t: "turn_started" }, ctx);
|
|
224
293
|
});
|
|
225
294
|
|
|
226
|
-
pi.on("message_end", async (event) => {
|
|
295
|
+
pi.on("message_end", async (event, ctx) => {
|
|
227
296
|
if (!state.active) return;
|
|
228
297
|
const msg = event.message as { role?: string; content?: unknown };
|
|
229
|
-
if (msg?.role
|
|
230
|
-
|
|
231
|
-
state.hasRoundContent = true;
|
|
232
|
-
const steps = extractReproductionSteps(extractAssistantText(msg.content));
|
|
233
|
-
if (steps.length > 0) state.reproductionSteps = steps;
|
|
234
|
-
}
|
|
298
|
+
if (msg?.role !== "assistant") return;
|
|
299
|
+
dispatch({ t: "assistant_message", text: extractAssistantText(msg.content) }, ctx);
|
|
235
300
|
});
|
|
236
301
|
|
|
237
302
|
pi.on("session_stop", async (_event, ctx) => {
|
|
238
|
-
if (!state.active) return;
|
|
239
|
-
if (state.
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
}
|
|
244
|
-
if (
|
|
245
|
-
|
|
246
|
-
const probesThisRound = state.probes.filter(p => p.round === state.round).length;
|
|
247
|
-
const decision = decideGate({
|
|
248
|
-
hasReproductionSteps: state.reproductionSteps.length > 0,
|
|
249
|
-
probesThisRound,
|
|
250
|
-
nudgesUsed: state.gateNudges,
|
|
251
|
-
});
|
|
252
|
-
if (decision.kind === "stay") return;
|
|
253
|
-
if (decision.kind === "nudge") {
|
|
254
|
-
state.gateNudges += 1;
|
|
255
|
-
return { continue: true, additionalContext: decision.context };
|
|
256
|
-
}
|
|
257
|
-
enterGate(ctx, probesThisRound, decision.missingSteps);
|
|
303
|
+
if (!state.active || !state.turnProduced) return;
|
|
304
|
+
if (state.stage !== "investigating" && state.stage !== "cleaning_up") return;
|
|
305
|
+
// A declared runtime_probe must be backed by a marker that is really on
|
|
306
|
+
// disk, so the ledger is reconciled before the round may close.
|
|
307
|
+
await syncLedger();
|
|
308
|
+
const context = dispatch({ t: "turn_settled" }, ctx);
|
|
309
|
+
if (context) return { continue: true, additionalContext: context };
|
|
258
310
|
});
|
|
259
311
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
312
|
+
registerDebugTools(pi, { getState: () => state, refreshLogCounts, readRunLines, syncLedger });
|
|
313
|
+
|
|
314
|
+
// ============================== command guards ==============================
|
|
315
|
+
|
|
316
|
+
function activeSession(ctx: ExtensionContext): DebugSession | null {
|
|
317
|
+
if (!state.active) {
|
|
318
|
+
ctx.ui.notify("debug-mode: not active", "error");
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
return state;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function ownsTurn(session: DebugSession): boolean {
|
|
325
|
+
return session.stage === "awaiting_evidence" || session.stage === "open";
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* The debug commands belong to the user's turn. An unclosed round is still
|
|
330
|
+
* the user's move, so it gets a confirmation rather than the flat "not
|
|
331
|
+
* waiting for reproduction" refusal that made an open round look stuck.
|
|
332
|
+
*/
|
|
333
|
+
async function claimTurn(ctx: ExtensionContext, action: string): Promise<boolean> {
|
|
334
|
+
const session = activeSession(ctx);
|
|
335
|
+
if (!session) return false;
|
|
336
|
+
if (!ownsTurn(session)) {
|
|
267
337
|
ctx.ui.notify(
|
|
268
|
-
`
|
|
269
|
-
"
|
|
338
|
+
`debug-mode: the agent still has round ${currentRound(session).index} (stage: ${session.stage}) — wait for it to stop.`,
|
|
339
|
+
"error",
|
|
270
340
|
);
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
if (session.stage === "open" && ctx.hasUI) {
|
|
344
|
+
const confirmed = await ctx.ui.confirm(
|
|
345
|
+
`${action} an unclosed round?`,
|
|
346
|
+
`${describeOpenReason(currentRound(session).openReason ?? "awaiting_reply", currentRound(session).index)} Continue with the evidence that exists?`,
|
|
275
347
|
);
|
|
276
|
-
|
|
277
|
-
ctx.ui.notify(`Debug round ${state.round} paused. Reproduce the bug, then ${PROCEED_REMINDER}`, "info");
|
|
348
|
+
if (!confirmed) return false;
|
|
278
349
|
}
|
|
350
|
+
return true;
|
|
279
351
|
}
|
|
280
352
|
|
|
281
353
|
// ============================== round transitions ==============================
|
|
282
354
|
|
|
283
355
|
function startDebug(ctx: ExtensionContext, problem: string): void {
|
|
284
|
-
|
|
356
|
+
const debugDir = debugDirFor(ctx.cwd, ctx.sessionManager.getSessionId());
|
|
357
|
+
try {
|
|
358
|
+
fs.mkdirSync(debugDir, { recursive: true });
|
|
359
|
+
excludeDebugLogsFromGit(ctx.cwd);
|
|
360
|
+
} catch (err) {
|
|
361
|
+
pi.logger.error("debug-mode: cannot create log dir", { dir: debugDir, err });
|
|
285
362
|
ctx.ui.notify("debug-mode: could not create the debug log directory; debug mode was not started", "error");
|
|
286
363
|
return;
|
|
287
364
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
state.problem = problem;
|
|
291
|
-
state.round = 1;
|
|
292
|
-
state.probes = [];
|
|
293
|
-
state.runHistory = [];
|
|
294
|
-
state.logCounts = {};
|
|
295
|
-
state.hasRoundContent = false;
|
|
296
|
-
state.cleanupReady = false;
|
|
297
|
-
state.reproductionSteps = [];
|
|
298
|
-
state.gateNudges = 0;
|
|
299
|
-
if (!newRun()) {
|
|
300
|
-
Object.assign(state, freshState());
|
|
365
|
+
const runId = createRun(debugDir, 1, null);
|
|
366
|
+
if (!runId) {
|
|
301
367
|
ctx.ui.notify("debug-mode: could not initialize the run log file; debug mode was not started", "error");
|
|
302
368
|
return;
|
|
303
369
|
}
|
|
304
|
-
|
|
305
|
-
const logFile = logFileFor(state);
|
|
306
|
-
if (!logFile) {
|
|
307
|
-
Object.assign(state, freshState());
|
|
308
|
-
ctx.ui.notify("debug-mode: could not resolve the run log file; debug mode was not started", "error");
|
|
309
|
-
return;
|
|
310
|
-
}
|
|
311
|
-
pi.sendMessage(
|
|
312
|
-
{
|
|
313
|
-
customType: "debug-mode-start",
|
|
314
|
-
content: buildStartMessage(problem, logFile),
|
|
315
|
-
display: true,
|
|
316
|
-
details: { summary: "debug mode started — hypotheses and instrumentation" } satisfies DebugMessageDetails,
|
|
317
|
-
},
|
|
318
|
-
{ triggerTurn: true },
|
|
319
|
-
);
|
|
370
|
+
dispatch({ t: "start", problem, debugDir, runId, logFile: path.join(debugDir, ACTIVE_LOG_FILE) }, ctx);
|
|
320
371
|
}
|
|
321
372
|
|
|
322
|
-
function
|
|
323
|
-
if (!
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
return false;
|
|
330
|
-
}
|
|
331
|
-
return true;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
async function markDebugFixed(ctx: ExtensionContext): Promise<void> {
|
|
335
|
-
if (!ensureWaiting(ctx)) return;
|
|
336
|
-
const logCount = currentLogCount();
|
|
337
|
-
if (logCount === 0 && ctx.hasUI) {
|
|
373
|
+
async function advanceDebug(ctx: ExtensionContext, userDetails?: string): Promise<void> {
|
|
374
|
+
if (!(await claimTurn(ctx, "Proceed from"))) return;
|
|
375
|
+
refreshLogCounts();
|
|
376
|
+
if (!state.active) return;
|
|
377
|
+
const closingRun = activeRunId(state);
|
|
378
|
+
const logCount = closingRun ? (state.logCounts[closingRun] ?? 0) : 0;
|
|
379
|
+
if (logCount === 0 && !userDetails && !hasNonProbeEvidence(currentRound(state)) && ctx.hasUI) {
|
|
338
380
|
const confirmed = await ctx.ui.confirm(
|
|
339
|
-
"
|
|
340
|
-
"No runtime observations were captured for this round.
|
|
381
|
+
"Proceed without runtime logs?",
|
|
382
|
+
"No runtime observations were captured for this round. Continue to log analysis anyway?",
|
|
341
383
|
);
|
|
342
384
|
if (!confirmed) return;
|
|
343
385
|
}
|
|
344
|
-
if (!state.active
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
state.cleanupReady = false;
|
|
348
|
-
unwatchLogFile();
|
|
349
|
-
refreshUi();
|
|
350
|
-
pi.sendMessage(
|
|
351
|
-
{
|
|
352
|
-
customType: "debug-mode-fixed",
|
|
353
|
-
content: buildFixedMessage(JSON.stringify(state.probes)),
|
|
354
|
-
display: true,
|
|
355
|
-
details: { summary: `marked fixed — removing ${state.probes.length} probe(s)` } satisfies DebugMessageDetails,
|
|
356
|
-
},
|
|
357
|
-
{ triggerTurn: true },
|
|
358
|
-
);
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
async function advanceDebug(ctx: ExtensionContext, reproductionDetails?: string): Promise<void> {
|
|
362
|
-
if (!ensureWaiting(ctx)) return;
|
|
363
|
-
refreshLogCounts();
|
|
364
|
-
const run = state.runId ?? "(none)";
|
|
365
|
-
const logCount = state.logCounts[run] ?? 0;
|
|
366
|
-
if (logCount === 0 && !reproductionDetails && ctx.hasUI) {
|
|
386
|
+
if (!state.active) return;
|
|
387
|
+
const stillPending = pendingRequests(state);
|
|
388
|
+
if (stillPending.length > 0 && ctx.hasUI) {
|
|
367
389
|
const confirmed = await ctx.ui.confirm(
|
|
368
|
-
"Proceed without
|
|
369
|
-
|
|
390
|
+
"Proceed without all requested evidence?",
|
|
391
|
+
`${stillPending.length} evidence request(s) still pending (${stillPending.map(r => r.id).join(", ")}). Continue anyway?`,
|
|
370
392
|
);
|
|
371
393
|
if (!confirmed) return;
|
|
372
394
|
}
|
|
373
|
-
if (!state.active || state
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
state.hasRoundContent = false;
|
|
381
|
-
state.reproductionSteps = [];
|
|
382
|
-
state.gateNudges = 0;
|
|
383
|
-
if (!newRun()) {
|
|
384
|
-
state.round = previousRound;
|
|
385
|
-
state.phase = "waiting";
|
|
386
|
-
state.reproductionSteps = previousSteps;
|
|
387
|
-
ctx.ui.notify("debug-mode: could not initialize the next run log file; staying at the reproduction gate", "error");
|
|
388
|
-
refreshUi();
|
|
395
|
+
if (!state.active || !ownsTurn(state) || !state.debugDir) return;
|
|
396
|
+
|
|
397
|
+
// The digest must be read before the active log is archived and truncated.
|
|
398
|
+
const hypotheses = describeHypotheses(summarizeHypotheses(closingRun ? readRunLines(closingRun) : []));
|
|
399
|
+
const runId = createRun(state.debugDir, currentRound(state).index + 1, closingRun);
|
|
400
|
+
if (!runId) {
|
|
401
|
+
ctx.ui.notify("debug-mode: could not initialize the next run log file; staying on this round", "error");
|
|
389
402
|
return;
|
|
390
403
|
}
|
|
391
|
-
|
|
392
|
-
refreshUi();
|
|
393
|
-
|
|
394
|
-
const summary = reproductionDetails
|
|
395
|
-
? `reproduction details added — analyzing run ${run} (${logCount} entries)`
|
|
396
|
-
: `proceed — analyzing run ${run} (${logCount} entries)`;
|
|
397
|
-
pi.sendMessage(
|
|
398
|
-
{
|
|
399
|
-
customType: reproductionDetails ? "debug-mode-note" : "debug-mode-proceed",
|
|
400
|
-
content: buildProceedMessage({ run, logCount, reproductionDetails, hypotheses }),
|
|
401
|
-
display: true,
|
|
402
|
-
details: { summary } satisfies DebugMessageDetails,
|
|
403
|
-
},
|
|
404
|
-
{ triggerTurn: true },
|
|
405
|
-
);
|
|
404
|
+
dispatch({ t: "proceed", runId, logCount, hypotheses, details: userDetails, now: Date.now() }, ctx);
|
|
406
405
|
}
|
|
407
406
|
|
|
408
|
-
async function
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
if (state.
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
pi.logger.warn("debug-mode: failed to remove debug dir", { err });
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
const scan = await syncLedger(state);
|
|
420
|
-
const probesLeft = [...scan.alive, ...scan.unknown];
|
|
421
|
-
Object.assign(state, freshState());
|
|
422
|
-
pi.appendEntry(DEBUG_ENTRY, { ...state });
|
|
423
|
-
refreshUi();
|
|
424
|
-
const resultLabel = outcome === "finished" ? "finished" : "aborted";
|
|
425
|
-
if (probesLeft.length > 0) {
|
|
426
|
-
ctx.ui.notify(
|
|
427
|
-
`Debug mode ${resultLabel}, but ${probesLeft.length} probe(s) remain in code: ${probesLeft.map(p => p.id).join(", ")} — remove manually. Applied fixes remain in the working diff.`,
|
|
428
|
-
"warning",
|
|
429
|
-
);
|
|
430
|
-
} else {
|
|
431
|
-
ctx.ui.notify(
|
|
432
|
-
`Debug mode ${resultLabel}. Log files removed; applied fixes remain in the working diff for review.`,
|
|
433
|
-
"info",
|
|
407
|
+
async function markDebugFixed(ctx: ExtensionContext): Promise<void> {
|
|
408
|
+
if (!(await claimTurn(ctx, "Mark as fixed from"))) return;
|
|
409
|
+
const logCount = currentLogCount();
|
|
410
|
+
if (!state.active) return;
|
|
411
|
+
if (logCount === 0 && !hasNonProbeEvidence(currentRound(state)) && ctx.hasUI) {
|
|
412
|
+
const confirmed = await ctx.ui.confirm(
|
|
413
|
+
"Mark as fixed without runtime logs?",
|
|
414
|
+
"No runtime observations were captured for this round. Mark the problem as fixed anyway?",
|
|
434
415
|
);
|
|
416
|
+
if (!confirmed) return;
|
|
435
417
|
}
|
|
418
|
+
if (!state.active || !ownsTurn(state)) return;
|
|
419
|
+
dispatch({ t: "mark_fixed" }, ctx);
|
|
436
420
|
}
|
|
437
421
|
|
|
438
422
|
async function abortDebug(ctx: ExtensionContext): Promise<void> {
|
|
439
|
-
if (!
|
|
440
|
-
ctx.ui.notify("debug-mode: not active", "error");
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
423
|
+
if (!activeSession(ctx)) return;
|
|
443
424
|
if (ctx.hasUI) {
|
|
444
425
|
const confirmed = await ctx.ui.confirm(
|
|
445
426
|
"Abort debug mode?",
|
|
@@ -448,53 +429,59 @@ export function registerDebugMode(pi: ExtensionAPI): void {
|
|
|
448
429
|
if (!confirmed) return;
|
|
449
430
|
}
|
|
450
431
|
if (!state.active) return;
|
|
451
|
-
|
|
432
|
+
dispatch({ t: "abort" }, ctx);
|
|
452
433
|
}
|
|
453
434
|
|
|
454
435
|
/**
|
|
455
|
-
*
|
|
456
|
-
*
|
|
457
|
-
*
|
|
436
|
+
* Attach one user-provided evidence file to the current round. The file is
|
|
437
|
+
* referenced in place — never copied, moved or deleted — and the action does
|
|
438
|
+
* not advance the workflow: no model-visible message, no agent turn.
|
|
458
439
|
*/
|
|
459
|
-
function
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
});
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
async function openReviewMenu(ctx: ExtensionContext): Promise<void> {
|
|
469
|
-
if (!ensureWaiting(ctx)) return;
|
|
470
|
-
if (!ctx.hasUI) {
|
|
471
|
-
ctx.ui.notify(`/${COMMAND_MENU} requires an interactive UI; use /${COMMAND_DONE} or /${COMMAND_PROCEED} instead.`, "warning");
|
|
472
|
-
return;
|
|
440
|
+
async function attachEvidence(ctx: ExtensionContext, rawPath?: string, requestId: string | null = null): Promise<boolean> {
|
|
441
|
+
const session = activeSession(ctx);
|
|
442
|
+
if (!session) return false;
|
|
443
|
+
if (!ownsTurn(session)) {
|
|
444
|
+
ctx.ui.notify(`debug-mode: cannot attach evidence while the agent is working (stage: ${session.stage})`, "error");
|
|
445
|
+
return false;
|
|
473
446
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
447
|
+
let input = rawPath?.trim() ?? "";
|
|
448
|
+
if (!input) {
|
|
449
|
+
if (!ctx.hasUI) {
|
|
450
|
+
ctx.ui.notify(`Usage: /${COMMAND_EVIDENCE} <path>`, "error");
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
input = (await ctx.ui.input("Path to debug evidence file", "absolute or cwd-relative path")) ?? "";
|
|
454
|
+
if (!input.trim()) {
|
|
455
|
+
ctx.ui.notify("debug-mode: no evidence file path provided", "error");
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
477
458
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
} else if (choice === REVIEW_ADD_DETAILS) {
|
|
490
|
-
ctx.ui.setEditorText("/debug-note ");
|
|
491
|
-
ctx.ui.notify("Add reproduction details in the editor, then submit /debug-note.", "info");
|
|
492
|
-
} else if (choice === REVIEW_ABORT) {
|
|
493
|
-
await abortDebug(ctx);
|
|
459
|
+
const trimmed = input.trim();
|
|
460
|
+
const cwdRoot = path.resolve(ctx.cwd);
|
|
461
|
+
const resolved = path.resolve(cwdRoot, trimmed);
|
|
462
|
+
const outsideCwd = resolved !== cwdRoot && !resolved.startsWith(`${cwdRoot}${path.sep}`);
|
|
463
|
+
if (outsideCwd) {
|
|
464
|
+
if (!ctx.hasUI) {
|
|
465
|
+
ctx.ui.notify(
|
|
466
|
+
`debug-mode: ${resolved} is outside the session working directory and cannot be confirmed without a UI`,
|
|
467
|
+
"error",
|
|
468
|
+
);
|
|
469
|
+
return false;
|
|
494
470
|
}
|
|
495
|
-
|
|
496
|
-
|
|
471
|
+
const confirmed = await ctx.ui.confirm(
|
|
472
|
+
"Attach evidence outside the working directory?",
|
|
473
|
+
`Record ${resolved} as debug evidence? The file is referenced in place and never modified.`,
|
|
474
|
+
);
|
|
475
|
+
if (!confirmed) return false;
|
|
476
|
+
}
|
|
477
|
+
const validation = validateEvidenceArtifact(trimmed, ctx.cwd);
|
|
478
|
+
if (!validation.ok) {
|
|
479
|
+
ctx.ui.notify(`debug-mode: evidence rejected — ${validation.reason}`, "error");
|
|
480
|
+
return false;
|
|
497
481
|
}
|
|
482
|
+
if (!state.active) return false;
|
|
483
|
+
dispatch({ t: "attach_artifact", candidate: validation.artifact, requestId, now: Date.now() }, ctx);
|
|
484
|
+
return true;
|
|
498
485
|
}
|
|
499
486
|
|
|
500
487
|
// ============================== commands ==============================
|
|
@@ -504,7 +491,7 @@ export function registerDebugMode(pi: ExtensionAPI): void {
|
|
|
504
491
|
handler: async (args, ctx) => {
|
|
505
492
|
uiCtx = ctx;
|
|
506
493
|
if (state.active) {
|
|
507
|
-
ctx.ui.notify(`debug-mode: already active (use /${
|
|
494
|
+
ctx.ui.notify(`debug-mode: already active (use /${COMMAND_STATUS}, /${COMMAND_DONE}, /${COMMAND_PROCEED}, or /${COMMAND_ABORT})`, "error");
|
|
508
495
|
return;
|
|
509
496
|
}
|
|
510
497
|
const problem = args.trim();
|
|
@@ -528,31 +515,40 @@ export function registerDebugMode(pi: ExtensionAPI): void {
|
|
|
528
515
|
});
|
|
529
516
|
|
|
530
517
|
pi.registerCommand(COMMAND_PROCEED, {
|
|
531
|
-
description: "Continue with captured
|
|
532
|
-
handler: async (
|
|
518
|
+
description: "Continue with captured evidence and optional user details: /debug-proceed [details]",
|
|
519
|
+
handler: async (args, ctx) => {
|
|
533
520
|
uiCtx = ctx;
|
|
534
|
-
|
|
521
|
+
const details = args.trim();
|
|
522
|
+
await advanceDebug(ctx, details || undefined);
|
|
535
523
|
},
|
|
536
524
|
});
|
|
537
525
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
526
|
+
/**
|
|
527
|
+
* Parse `/debug-evidence [<request-id>] <path>`: when the first
|
|
528
|
+
* whitespace-delimited token names a pending current-round user_artifact
|
|
529
|
+
* request, link to it and treat the remainder (spaces intact) as the path;
|
|
530
|
+
* otherwise the whole argument is an unlinked path.
|
|
531
|
+
*/
|
|
532
|
+
function parseEvidenceArgument(args: string): { requestId: string | null; rawPath: string | undefined } {
|
|
533
|
+
const trimmed = args.trim();
|
|
534
|
+
if (!trimmed) return { requestId: null, rawPath: undefined };
|
|
535
|
+
const firstToken = trimmed.split(/\s+/, 1)[0];
|
|
536
|
+
const isPending =
|
|
537
|
+
state.active &&
|
|
538
|
+
pendingRequests(state).some(request => request.method === "user_artifact" && request.id === firstToken);
|
|
539
|
+
if (isPending) {
|
|
540
|
+
const rest = trimmed.slice(firstToken.length).trim();
|
|
541
|
+
return rest ? { requestId: firstToken, rawPath: rest } : { requestId: firstToken, rawPath: undefined };
|
|
542
|
+
}
|
|
543
|
+
return { requestId: null, rawPath: trimmed };
|
|
544
|
+
}
|
|
545
545
|
|
|
546
|
-
pi.registerCommand(
|
|
547
|
-
description: "
|
|
546
|
+
pi.registerCommand(COMMAND_EVIDENCE, {
|
|
547
|
+
description: "Attach one user-provided evidence file: /debug-evidence [<request-id>] <path> (no argument opens a path prompt)",
|
|
548
548
|
handler: async (args, ctx) => {
|
|
549
549
|
uiCtx = ctx;
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
ctx.ui.notify("Usage: /debug-note <reproduction details>", "error");
|
|
553
|
-
return;
|
|
554
|
-
}
|
|
555
|
-
await advanceDebug(ctx, details);
|
|
550
|
+
const { requestId, rawPath } = parseEvidenceArgument(args);
|
|
551
|
+
await attachEvidence(ctx, rawPath, requestId);
|
|
556
552
|
},
|
|
557
553
|
});
|
|
558
554
|
|
|
@@ -573,23 +569,40 @@ export function registerDebugMode(pi: ExtensionAPI): void {
|
|
|
573
569
|
return;
|
|
574
570
|
}
|
|
575
571
|
refreshLogCounts();
|
|
576
|
-
const scan = await syncLedger(
|
|
577
|
-
|
|
572
|
+
const scan = await syncLedger();
|
|
573
|
+
if (!state.active) return;
|
|
574
|
+
const session: DebugSession = state;
|
|
575
|
+
const round = currentRound(session);
|
|
576
|
+
const run = activeRunId(session);
|
|
577
|
+
const tallies = run ? summarizeHypotheses(readRunLines(run)) : [];
|
|
578
|
+
const requests = round.plan ?? [];
|
|
579
|
+
const pending = pendingRequests(session, round);
|
|
580
|
+
const unavailable = session.artifacts.filter(artifact => {
|
|
581
|
+
try {
|
|
582
|
+
const stats = fs.statSync(artifact.path);
|
|
583
|
+
fs.accessSync(artifact.path, fs.constants.R_OK);
|
|
584
|
+
return !stats.isFile();
|
|
585
|
+
} catch {
|
|
586
|
+
return true;
|
|
587
|
+
}
|
|
588
|
+
});
|
|
578
589
|
ctx.ui.notify(
|
|
579
|
-
`debug-mode:
|
|
590
|
+
`debug-mode: stage=${session.stage} round=${round.index} run=${run ?? "(none)"}\n` +
|
|
591
|
+
(round.openReason ? `${describeOpenReason(round.openReason, round.index)}\n` : "") +
|
|
580
592
|
`${describeLedger(scan)}\n` +
|
|
581
|
-
`logs: ${
|
|
593
|
+
`logs: ${session.runHistory.map(r => `${r}=${session.logCounts[r] ?? 0}`).join(", ") || "(none)"}\n` +
|
|
582
594
|
`this run by hypothesis: ${describeHypotheses(tallies)}\n` +
|
|
583
|
-
`current log file: ${logFileFor(
|
|
595
|
+
`current log file: ${logFileFor(session) ?? "(not initialized)"}\n` +
|
|
596
|
+
`evidence requests: ${requests.map(r => `${r.id}[${r.method}] ${r.title}`).join("; ") || "(none)"}\n` +
|
|
597
|
+
`pending: ${pending.map(r => r.id).join(", ") || "(none)"}\n` +
|
|
598
|
+
`observations: ${session.observations.map(o => o.id).join(", ") || "(none)"}\n` +
|
|
599
|
+
`artifacts: ${session.artifacts.map(a => a.id).join(", ") || "(none)"}` +
|
|
600
|
+
(unavailable.length > 0 ? `\nunavailable artifacts: ${unavailable.map(a => `${a.id} ${a.path}`).join(", ")}` : ""),
|
|
584
601
|
"info",
|
|
585
602
|
);
|
|
586
603
|
},
|
|
587
604
|
});
|
|
588
605
|
|
|
589
|
-
// ============================== tools ==============================
|
|
590
|
-
|
|
591
|
-
registerDebugTools(pi, { state, refreshLogCounts, readRunLines });
|
|
592
|
-
|
|
593
606
|
// ============================== lifecycle ==============================
|
|
594
607
|
|
|
595
608
|
pi.on("session_start", async (_event, ctx) => {
|
|
@@ -597,18 +610,24 @@ export function registerDebugMode(pi: ExtensionAPI): void {
|
|
|
597
610
|
const entries = ctx.sessionManager.getEntries();
|
|
598
611
|
const last = entries
|
|
599
612
|
.filter((e: { type: string; customType?: string }) => e.type === "custom" && e.customType === DEBUG_ENTRY)
|
|
600
|
-
.pop() as { data?:
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
if (!
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
613
|
+
.pop() as { data?: unknown } | undefined;
|
|
614
|
+
state = reviveState(last?.data);
|
|
615
|
+
if (state.active) {
|
|
616
|
+
if (!state.debugDir || !fs.existsSync(state.debugDir)) {
|
|
617
|
+
const debugDir = debugDirFor(ctx.cwd, ctx.sessionManager.getSessionId());
|
|
618
|
+
try {
|
|
619
|
+
fs.mkdirSync(debugDir, { recursive: true });
|
|
620
|
+
excludeDebugLogsFromGit(ctx.cwd);
|
|
621
|
+
state = { ...state, debugDir };
|
|
622
|
+
} catch (err) {
|
|
623
|
+
pi.logger.error("debug-mode: cannot restore log dir", { dir: debugDir, err });
|
|
624
|
+
}
|
|
625
|
+
}
|
|
608
626
|
const currentFile = logFileFor(state);
|
|
609
627
|
if (currentFile) {
|
|
610
628
|
try {
|
|
611
|
-
const
|
|
629
|
+
const run = activeRunId(state);
|
|
630
|
+
const legacyRunFile = state.debugDir && run ? path.join(state.debugDir, `${run}.jsonl`) : null;
|
|
612
631
|
if (!fs.existsSync(currentFile) && legacyRunFile && fs.existsSync(legacyRunFile)) {
|
|
613
632
|
fs.renameSync(legacyRunFile, currentFile);
|
|
614
633
|
}
|
|
@@ -618,18 +637,15 @@ export function registerDebugMode(pi: ExtensionAPI): void {
|
|
|
618
637
|
}
|
|
619
638
|
}
|
|
620
639
|
refreshLogCounts();
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
640
|
+
if (state.active) {
|
|
641
|
+
ctx.ui.notify(
|
|
642
|
+
`debug-mode resumed: stage=${state.stage} round=${currentRound(state).index}, log=${currentFile ?? "unavailable"}; evidence: ${evidenceSummary(state)}. Use /${COMMAND_STATUS}, /${COMMAND_EVIDENCE}, /${COMMAND_DONE}, /${COMMAND_PROCEED}, or /${COMMAND_ABORT}.`,
|
|
643
|
+
"info",
|
|
644
|
+
);
|
|
645
|
+
}
|
|
625
646
|
}
|
|
626
647
|
refreshUi();
|
|
627
648
|
watchLogFile();
|
|
628
|
-
if (state.active && state.phase === "waiting") scheduleReviewMenu(ctx);
|
|
629
|
-
});
|
|
630
|
-
|
|
631
|
-
pi.on("turn_start", async () => {
|
|
632
|
-
if (state.active) pi.appendEntry(DEBUG_ENTRY, { ...state });
|
|
633
649
|
});
|
|
634
650
|
|
|
635
651
|
pi.on("session_shutdown", async () => {
|