@siuver/omp-debug-mode 0.1.4 → 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 +13 -0
- package/README.md +31 -10
- package/package.json +1 -1
- package/src/debug-mode.ts +281 -375
- package/src/evidence.ts +9 -36
- package/src/gate.ts +59 -30
- package/src/machine.ts +334 -0
- package/src/methodology.ts +34 -8
- package/src/probes.ts +9 -9
- package/src/state.ts +233 -74
- package/src/tools.ts +39 -23
- package/src/ui.ts +43 -19
package/src/debug-mode.ts
CHANGED
|
@@ -2,8 +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 {
|
|
6
|
-
import {
|
|
5
|
+
import { describeEvidence, validateEvidenceArtifact } from "./evidence";
|
|
6
|
+
import { describeOpenReason } from "./gate";
|
|
7
7
|
import {
|
|
8
8
|
ACTIVE_LOG_FILE,
|
|
9
9
|
JsonlLineCounter,
|
|
@@ -12,30 +12,26 @@ import {
|
|
|
12
12
|
readJsonlLines,
|
|
13
13
|
summarizeHypotheses,
|
|
14
14
|
} from "./log-files";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
METHODOLOGY,
|
|
19
|
-
PROCEED_REMINDER,
|
|
20
|
-
buildFixedMessage,
|
|
21
|
-
buildProceedMessage,
|
|
22
|
-
buildStartMessage,
|
|
23
|
-
extractAssistantText,
|
|
24
|
-
extractReproductionSteps,
|
|
25
|
-
} from "./methodology";
|
|
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";
|
|
26
18
|
import {
|
|
19
|
+
type DebugSession,
|
|
20
|
+
type DebugState,
|
|
27
21
|
DEBUG_CONTEXT_TYPE,
|
|
28
22
|
DEBUG_ENTRY,
|
|
29
|
-
|
|
30
|
-
|
|
23
|
+
INACTIVE,
|
|
24
|
+
activeRunId,
|
|
31
25
|
blackboard,
|
|
32
26
|
compareRunIds,
|
|
27
|
+
currentRound,
|
|
33
28
|
evidenceSummary,
|
|
34
|
-
|
|
29
|
+
evidenceView,
|
|
30
|
+
hasNonProbeEvidence,
|
|
35
31
|
keepLatestCustomType,
|
|
36
32
|
logFileFor,
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
pendingRequests,
|
|
34
|
+
reviveState,
|
|
39
35
|
} from "./state";
|
|
40
36
|
import { registerDebugTools } from "./tools";
|
|
41
37
|
import { applyUi } from "./ui";
|
|
@@ -50,9 +46,9 @@ const COMMAND_STATUS = "debug-status";
|
|
|
50
46
|
|
|
51
47
|
/** Compact transcript lines for the prompts this extension injects. */
|
|
52
48
|
const MESSAGE_SUMMARIES: Record<string, string> = {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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",
|
|
56
52
|
};
|
|
57
53
|
|
|
58
54
|
interface DebugMessageDetails {
|
|
@@ -60,7 +56,7 @@ interface DebugMessageDetails {
|
|
|
60
56
|
}
|
|
61
57
|
|
|
62
58
|
export function registerDebugMode(pi: ExtensionAPI): void {
|
|
63
|
-
|
|
59
|
+
let state: DebugState = INACTIVE;
|
|
64
60
|
let uiCtx: ExtensionContext | null = null;
|
|
65
61
|
let watchedLogFile: string | null = null;
|
|
66
62
|
const lineCounter = new JsonlLineCounter();
|
|
@@ -73,79 +69,142 @@ export function registerDebugMode(pi: ExtensionAPI): void {
|
|
|
73
69
|
pi.registerMessageRenderer<DebugMessageDetails>(customType, renderer);
|
|
74
70
|
}
|
|
75
71
|
|
|
76
|
-
// ==============================
|
|
72
|
+
// ============================== state plumbing ==============================
|
|
77
73
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
+
}
|
|
89
143
|
}
|
|
90
144
|
}
|
|
91
145
|
|
|
146
|
+
function refreshUi(): void {
|
|
147
|
+
applyUi(uiCtx, state, currentLogCount);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ============================== log files ==============================
|
|
151
|
+
|
|
92
152
|
function readRunLines(run: string): string[] {
|
|
93
|
-
return readJsonlLines(logFileFor(state, run));
|
|
153
|
+
return state.active ? readJsonlLines(logFileFor(state, run)) : [];
|
|
94
154
|
}
|
|
95
155
|
|
|
156
|
+
/** Re-count every run on disk. Pure cache maintenance — never a decision. */
|
|
96
157
|
function refreshLogCounts(): void {
|
|
97
|
-
|
|
98
|
-
|
|
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) {
|
|
99
163
|
try {
|
|
100
|
-
for (const file of fs.readdirSync(
|
|
164
|
+
for (const file of fs.readdirSync(session.debugDir)) {
|
|
101
165
|
if (!file.endsWith(".jsonl")) continue;
|
|
102
166
|
if (file === ACTIVE_LOG_FILE) {
|
|
103
|
-
if (
|
|
167
|
+
if (active) runs.add(active);
|
|
104
168
|
} else {
|
|
105
169
|
runs.add(file.replace(/\.jsonl$/, ""));
|
|
106
170
|
}
|
|
107
171
|
}
|
|
108
172
|
} catch {}
|
|
109
173
|
}
|
|
110
|
-
|
|
111
|
-
|
|
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 });
|
|
112
177
|
}
|
|
113
178
|
|
|
114
179
|
/** Keep the ordered history complete when runs are discovered from disk. */
|
|
115
|
-
function
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
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);
|
|
120
185
|
completed.sort(compareRunIds);
|
|
121
|
-
|
|
186
|
+
return active ? [...completed, active] : completed;
|
|
122
187
|
}
|
|
123
188
|
|
|
124
189
|
function currentLogCount(): number {
|
|
125
190
|
refreshLogCounts();
|
|
126
|
-
|
|
191
|
+
if (!state.active) return 0;
|
|
192
|
+
const run = activeRunId(state);
|
|
193
|
+
return run ? (state.logCounts[run] ?? 0) : 0;
|
|
127
194
|
}
|
|
128
195
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
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 {
|
|
132
198
|
try {
|
|
133
|
-
prepareRunLog(
|
|
199
|
+
prepareRunLog(debugDir, previousRun);
|
|
134
200
|
} catch (err) {
|
|
135
201
|
pi.logger.error("debug-mode: cannot initialize run log", {
|
|
136
|
-
file: path.join(
|
|
202
|
+
file: path.join(debugDir, ACTIVE_LOG_FILE),
|
|
137
203
|
err,
|
|
138
204
|
});
|
|
139
205
|
return null;
|
|
140
206
|
}
|
|
141
|
-
|
|
142
|
-
state.runHistory.push(run);
|
|
143
|
-
state.logCounts[run] = 0;
|
|
144
|
-
return run;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function refreshUi(): void {
|
|
148
|
-
applyUi(uiCtx, state, currentLogCount);
|
|
207
|
+
return `run${round}-${Date.now().toString(36)}`;
|
|
149
208
|
}
|
|
150
209
|
|
|
151
210
|
/**
|
|
@@ -154,13 +213,13 @@ export function registerDebugMode(pi: ExtensionAPI): void {
|
|
|
154
213
|
* refresh the widget as observations land.
|
|
155
214
|
*/
|
|
156
215
|
function watchLogFile(): void {
|
|
157
|
-
const file = state.
|
|
216
|
+
const file = state.active && state.stage === "awaiting_evidence" ? logFileFor(state) : null;
|
|
158
217
|
if (file === watchedLogFile) return;
|
|
159
218
|
unwatchLogFile();
|
|
160
219
|
if (!file || !uiCtx?.hasUI) return;
|
|
161
220
|
try {
|
|
162
221
|
fs.watchFile(file, { interval: 1000 }, (curr, prev) => {
|
|
163
|
-
if (state.
|
|
222
|
+
if (!state.active || state.stage !== "awaiting_evidence") return;
|
|
164
223
|
if (curr.mtimeMs === prev.mtimeMs && curr.size === prev.size) return;
|
|
165
224
|
try {
|
|
166
225
|
refreshUi();
|
|
@@ -186,26 +245,35 @@ export function registerDebugMode(pi: ExtensionAPI): void {
|
|
|
186
245
|
|
|
187
246
|
// ============================== probe ledger ==============================
|
|
188
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
|
+
|
|
189
255
|
pi.on("tool_call", async (event, ctx) => {
|
|
190
256
|
if (!state.active) return;
|
|
191
257
|
if (event.toolName !== "edit" && event.toolName !== "write") return;
|
|
192
|
-
|
|
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);
|
|
193
260
|
});
|
|
194
261
|
|
|
195
262
|
// ============================== prompt injection ==============================
|
|
196
263
|
|
|
197
264
|
pi.on("before_agent_start", async () => {
|
|
198
|
-
if (!state.active ||
|
|
265
|
+
if (!state.active || state.stage === "awaiting_evidence") return;
|
|
199
266
|
// The blackboard claims to be ground truth, so reconcile it with disk first.
|
|
200
|
-
await syncLedger(
|
|
267
|
+
await syncLedger();
|
|
268
|
+
if (!state.active) return;
|
|
201
269
|
// Cleanup has no hypotheses left to form; the full methodology would only
|
|
202
270
|
// invite another round.
|
|
203
|
-
const contract = state.
|
|
271
|
+
const contract = state.stage === "cleaning_up" ? CLEANUP_CONTRACT : METHODOLOGY;
|
|
204
272
|
return {
|
|
205
273
|
message: {
|
|
206
274
|
customType: DEBUG_CONTEXT_TYPE,
|
|
207
|
-
content: `${blackboard(state, describeEvidence(state))}\n\n${contract}`,
|
|
208
|
-
|
|
275
|
+
content: `${blackboard(state, describeEvidence(evidenceView(state)))}\n\n${contract}`,
|
|
276
|
+
display: false,
|
|
209
277
|
},
|
|
210
278
|
};
|
|
211
279
|
});
|
|
@@ -218,304 +286,141 @@ export function registerDebugMode(pi: ExtensionAPI): void {
|
|
|
218
286
|
if (filtered.length !== event.messages.length) return { messages: filtered };
|
|
219
287
|
});
|
|
220
288
|
|
|
221
|
-
// ==============================
|
|
289
|
+
// ============================== turn lifecycle ==============================
|
|
222
290
|
|
|
223
|
-
pi.on("agent_start", async () => {
|
|
224
|
-
if (state.active)
|
|
291
|
+
pi.on("agent_start", async (_event, ctx) => {
|
|
292
|
+
if (state.active) dispatch({ t: "turn_started" }, ctx);
|
|
225
293
|
});
|
|
226
294
|
|
|
227
|
-
pi.on("message_end", async (event) => {
|
|
295
|
+
pi.on("message_end", async (event, ctx) => {
|
|
228
296
|
if (!state.active) return;
|
|
229
297
|
const msg = event.message as { role?: string; content?: unknown };
|
|
230
|
-
if (msg?.role
|
|
231
|
-
|
|
232
|
-
state.hasRoundContent = true;
|
|
233
|
-
const text = extractAssistantText(msg.content);
|
|
234
|
-
const steps = extractReproductionSteps(text);
|
|
235
|
-
if (steps.length > 0) state.reproductionSteps = steps;
|
|
236
|
-
if (state.phase === "round") {
|
|
237
|
-
const plan = parseEvidencePlan(text, state.round);
|
|
238
|
-
if (plan.found) {
|
|
239
|
-
replaceRoundEvidenceRequests(state, state.round, plan.valid ? plan.requests : []);
|
|
240
|
-
pi.appendEntry(DEBUG_ENTRY, { ...state });
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
298
|
+
if (msg?.role !== "assistant") return;
|
|
299
|
+
dispatch({ t: "assistant_message", text: extractAssistantText(msg.content) }, ctx);
|
|
244
300
|
});
|
|
245
301
|
|
|
246
302
|
pi.on("session_stop", async (_event, ctx) => {
|
|
247
|
-
if (!state.active) return;
|
|
248
|
-
if (state.
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if (state.phase !== "round" || !state.hasRoundContent) return;
|
|
255
|
-
const probesThisRound = state.probes.filter(p => p.round === state.round).length;
|
|
256
|
-
const hasEvidencePlan = state.evidenceRequests.some(request => request.round === state.round);
|
|
257
|
-
const decision = decideGate({
|
|
258
|
-
hasReproductionSteps: state.reproductionSteps.length > 0,
|
|
259
|
-
hasEvidencePlan,
|
|
260
|
-
probesThisRound,
|
|
261
|
-
nudgesUsed: state.gateNudges,
|
|
262
|
-
});
|
|
263
|
-
if (decision.kind === "stay") return;
|
|
264
|
-
if (decision.kind === "nudge") {
|
|
265
|
-
state.gateNudges += 1;
|
|
266
|
-
return { continue: true, additionalContext: decision.context };
|
|
267
|
-
}
|
|
268
|
-
enterGate(ctx, probesThisRound, decision.missingSteps, decision.missingEvidencePlan);
|
|
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 };
|
|
269
310
|
});
|
|
270
311
|
|
|
271
|
-
|
|
272
|
-
ctx: ExtensionContext,
|
|
273
|
-
probesThisRound: number,
|
|
274
|
-
missingSteps: boolean,
|
|
275
|
-
missingEvidencePlan: boolean,
|
|
276
|
-
): void {
|
|
277
|
-
state.phase = "waiting";
|
|
278
|
-
pi.appendEntry(DEBUG_ENTRY, { ...state });
|
|
279
|
-
refreshUi();
|
|
280
|
-
watchLogFile();
|
|
281
|
-
// Commands are the only interaction at the gate: the widget lists them,
|
|
282
|
-
// the user picks one. No menu is opened automatically.
|
|
283
|
-
const pending = pendingEvidenceRequests(state, state.round);
|
|
284
|
-
const hasValidPlan = state.evidenceRequests.some(request => request.round === state.round);
|
|
285
|
-
if (probesThisRound === 0 && !hasValidPlan) {
|
|
286
|
-
ctx.ui.notify(
|
|
287
|
-
`Debug round ${state.round} paused, but it added no probes and declared no evidence plan — this round cannot produce runtime evidence. Use /${COMMAND_PROCEED} to ask for instrumentation.`,
|
|
288
|
-
"warning",
|
|
289
|
-
);
|
|
290
|
-
} else if (missingSteps) {
|
|
291
|
-
ctx.ui.notify(
|
|
292
|
-
`Debug round ${state.round} paused without reproduction steps. Exercise the instrumented path, then ${PROCEED_REMINDER}`,
|
|
293
|
-
"warning",
|
|
294
|
-
);
|
|
295
|
-
} else if (pending.length > 0) {
|
|
296
|
-
ctx.ui.notify(
|
|
297
|
-
`Debug round ${state.round} paused. User evidence requested (${pending.length} pending: ${pending.map(r => r.id).join(", ")}) — attach via /${COMMAND_EVIDENCE} <request-id> <path>, then ${PROCEED_REMINDER}`,
|
|
298
|
-
"info",
|
|
299
|
-
);
|
|
300
|
-
} else if (missingEvidencePlan) {
|
|
301
|
-
ctx.ui.notify(
|
|
302
|
-
`Debug round ${state.round} paused. The gate is usable, but the next Proceed will ask the model to declare an evidence method.`,
|
|
303
|
-
"info",
|
|
304
|
-
);
|
|
305
|
-
} else {
|
|
306
|
-
ctx.ui.notify(`Debug round ${state.round} paused. Reproduce the bug, then ${PROCEED_REMINDER}`, "info");
|
|
307
|
-
}
|
|
308
|
-
}
|
|
312
|
+
registerDebugTools(pi, { getState: () => state, refreshLogCounts, readRunLines, syncLedger });
|
|
309
313
|
|
|
310
|
-
// ==============================
|
|
314
|
+
// ============================== command guards ==============================
|
|
311
315
|
|
|
312
|
-
function
|
|
313
|
-
if (!
|
|
314
|
-
ctx.ui.notify("debug-mode:
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
317
|
-
state.active = true;
|
|
318
|
-
state.phase = "round";
|
|
319
|
-
state.problem = problem;
|
|
320
|
-
state.round = 1;
|
|
321
|
-
state.probes = [];
|
|
322
|
-
state.runHistory = [];
|
|
323
|
-
state.logCounts = {};
|
|
324
|
-
state.hasRoundContent = false;
|
|
325
|
-
state.cleanupReady = false;
|
|
326
|
-
state.reproductionSteps = [];
|
|
327
|
-
state.gateNudges = 0;
|
|
328
|
-
state.evidenceRequests = [];
|
|
329
|
-
state.evidenceArtifacts = [];
|
|
330
|
-
state.evidenceObservations = [];
|
|
331
|
-
if (!newRun()) {
|
|
332
|
-
Object.assign(state, freshState());
|
|
333
|
-
ctx.ui.notify("debug-mode: could not initialize the run log file; debug mode was not started", "error");
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
refreshUi();
|
|
337
|
-
const logFile = logFileFor(state);
|
|
338
|
-
if (!logFile) {
|
|
339
|
-
Object.assign(state, freshState());
|
|
340
|
-
ctx.ui.notify("debug-mode: could not resolve the run log file; debug mode was not started", "error");
|
|
341
|
-
return;
|
|
316
|
+
function activeSession(ctx: ExtensionContext): DebugSession | null {
|
|
317
|
+
if (!state.active) {
|
|
318
|
+
ctx.ui.notify("debug-mode: not active", "error");
|
|
319
|
+
return null;
|
|
342
320
|
}
|
|
343
|
-
|
|
344
|
-
{
|
|
345
|
-
customType: "debug-mode-start",
|
|
346
|
-
content: buildStartMessage(problem, logFile),
|
|
347
|
-
display: true,
|
|
348
|
-
details: { summary: "debug mode started — hypotheses and instrumentation" } satisfies DebugMessageDetails,
|
|
349
|
-
},
|
|
350
|
-
{ triggerTurn: true },
|
|
351
|
-
);
|
|
321
|
+
return state;
|
|
352
322
|
}
|
|
353
323
|
|
|
354
|
-
function
|
|
355
|
-
|
|
356
|
-
|
|
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)) {
|
|
337
|
+
ctx.ui.notify(
|
|
338
|
+
`debug-mode: the agent still has round ${currentRound(session).index} (stage: ${session.stage}) — wait for it to stop.`,
|
|
339
|
+
"error",
|
|
340
|
+
);
|
|
357
341
|
return false;
|
|
358
342
|
}
|
|
359
|
-
if (
|
|
360
|
-
ctx.ui.
|
|
361
|
-
|
|
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?`,
|
|
347
|
+
);
|
|
348
|
+
if (!confirmed) return false;
|
|
362
349
|
}
|
|
363
350
|
return true;
|
|
364
351
|
}
|
|
365
352
|
|
|
366
|
-
|
|
367
|
-
if (!ensureWaiting(ctx)) return;
|
|
368
|
-
const logCount = currentLogCount();
|
|
369
|
-
const hasNonProbeEvidence = state.evidenceRequests.some(
|
|
370
|
-
request => request.round === state.round && request.method !== "runtime_probe",
|
|
371
|
-
);
|
|
372
|
-
if (logCount === 0 && !hasNonProbeEvidence && ctx.hasUI) {
|
|
373
|
-
const confirmed = await ctx.ui.confirm(
|
|
374
|
-
"Mark as fixed without runtime logs?",
|
|
375
|
-
"No runtime observations were captured for this round. Mark the problem as fixed anyway?",
|
|
376
|
-
);
|
|
377
|
-
if (!confirmed) return;
|
|
378
|
-
}
|
|
379
|
-
if (!state.active || state.phase !== "waiting") return;
|
|
353
|
+
// ============================== round transitions ==============================
|
|
380
354
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
}
|
|
355
|
+
function startDebug(ctx: ExtensionContext, problem: string): void {
|
|
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 });
|
|
362
|
+
ctx.ui.notify("debug-mode: could not create the debug log directory; debug mode was not started", "error");
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const runId = createRun(debugDir, 1, null);
|
|
366
|
+
if (!runId) {
|
|
367
|
+
ctx.ui.notify("debug-mode: could not initialize the run log file; debug mode was not started", "error");
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
dispatch({ t: "start", problem, debugDir, runId, logFile: path.join(debugDir, ACTIVE_LOG_FILE) }, ctx);
|
|
371
|
+
}
|
|
399
372
|
|
|
400
373
|
async function advanceDebug(ctx: ExtensionContext, userDetails?: string): Promise<void> {
|
|
401
|
-
if (!
|
|
374
|
+
if (!(await claimTurn(ctx, "Proceed from"))) return;
|
|
402
375
|
refreshLogCounts();
|
|
403
|
-
|
|
404
|
-
const
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
);
|
|
408
|
-
if (logCount === 0 && !userDetails && !hasNonProbeEvidence && ctx.hasUI) {
|
|
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) {
|
|
409
380
|
const confirmed = await ctx.ui.confirm(
|
|
410
381
|
"Proceed without runtime logs?",
|
|
411
382
|
"No runtime observations were captured for this round. Continue to log analysis anyway?",
|
|
412
383
|
);
|
|
413
384
|
if (!confirmed) return;
|
|
414
385
|
}
|
|
415
|
-
if (!state.active
|
|
416
|
-
|
|
417
|
-
// Persist optional /debug-proceed details as a batched user observation
|
|
418
|
-
// tied to every current-round user_report request.
|
|
419
|
-
if (userDetails && userDetails.trim().length > 0) {
|
|
420
|
-
const reportIds = state.evidenceRequests
|
|
421
|
-
.filter(request => request.round === state.round && request.method === "user_report")
|
|
422
|
-
.map(request => request.id);
|
|
423
|
-
const observation: EvidenceObservation = {
|
|
424
|
-
id: `observation-${Date.now().toString(36)}`,
|
|
425
|
-
requestIds: reportIds,
|
|
426
|
-
text: userDetails.trim(),
|
|
427
|
-
round: state.round,
|
|
428
|
-
addedAt: Date.now(),
|
|
429
|
-
};
|
|
430
|
-
state.evidenceObservations = [...state.evidenceObservations, observation];
|
|
431
|
-
}
|
|
432
|
-
const stillPending = pendingEvidenceRequests(state, state.round);
|
|
386
|
+
if (!state.active) return;
|
|
387
|
+
const stillPending = pendingRequests(state);
|
|
433
388
|
if (stillPending.length > 0 && ctx.hasUI) {
|
|
434
389
|
const confirmed = await ctx.ui.confirm(
|
|
435
390
|
"Proceed without all requested evidence?",
|
|
436
391
|
`${stillPending.length} evidence request(s) still pending (${stillPending.map(r => r.id).join(", ")}). Continue anyway?`,
|
|
437
392
|
);
|
|
438
|
-
if (!confirmed)
|
|
439
|
-
pi.appendEntry(DEBUG_ENTRY, { ...state });
|
|
440
|
-
refreshUi();
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
393
|
+
if (!confirmed) return;
|
|
443
394
|
}
|
|
395
|
+
if (!state.active || !ownsTurn(state) || !state.debugDir) return;
|
|
444
396
|
|
|
445
|
-
|
|
446
|
-
const
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
state.hasRoundContent = false;
|
|
451
|
-
state.reproductionSteps = [];
|
|
452
|
-
state.gateNudges = 0;
|
|
453
|
-
if (!newRun()) {
|
|
454
|
-
state.round = previousRound;
|
|
455
|
-
state.phase = "waiting";
|
|
456
|
-
state.reproductionSteps = previousSteps;
|
|
457
|
-
ctx.ui.notify("debug-mode: could not initialize the next run log file; staying at the reproduction gate", "error");
|
|
458
|
-
refreshUi();
|
|
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");
|
|
459
402
|
return;
|
|
460
403
|
}
|
|
461
|
-
|
|
462
|
-
refreshUi();
|
|
463
|
-
|
|
464
|
-
const summary = userDetails
|
|
465
|
-
? `proceed with user details — analyzing run ${run} (${logCount} entries)`
|
|
466
|
-
: `proceed — analyzing run ${run} (${logCount} entries)`;
|
|
467
|
-
pi.sendMessage(
|
|
468
|
-
{
|
|
469
|
-
customType: "debug-mode-proceed",
|
|
470
|
-
content: buildProceedMessage({
|
|
471
|
-
run,
|
|
472
|
-
logCount,
|
|
473
|
-
userDetails,
|
|
474
|
-
hypotheses,
|
|
475
|
-
evidenceSummary: evidenceSummary(state, previousRound),
|
|
476
|
-
}),
|
|
477
|
-
display: true,
|
|
478
|
-
details: { summary } satisfies DebugMessageDetails,
|
|
479
|
-
},
|
|
480
|
-
{ triggerTurn: true },
|
|
481
|
-
);
|
|
404
|
+
dispatch({ t: "proceed", runId, logCount, hypotheses, details: userDetails, now: Date.now() }, ctx);
|
|
482
405
|
}
|
|
483
406
|
|
|
484
|
-
async function
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
if (state.
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
pi.logger.warn("debug-mode: failed to remove debug dir", { err });
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
const scan = await syncLedger(state);
|
|
496
|
-
const probesLeft = [...scan.alive, ...scan.unknown];
|
|
497
|
-
Object.assign(state, freshState());
|
|
498
|
-
pi.appendEntry(DEBUG_ENTRY, { ...state });
|
|
499
|
-
refreshUi();
|
|
500
|
-
const resultLabel = outcome === "finished" ? "finished" : "aborted";
|
|
501
|
-
if (probesLeft.length > 0) {
|
|
502
|
-
ctx.ui.notify(
|
|
503
|
-
`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.`,
|
|
504
|
-
"warning",
|
|
505
|
-
);
|
|
506
|
-
} else {
|
|
507
|
-
ctx.ui.notify(
|
|
508
|
-
`Debug mode ${resultLabel}. Log files removed; applied fixes remain in the working diff for review.`,
|
|
509
|
-
"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?",
|
|
510
415
|
);
|
|
416
|
+
if (!confirmed) return;
|
|
511
417
|
}
|
|
418
|
+
if (!state.active || !ownsTurn(state)) return;
|
|
419
|
+
dispatch({ t: "mark_fixed" }, ctx);
|
|
512
420
|
}
|
|
513
421
|
|
|
514
422
|
async function abortDebug(ctx: ExtensionContext): Promise<void> {
|
|
515
|
-
if (!
|
|
516
|
-
ctx.ui.notify("debug-mode: not active", "error");
|
|
517
|
-
return;
|
|
518
|
-
}
|
|
423
|
+
if (!activeSession(ctx)) return;
|
|
519
424
|
if (ctx.hasUI) {
|
|
520
425
|
const confirmed = await ctx.ui.confirm(
|
|
521
426
|
"Abort debug mode?",
|
|
@@ -524,16 +429,21 @@ function enterGate(
|
|
|
524
429
|
if (!confirmed) return;
|
|
525
430
|
}
|
|
526
431
|
if (!state.active) return;
|
|
527
|
-
|
|
432
|
+
dispatch({ t: "abort" }, ctx);
|
|
528
433
|
}
|
|
529
434
|
|
|
530
435
|
/**
|
|
531
|
-
* Attach one user-provided evidence file to the current
|
|
532
|
-
*
|
|
533
|
-
*
|
|
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.
|
|
534
439
|
*/
|
|
535
440
|
async function attachEvidence(ctx: ExtensionContext, rawPath?: string, requestId: string | null = null): Promise<boolean> {
|
|
536
|
-
|
|
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;
|
|
446
|
+
}
|
|
537
447
|
let input = rawPath?.trim() ?? "";
|
|
538
448
|
if (!input) {
|
|
539
449
|
if (!ctx.hasUI) {
|
|
@@ -564,20 +474,15 @@ function enterGate(
|
|
|
564
474
|
);
|
|
565
475
|
if (!confirmed) return false;
|
|
566
476
|
}
|
|
567
|
-
const
|
|
568
|
-
if (
|
|
569
|
-
ctx.ui.notify(`debug-mode: evidence rejected — ${
|
|
477
|
+
const validation = validateEvidenceArtifact(trimmed, ctx.cwd);
|
|
478
|
+
if (!validation.ok) {
|
|
479
|
+
ctx.ui.notify(`debug-mode: evidence rejected — ${validation.reason}`, "error");
|
|
570
480
|
return false;
|
|
571
481
|
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
refreshUi();
|
|
575
|
-
ctx.ui.notify(
|
|
576
|
-
`debug-mode: attached ${result.artifact.id} → ${result.artifact.path} (${result.artifact.size} bytes)`,
|
|
577
|
-
"info",
|
|
578
|
-
);
|
|
482
|
+
if (!state.active) return false;
|
|
483
|
+
dispatch({ t: "attach_artifact", candidate: validation.artifact, requestId, now: Date.now() }, ctx);
|
|
579
484
|
return true;
|
|
580
|
-
}
|
|
485
|
+
}
|
|
581
486
|
|
|
582
487
|
// ============================== commands ==============================
|
|
583
488
|
|
|
@@ -628,9 +533,9 @@ function enterGate(
|
|
|
628
533
|
const trimmed = args.trim();
|
|
629
534
|
if (!trimmed) return { requestId: null, rawPath: undefined };
|
|
630
535
|
const firstToken = trimmed.split(/\s+/, 1)[0];
|
|
631
|
-
const isPending =
|
|
632
|
-
|
|
633
|
-
|
|
536
|
+
const isPending =
|
|
537
|
+
state.active &&
|
|
538
|
+
pendingRequests(state).some(request => request.method === "user_artifact" && request.id === firstToken);
|
|
634
539
|
if (isPending) {
|
|
635
540
|
const rest = trimmed.slice(firstToken.length).trim();
|
|
636
541
|
return rest ? { requestId: firstToken, rawPath: rest } : { requestId: firstToken, rawPath: undefined };
|
|
@@ -647,7 +552,6 @@ function enterGate(
|
|
|
647
552
|
},
|
|
648
553
|
});
|
|
649
554
|
|
|
650
|
-
|
|
651
555
|
pi.registerCommand(COMMAND_ABORT, {
|
|
652
556
|
description: "Abort debug mode: delete logs (fixes stay in the working diff)",
|
|
653
557
|
handler: async (_args, ctx) => {
|
|
@@ -665,11 +569,15 @@ function enterGate(
|
|
|
665
569
|
return;
|
|
666
570
|
}
|
|
667
571
|
refreshLogCounts();
|
|
668
|
-
const scan = await syncLedger(
|
|
669
|
-
|
|
670
|
-
const
|
|
671
|
-
const
|
|
672
|
-
const
|
|
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 => {
|
|
673
581
|
try {
|
|
674
582
|
const stats = fs.statSync(artifact.path);
|
|
675
583
|
fs.accessSync(artifact.path, fs.constants.R_OK);
|
|
@@ -679,25 +587,22 @@ function enterGate(
|
|
|
679
587
|
}
|
|
680
588
|
});
|
|
681
589
|
ctx.ui.notify(
|
|
682
|
-
`debug-mode:
|
|
590
|
+
`debug-mode: stage=${session.stage} round=${round.index} run=${run ?? "(none)"}\n` +
|
|
591
|
+
(round.openReason ? `${describeOpenReason(round.openReason, round.index)}\n` : "") +
|
|
683
592
|
`${describeLedger(scan)}\n` +
|
|
684
|
-
`logs: ${
|
|
593
|
+
`logs: ${session.runHistory.map(r => `${r}=${session.logCounts[r] ?? 0}`).join(", ") || "(none)"}\n` +
|
|
685
594
|
`this run by hypothesis: ${describeHypotheses(tallies)}\n` +
|
|
686
|
-
`current log file: ${logFileFor(
|
|
687
|
-
`evidence requests: ${
|
|
595
|
+
`current log file: ${logFileFor(session) ?? "(not initialized)"}\n` +
|
|
596
|
+
`evidence requests: ${requests.map(r => `${r.id}[${r.method}] ${r.title}`).join("; ") || "(none)"}\n` +
|
|
688
597
|
`pending: ${pending.map(r => r.id).join(", ") || "(none)"}\n` +
|
|
689
|
-
`observations: ${
|
|
690
|
-
`artifacts: ${
|
|
598
|
+
`observations: ${session.observations.map(o => o.id).join(", ") || "(none)"}\n` +
|
|
599
|
+
`artifacts: ${session.artifacts.map(a => a.id).join(", ") || "(none)"}` +
|
|
691
600
|
(unavailable.length > 0 ? `\nunavailable artifacts: ${unavailable.map(a => `${a.id} ${a.path}`).join(", ")}` : ""),
|
|
692
601
|
"info",
|
|
693
602
|
);
|
|
694
603
|
},
|
|
695
604
|
});
|
|
696
605
|
|
|
697
|
-
// ============================== tools ==============================
|
|
698
|
-
|
|
699
|
-
registerDebugTools(pi, { state, refreshLogCounts, readRunLines });
|
|
700
|
-
|
|
701
606
|
// ============================== lifecycle ==============================
|
|
702
607
|
|
|
703
608
|
pi.on("session_start", async (_event, ctx) => {
|
|
@@ -705,21 +610,24 @@ function enterGate(
|
|
|
705
610
|
const entries = ctx.sessionManager.getEntries();
|
|
706
611
|
const last = entries
|
|
707
612
|
.filter((e: { type: string; customType?: string }) => e.type === "custom" && e.customType === DEBUG_ENTRY)
|
|
708
|
-
.pop() as { data?:
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
if (!
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
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
|
+
}
|
|
719
626
|
const currentFile = logFileFor(state);
|
|
720
627
|
if (currentFile) {
|
|
721
628
|
try {
|
|
722
|
-
const
|
|
629
|
+
const run = activeRunId(state);
|
|
630
|
+
const legacyRunFile = state.debugDir && run ? path.join(state.debugDir, `${run}.jsonl`) : null;
|
|
723
631
|
if (!fs.existsSync(currentFile) && legacyRunFile && fs.existsSync(legacyRunFile)) {
|
|
724
632
|
fs.renameSync(legacyRunFile, currentFile);
|
|
725
633
|
}
|
|
@@ -729,19 +637,17 @@ function enterGate(
|
|
|
729
637
|
}
|
|
730
638
|
}
|
|
731
639
|
refreshLogCounts();
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
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
|
+
}
|
|
736
646
|
}
|
|
737
647
|
refreshUi();
|
|
738
648
|
watchLogFile();
|
|
739
649
|
});
|
|
740
650
|
|
|
741
|
-
pi.on("turn_start", async () => {
|
|
742
|
-
if (state.active) pi.appendEntry(DEBUG_ENTRY, { ...state });
|
|
743
|
-
});
|
|
744
|
-
|
|
745
651
|
pi.on("session_shutdown", async () => {
|
|
746
652
|
unwatchLogFile();
|
|
747
653
|
});
|