@siuver/omp-debug-mode 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1 - 2026-08-20
4
+
5
+ - Replaced the localhost HTTP collector with direct JSONL file appends from instrumented runtime code.
6
+ - The injected prompt now supplies the exact absolute log file and explicitly forbids POST, sockets, and other network transports.
7
+ - Updated log analysis, status reporting, and session resume to read JSONL directly; active probes use a stable `current.jsonl` path while completed rounds are archived by run id.
8
+
3
9
  ## 0.1.0 - 2026-08-20
4
10
 
5
11
  - Initial npm release of the Cursor Debug Mode replica for OMP.
package/README.md CHANGED
@@ -17,20 +17,30 @@ The plugin makes the agent form hypotheses, add temporary runtime probes, attemp
17
17
  ## Workflow
18
18
 
19
19
  1. Run `/debug-mode <problem description>`.
20
- 2. The agent investigates, records hypotheses, inserts minimal probes marked with `@omp-probe <id>`, and attempts a fix.
21
- 3. When the agent stops, reproduce the issue in the real application.
22
- 4. Run `/debug-done fixed` if the issue is resolved, or `/debug-done proceed` to analyze evidence and continue.
20
+ 2. The agent investigates, records hypotheses, inserts minimal probes marked with `@omp-probe <id>`, and attempts a fix. Each probe appends its runtime observation directly to the exact JSONL file provided in the injected prompt.
21
+ 3. When the agent stops, reproduce the issue in the real application so the instrumented code writes its observations.
22
+ 4. Run `/debug-done fixed` if the issue is resolved, or `/debug-done proceed` to make the agent read the completed run's JSONL file, analyze the evidence, and continue.
23
23
  5. On success, the agent removes all probes, verifies the probe ledger is empty, and summarizes the root cause and fix.
24
24
 
25
25
  The extension also provides the read-only `get_debug_logs` and `list_debug_probes` tools so the agent can inspect runtime evidence and verify cleanup.
26
26
 
27
27
  ## Runtime Data
28
28
 
29
- During an active session, the plugin starts a local HTTP server on `127.0.0.1:7842`. Instrumentation posts `{ "probe": "...", "data": ... }` to `/log`, and the plugin appends the entries to `<project>/.omp/debug/<run>.jsonl`.
29
+ At the start of each round, the plugin creates an absolute log path:
30
30
 
31
- The server only listens on localhost. Debug data may still contain values captured from your application, so review probe payloads before reproducing sensitive workflows. Finishing or aborting debug mode removes the debug log directory. Applied fixes remain in the working tree for review.
31
+ ```text
32
+ <project>/.omp/debug/current.jsonl
33
+ ```
34
+
35
+ That exact stable path is injected into the Agent prompt. The prompt requires every runtime probe to use the target environment's native file append API and write one compact JSON object plus a newline using this schema:
36
+
37
+ ```json
38
+ {"probe":"player-state","ts":1787193600000,"data":{"isGrounded":false}}
39
+ ```
40
+
41
+ Probes append directly to `current.jsonl`; they must not overwrite it, and they should flush and close the file promptly rather than retaining an exclusive handle. The stable filename means probes retained across rounds continue writing to the correct place without being rewritten just to change a path. The mechanism does not use HTTP, localhost, sockets, or any other network transport, so environments such as the Unity Editor can use their normal filesystem APIs. The target process still needs permission to access the displayed absolute path.
32
42
 
33
- Debug state is recorded in the OMP session so an active workflow can resume after reopening the session. Port `7842` must be available for probes to report data.
43
+ When you run `/debug-done proceed`, the plugin archives the completed file as `<run-id>.jsonl`, creates an empty `current.jsonl` for the next reproduction, and makes the Agent analyze the archived evidence through `get_debug_logs`. `/debug-status` shows the current absolute file path and log count.
34
44
 
35
45
  ## Install
36
46
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siuver/omp-debug-mode",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "description": "A Cursor Debug Mode replica for evidence-driven, human-in-the-loop debugging in oh-my-pi.",
6
6
  "license": "MIT",
@@ -0,0 +1,46 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+
4
+ export const ACTIVE_LOG_FILE = "current.jsonl";
5
+
6
+ export function resolveRunLogFile(
7
+ debugDir: string | null,
8
+ run: string | null,
9
+ activeRun: string | null,
10
+ ): string | null {
11
+ if (!debugDir || !run) return null;
12
+ return path.join(debugDir, run === activeRun ? ACTIVE_LOG_FILE : `${run}.jsonl`);
13
+ }
14
+
15
+ export function readJsonlLines(file: string | null): string[] {
16
+ if (!file) return [];
17
+ try {
18
+ return fs
19
+ .readFileSync(file, "utf8")
20
+ .split(/\r?\n/)
21
+ .filter(line => line.trim().length > 0);
22
+ } catch {
23
+ return [];
24
+ }
25
+ }
26
+
27
+ export function prepareRunLog(debugDir: string, previousRun: string | null): string {
28
+ const activeFile = path.join(debugDir, ACTIVE_LOG_FILE);
29
+ const archivedFile = previousRun ? path.join(debugDir, `${previousRun}.jsonl`) : null;
30
+
31
+ try {
32
+ if (archivedFile) {
33
+ if (fs.existsSync(activeFile)) fs.copyFileSync(activeFile, archivedFile);
34
+ else fs.writeFileSync(archivedFile, "", { flag: "a" });
35
+ }
36
+ fs.writeFileSync(activeFile, "", { flag: "w" });
37
+ return activeFile;
38
+ } catch (error) {
39
+ if (archivedFile) {
40
+ try {
41
+ fs.rmSync(archivedFile, { force: true });
42
+ } catch {}
43
+ }
44
+ throw error;
45
+ }
46
+ }
package/src/main.ts CHANGED
@@ -8,23 +8,20 @@
8
8
  * → /debug-done fixed → agent removes probes + summarizes → teardown (logs deleted)
9
9
  * → /debug-done proceed → agent analyzes captured logs, re-instruments + fixes → loop
10
10
  *
11
- * Components:
12
- * - Local log server (Bun.serve, 127.0.0.1:7842) probes POST {probe, data};
13
- * logs append to <cwd>/.omp/debug/<run>.jsonl
11
+ * - Stable JSONL file at <cwd>/.omp/debug/current.jsonl — runtime probes append
12
+ * observations directly with the target environment's native file APIs
14
13
  * - Probe ledger: tool_call interception on edit/write records @omp-probe ids;
15
14
  * ground truth is rescanned from disk (checkLedger)
16
15
  * - Tools: get_debug_logs, list_debug_probes
17
16
  * - Commands: /debug-mode, /debug-status, /debug-done fixed|proceed, /debug-abort
18
17
  */
19
18
  import type { ExtensionAPI, ExtensionContext } from "@oh-my-pi/pi-coding-agent";
20
- import type { Server } from "bun";
21
- import type { WriteStream } from "node:fs";
22
19
  import * as fs from "node:fs";
23
20
  import * as path from "node:path";
21
+ import { ACTIVE_LOG_FILE, prepareRunLog, readJsonlLines, resolveRunLogFile } from "./log-files";
24
22
 
25
23
  const DEBUG_ENTRY = "com.omp.debug-mode.state";
26
24
  const PROBE_MARK = /@omp-probe\s+([A-Za-z0-9_-]+)/g;
27
- const DEFAULT_PORT = 7842;
28
25
 
29
26
  type Phase = "idle" | "round" | "waiting" | "cleanup";
30
27
 
@@ -47,12 +44,6 @@ interface DebugState {
47
44
  cleanupReady: boolean;
48
45
  }
49
46
 
50
- interface LogEntry {
51
- run: string;
52
- probe: string;
53
- ts: number;
54
- data: unknown;
55
- }
56
47
 
57
48
  function freshState(): DebugState {
58
49
  return {
@@ -76,6 +67,10 @@ function probeIdsIn(text: unknown): string[] {
76
67
  return ids;
77
68
  }
78
69
 
70
+ function logFileFor(s: DebugState, run = s.runId): string | null {
71
+ return resolveRunLogFile(s.debugDir, run, s.runId);
72
+ }
73
+
79
74
  function blackboard(s: DebugState): string {
80
75
  const probes = s.probes.map(p => `${p.id} (${p.file}, round ${p.round})`).join("\n ") || "(none)";
81
76
  const counts = Object.entries(s.logCounts)
@@ -90,7 +85,8 @@ ${s.problem}
90
85
  Deployed probes (ground truth, maintained by the extension):
91
86
  ${probes}
92
87
 
93
- Log endpoint for probes: POST JSON {probe, data} to http://127.0.0.1:${DEFAULT_PORT}/log
88
+ Current run log file (absolute path): ${logFileFor(s) ?? "(not initialized)"}
89
+ Runtime probes MUST append JSONL directly to that exact file using the target runtime's native file APIs.
94
90
  Probe code MUST carry the marker comment \`// @omp-probe <id>\` adjacent to it.
95
91
  Logs by run: ${counts}
96
92
  Current run id: ${s.runId ?? "(not started)"}`;
@@ -101,9 +97,14 @@ const METHODOLOGY = `\
101
97
  Each round consists of, in order:
102
98
  1. Restate hypotheses (mark each: pending / ruled-out / confirmed via runtime evidence).
103
99
  2. Update instrumentation: remove probes that yielded no information, add probes that
104
- discriminate between remaining hypotheses. Each probe is one minimal statement sending
105
- JSON via HTTP POST to the log endpoint, with the marker comment \`// @omp-probe <id>\`
106
- adjacent (language-appropriate; keep the block small and easy to remove).
100
+ discriminate between remaining hypotheses. Each probe must use the target runtime's
101
+ native file append API to append exactly one compact JSON object plus a newline to the
102
+ exact absolute log file shown above. Use the schema {"probe":"<id>","ts":<epoch-ms>,
103
+ "data":<JSON-serializable-observation>}. Append; never overwrite or truncate. Open,
104
+ append, flush, and close promptly for each observation; do not retain an exclusive file
105
+ handle across the reproduction gate. Do not use HTTP, POST, localhost, sockets, or any
106
+ network transport. Escape the path correctly for the target language. Keep the marker
107
+ comment \`// @omp-probe <id>\` adjacent to the code.
107
108
  3. ATTEMPT A FIX for your leading hypothesis in the same round. Instrumentation without
108
109
  a fix is an incomplete round — the loop only advances when you fix something.
109
110
  4. End the round by writing concise reproduction steps for the user (exact commands or
@@ -115,80 +116,58 @@ tool that the ledger is empty, then summarize root cause and the final fix.`;
115
116
  export default function debugModeExtension(pi: ExtensionAPI) {
116
117
  const z = pi.zod;
117
118
  const state: DebugState = freshState();
118
- let server: Server | null = null;
119
- let logBuffer: LogEntry[] = [];
120
- let pendingWrite: WriteStream | null = null;
121
119
  let uiCtx: ExtensionContext | null = null;
122
120
 
123
- // ============================== log server ==============================
121
+ // ============================== log files ==============================
124
122
 
125
- function startServer(ctx: ExtensionContext, port: number): boolean {
126
- if (server) return true;
127
- const dir = path.join(ctx.cwd, ".omp", "debug");
123
+ function initializeLogDirectory(ctx: ExtensionContext): boolean {
124
+ const dir = path.resolve(ctx.cwd, ".omp", "debug");
128
125
  try {
129
126
  fs.mkdirSync(dir, { recursive: true });
130
- } catch (err) {
131
- pi.logger.error("debug-mode: cannot create log dir", { dir, err });
132
- return false;
133
- }
134
- state.debugDir = dir;
135
- logBuffer = [];
136
- try {
137
- server = Bun.serve({
138
- port,
139
- hostname: "127.0.0.1",
140
- idleTimeout: 5,
141
- async fetch(req) {
142
- const url = new URL(req.url);
143
- if (req.method === "POST" && url.pathname === "/log") {
144
- try {
145
- const body = (await req.json()) as { probe?: unknown; data?: unknown };
146
- if (typeof body.probe !== "string") return new Response("bad probe", { status: 400 });
147
- const entry: LogEntry = {
148
- run: state.runId ?? "orphan",
149
- probe: body.probe,
150
- ts: Date.now(),
151
- data: body.data ?? null,
152
- };
153
- logBuffer.push(entry);
154
- state.logCounts[entry.run] = (state.logCounts[entry.run] ?? 0) + 1;
155
- pendingWrite?.write(JSON.stringify(entry) + "\n");
156
- refreshUi();
157
- return new Response("ok");
158
- } catch {
159
- return new Response("bad json", { status: 400 });
160
- }
161
- }
162
- if (req.method === "GET" && url.pathname === "/health") {
163
- return new Response("ok");
164
- }
165
- return new Response("not found", { status: 404 });
166
- },
167
- });
127
+ state.debugDir = dir;
168
128
  return true;
169
129
  } catch (err) {
170
- pi.logger.error("debug-mode: log server failed to start", { err });
171
- server = null;
130
+ pi.logger.error("debug-mode: cannot create log dir", { dir, err });
131
+ state.debugDir = null;
172
132
  return false;
173
133
  }
174
134
  }
175
135
 
176
- function stopServer(): void {
177
- pendingWrite?.end();
178
- pendingWrite = null;
179
- server?.stop(true);
180
- server = null;
136
+ function readRunLines(run: string): string[] {
137
+ return readJsonlLines(logFileFor(state, run));
138
+ }
139
+
140
+ function refreshLogCounts(): void {
141
+ const runs = new Set(Object.keys(state.logCounts));
142
+ if (state.debugDir) {
143
+ try {
144
+ for (const file of fs.readdirSync(state.debugDir)) {
145
+ if (!file.endsWith(".jsonl")) continue;
146
+ if (file === ACTIVE_LOG_FILE) {
147
+ if (state.runId) runs.add(state.runId);
148
+ } else {
149
+ runs.add(file.replace(/\.jsonl$/, ""));
150
+ }
151
+ }
152
+ } catch {}
153
+ }
154
+ for (const run of runs) state.logCounts[run] = readRunLines(run).length;
181
155
  }
182
156
 
183
- function newRun(): string {
184
- pendingWrite?.end();
185
- pendingWrite = null;
157
+ function newRun(): string | null {
158
+ if (!state.debugDir) return null;
186
159
  const run = `run${state.round}-${Date.now().toString(36)}`;
160
+ try {
161
+ prepareRunLog(state.debugDir, state.runId);
162
+ } catch (err) {
163
+ pi.logger.error("debug-mode: cannot initialize run log", {
164
+ file: path.join(state.debugDir, ACTIVE_LOG_FILE),
165
+ err,
166
+ });
167
+ return null;
168
+ }
187
169
  state.runId = run;
188
170
  state.logCounts[run] = 0;
189
- if (state.debugDir) {
190
- pendingWrite = fs.createWriteStream(path.join(state.debugDir, `${run}.jsonl`), { flags: "a" });
191
- }
192
171
  return run;
193
172
  }
194
173
 
@@ -247,6 +226,7 @@ export default function debugModeExtension(pi: ExtensionAPI) {
247
226
  ctx.ui.setStatus("debug-mode", ctx.ui.theme.fg("warning", label));
248
227
  const lines: string[] = [];
249
228
  if (state.phase === "waiting") {
229
+ refreshLogCounts();
250
230
  const n = state.runId ? (state.logCounts[state.runId] ?? 0) : 0;
251
231
  lines.push(ctx.ui.theme.fg("accent", "reproduce the bug, then: /debug-done fixed | proceed"));
252
232
  lines.push(ctx.ui.theme.fg("dim", `run ${state.runId} — ${n} log entries`));
@@ -293,7 +273,7 @@ export default function debugModeExtension(pi: ExtensionAPI) {
293
273
  pi.on("session_stop", async (_event, ctx) => {
294
274
  if (!state.active) return;
295
275
  if (state.phase === "cleanup") {
296
- // cleanup turn settled teardown (fixes kept, logs + server removed)
276
+ // Cleanup turn settled: keep fixes and remove the temporary logs.
297
277
  if (state.cleanupReady) await teardown(ctx, true);
298
278
  return;
299
279
  }
@@ -324,6 +304,10 @@ export default function debugModeExtension(pi: ExtensionAPI) {
324
304
  });
325
305
 
326
306
  function startDebug(ctx: ExtensionContext, problem: string): void {
307
+ if (!initializeLogDirectory(ctx)) {
308
+ ctx.ui.notify("debug-mode: could not create the debug log directory; debug mode was not started", "error");
309
+ return;
310
+ }
327
311
  state.active = true;
328
312
  state.phase = "round";
329
313
  state.problem = problem;
@@ -331,15 +315,16 @@ export default function debugModeExtension(pi: ExtensionAPI) {
331
315
  state.probes = [];
332
316
  state.logCounts = {};
333
317
  state.hasRoundContent = false;
334
- if (!startServer(ctx, DEFAULT_PORT)) {
335
- ctx.ui.notify("debug-mode: log server failed to start; probes will have nowhere to report", "error");
318
+ if (!newRun()) {
319
+ Object.assign(state, freshState());
320
+ ctx.ui.notify("debug-mode: could not initialize the run log file; debug mode was not started", "error");
321
+ return;
336
322
  }
337
- newRun();
338
323
  refreshUi();
339
324
  pi.sendMessage(
340
325
  {
341
326
  customType: "debug-mode-start",
342
- content: `Starting debug mode. Problem report:\n\n${problem}\n\nBegin round 1: explore, hypothesize, instrument, and attempt a fix. Then give reproduction steps and stop.`,
327
+ content: `Starting debug mode. Problem report:\n\n${problem}\n\nBegin round 1: explore, hypothesize, instrument with direct file writes, and attempt a fix. Then give reproduction steps and stop.`,
343
328
  display: true,
344
329
  },
345
330
  { triggerTurn: true },
@@ -355,6 +340,7 @@ export default function debugModeExtension(pi: ExtensionAPI) {
355
340
  ctx.ui.notify(`debug-mode: not waiting for reproduction (phase: ${state.phase})`, "error");
356
341
  return;
357
342
  }
343
+ refreshLogCounts();
358
344
  if (verdict === "fixed") {
359
345
  state.phase = "cleanup";
360
346
  state.cleanupReady = false;
@@ -375,10 +361,17 @@ export default function debugModeExtension(pi: ExtensionAPI) {
375
361
  } else {
376
362
  const run = state.runId ?? "(none)";
377
363
  const n = state.logCounts[run] ?? 0;
364
+ const previousRound = state.round;
378
365
  state.round += 1;
379
366
  state.phase = "round";
380
367
  state.hasRoundContent = false;
381
- newRun();
368
+ if (!newRun()) {
369
+ state.round = previousRound;
370
+ state.phase = "waiting";
371
+ ctx.ui.notify("debug-mode: could not initialize the next run log file; staying at the reproduction gate", "error");
372
+ refreshUi();
373
+ return;
374
+ }
382
375
  refreshUi();
383
376
  pi.sendMessage(
384
377
  {
@@ -386,7 +379,7 @@ export default function debugModeExtension(pi: ExtensionAPI) {
386
379
  content:
387
380
  `User chose PROCEED — the fix did not resolve it (run ${run} captured ${n} log entries).\n` +
388
381
  (n === 0
389
- ? "No logs were captured: the probes may not have executed (wrong code path, not rebuilt, or server unreachable) — treat that as a signal.\n"
382
+ ? "No logs were captured: the instrumented code path may not have executed, the build may be stale, the path may be wrong, or the file append may have failed — treat that as a signal.\n"
390
383
  : "") +
391
384
  "Read the logs with get_debug_logs, rule out / strengthen hypotheses, re-instrument, attempt the next fix, give reproduction steps, and stop.",
392
385
  display: true,
@@ -404,9 +397,9 @@ export default function debugModeExtension(pi: ExtensionAPI) {
404
397
  pi.logger.warn("debug-mode: failed to remove debug dir", { err });
405
398
  }
406
399
  }
407
- stopServer();
408
400
  const probesLeft = await checkLedger();
409
401
  Object.assign(state, freshState());
402
+ pi.appendEntry(DEBUG_ENTRY, { ...state });
410
403
  refreshUi();
411
404
  if (probesLeft.length > 0) {
412
405
  ctx.ui.notify(
@@ -463,12 +456,14 @@ export default function debugModeExtension(pi: ExtensionAPI) {
463
456
  ctx.ui.notify("debug-mode: idle", "info");
464
457
  return;
465
458
  }
459
+ refreshLogCounts();
466
460
  const alive = await checkLedger();
467
461
  ctx.ui.notify(
468
462
  `debug-mode: phase=${state.phase} round=${state.round} run=${state.runId}\n` +
469
463
  `probes (ledger ${state.probes.length}, alive ${alive.length}):\n` +
470
464
  (alive.map(p => ` ${p.id} — ${p.file}`).join("\n") || " (none)") +
471
- `\nlogs: ${Object.entries(state.logCounts).map(([r, n]) => `${r}=${n}`).join(", ") || "(none)"}`,
465
+ `\nlogs: ${Object.entries(state.logCounts).map(([r, n]) => `${r}=${n}`).join(", ") || "(none)"}` +
466
+ `\ncurrent log file: ${logFileFor(state) ?? "(not initialized)"}`,
472
467
  "info",
473
468
  );
474
469
  },
@@ -480,7 +475,7 @@ export default function debugModeExtension(pi: ExtensionAPI) {
480
475
  name: "get_debug_logs",
481
476
  label: "Get Debug Logs",
482
477
  description:
483
- "Read logs captured by debug-mode probes. Each entry: {run, probe, ts, data}. Returns JSONL text. Call with previous=true to analyze the last completed reproduction run.",
478
+ "Read JSONL observations appended directly by debug-mode runtime probes. Each entry: {probe, ts, data}. Call with previous=true to analyze the last completed reproduction run.",
484
479
  parameters: z.object({
485
480
  run: z.string().optional().describe("Run id filter (default: current run)"),
486
481
  probe: z.string().optional().describe("Probe id filter"),
@@ -488,33 +483,42 @@ export default function debugModeExtension(pi: ExtensionAPI) {
488
483
  }),
489
484
  approval: "read",
490
485
  async execute(_toolCallId, params) {
486
+ refreshLogCounts();
491
487
  const runs = Object.keys(state.logCounts);
492
488
  let run = params.run;
493
- if (!run && params.previous) run = runs[runs.length - 2];
494
- if (!run) run = state.runId ?? undefined;
495
- // disk is authoritative (survives resume); memory buffer is fallback
496
- let text = "";
497
- if (state.debugDir && run) {
498
- try {
499
- text = await Bun.file(path.join(state.debugDir, `${run}.jsonl`)).text();
500
- } catch {
501
- text = "";
489
+ if (!run && params.previous) {
490
+ run = runs[runs.length - 2];
491
+ if (!run) {
492
+ return {
493
+ content: [{ type: "text", text: "(no completed previous debug run is available)" }],
494
+ details: { run: null, file: null, count: 0 },
495
+ };
502
496
  }
503
497
  }
504
- if (!text) {
505
- text = logBuffer
506
- .filter(e => (!run || e.run === run) && (!params.probe || e.probe === params.probe))
507
- .map(e => JSON.stringify(e))
508
- .join("\n");
498
+ if (!run) run = state.runId ?? undefined;
499
+
500
+ let lines = run ? readRunLines(run) : [];
501
+ if (params.probe) {
502
+ lines = lines.filter(line => {
503
+ try {
504
+ const entry = JSON.parse(line) as { probe?: unknown };
505
+ return entry.probe === params.probe;
506
+ } catch {
507
+ return false;
508
+ }
509
+ });
509
510
  }
511
+ const text = lines.join("\n");
510
512
  return {
511
513
  content: [
512
514
  {
513
515
  type: "text",
514
- text: text || "(no logs captured — probes did not report; wrong code path, stale build, or server unreachable)",
516
+ text:
517
+ text ||
518
+ "(no logs captured — the instrumented path may not have executed, the build may be stale, the path may be wrong, or the file append may have failed)",
515
519
  },
516
520
  ],
517
- details: { run: run ?? null, count: text.split("\n").filter(Boolean).length },
521
+ details: { run: run ?? null, file: run ? logFileFor(state, run) : null, count: lines.length },
518
522
  };
519
523
  },
520
524
  });
@@ -553,29 +557,22 @@ export default function debugModeExtension(pi: ExtensionAPI) {
553
557
  .pop() as { data?: DebugState } | undefined;
554
558
  if (last?.data?.active) {
555
559
  Object.assign(state, last.data);
556
- if (state.debugDir && startServer(ctx, DEFAULT_PORT)) {
557
- // replay disk logs for analysis continuity across resume
558
- if (fs.existsSync(state.debugDir)) {
559
- for (const f of fs.readdirSync(state.debugDir)) {
560
- if (!f.endsWith(".jsonl")) continue;
561
- const run = f.replace(/\.jsonl$/, "");
562
- try {
563
- const lines = (await Bun.file(path.join(state.debugDir!, f)).text()).split("\n").filter(Boolean);
564
- state.logCounts[run] = lines.length;
565
- for (const l of lines) {
566
- try {
567
- logBuffer.push(JSON.parse(l) as LogEntry);
568
- } catch {}
569
- }
570
- } catch {}
560
+ if (!state.debugDir || !fs.existsSync(state.debugDir)) initializeLogDirectory(ctx);
561
+ const currentFile = logFileFor(state);
562
+ if (currentFile) {
563
+ try {
564
+ const legacyRunFile = state.debugDir && state.runId ? path.join(state.debugDir, `${state.runId}.jsonl`) : null;
565
+ if (!fs.existsSync(currentFile) && legacyRunFile && fs.existsSync(legacyRunFile)) {
566
+ fs.renameSync(legacyRunFile, currentFile);
571
567
  }
572
- }
573
- if (state.runId) {
574
- pendingWrite = fs.createWriteStream(path.join(state.debugDir, `${state.runId}.jsonl`), { flags: "a" });
568
+ fs.writeFileSync(currentFile, "", { flag: "a" });
569
+ } catch (err) {
570
+ pi.logger.error("debug-mode: cannot restore run log", { file: currentFile, err });
575
571
  }
576
572
  }
573
+ refreshLogCounts();
577
574
  ctx.ui.notify(
578
- `debug-mode resumed: phase=${state.phase} round=${state.round}. Use /debug-status, /debug-done fixed|proceed, or /debug-abort.`,
575
+ `debug-mode resumed: phase=${state.phase} round=${state.round}, log=${currentFile ?? "unavailable"}. Use /debug-status, /debug-done fixed|proceed, or /debug-abort.`,
579
576
  "info",
580
577
  );
581
578
  }
@@ -586,9 +583,6 @@ export default function debugModeExtension(pi: ExtensionAPI) {
586
583
  if (state.active) pi.appendEntry(DEBUG_ENTRY, { ...state });
587
584
  });
588
585
 
589
- pi.on("session_shutdown", async () => {
590
- stopServer();
591
- });
592
586
 
593
587
  pi.setLabel("Debug Mode");
594
588
  }