Skip to content

fix(debugger): keep a crashed app's console log instead of deleting it - #705

Draft
latekvo wants to merge 1 commit into
mainfrom
fix/debugger-logs-survive-crash
Draft

fix(debugger): keep a crashed app's console log instead of deleting it#705
latekvo wants to merge 1 commit into
mainfrom
fix/debugger-logs-survive-crash

Conversation

@latekvo

@latekvo latekvo commented Aug 3, 2026

Copy link
Copy Markdown
Member

Found while reviewing #610 and set aside as out of scope: pre-existing since the log registry landed in #30 (2026-03-18), untouched by that PR.

The defect

LogFileWriter.close() ends in fs.unlinkSync(this.filePath) (log-file-writer.ts:231), and both JS-runtime debugger blueprints call it from dispose. When the CDP socket drops - the app crashed or was force-quit - the blueprint emits terminated, the registry's cascade calls dispose, and the log file is unlinked. No tool call is involved. The socket dropping is sufficient.

Real tool-server over HTTP, mock Metro + CDP:

before crash: totalEntries=4  file=~/.argent/tmp/argent-logs-39841-1785765343514.log
  stat: 300 bytes          grep CRITICAL: 4 lines
--- terminate the CDP socket ---
AFTER crash:
  stat: No such file or directory     grep CRITICAL: 0 lines

That is the exact artifact the documented workflow tells you to read:

  • debugger-log-registry's description: "Returns the log file path... call this first for an overview, then read the returned file for details."
  • argent-metro-debugger/SKILL.md:85: "Logs are written to a flat log file on disk. Use the log-registry -> grep pattern", :90 "Search the file using Grep or Read"
  • references/failure-scenarios.md:10 is the one row that describes this exact situation - "The app may have crashed or been closed" - and it does not mention that the log file was just destroyed.

Calling the tool again does not recover it: the registry nulls the instance, so the in-memory clusters die with the writer and a fresh session is minted with a new path and totalEntries: 0 - which that tool's own description defines as "no log data has been captured yet". An agent reads that as "the app logged nothing", the opposite of the truth.

The fix

dispose keeps the file when the teardown was caused by the runtime dying, and still removes it on an explicit teardown. The discriminator has to live in the blueprint: every teardown funnels through registry._teardown -> instance.dispose(), which takes no cause argument. The disconnected handler is the only unexpected-death entry point for these two services - neither blueprint declares recoverable, so _recoverFailedServices can never dispose them.

Explicit teardowns that must keep unlinking, and do: index.ts:386 registry.dispose() on shutdown, flow-run.ts:642, stop-simulator-server.ts:61, stop-all-simulator-servers.ts:49. react-profiler-start.ts:165,170 also disposes the service, but both call sites are already guarded on the service being non-RUNNING or !cdp.isConnected(), i.e. the runtime is already dead - so no healthy session loses its logs there.

The cost, and why the pruner is in the same PR. ~/.argent/tmp has exactly one writer and no sweeper anywhere in the repo. Files already leak today whenever the tool-server is killed rather than shut down - this machine had 8 orphans, the oldest from Jul 29. Keeping the file on crash adds one orphan per crash, so the change has to carry its own cleanup or it trades a data-loss bug for an unbounded-growth bug. The writer now prunes argent-logs-*.log older than 24h when it opens a new one: age-based rather than delete-all because several tool-servers can run concurrently, and 24h far exceeds any debugger session, so a live writer's file is never a candidate. This also drains the pre-existing leak.

Verification

The repro, both directions, through the real HTTP entry point (identical script, two builds): on main the file is gone after the crash; with the fix stat still reports 300 bytes and all four CRITICAL pre-crash lines at the path the tool had already returned.

Discriminating test - test/metro/log-survives-crash.test.ts, real Registry + blueprint + both tools against a mock Metro/CDP:

crash case explicit-teardown case
unfixed source FAIL expected false to be true at the post-crash existsSync PASS
with fix PASS PASS

The second case pins the cost: disposeService must still remove the file.

The pruner has its own test, and all three mutations of it are killed:

mutation result
drop the pruneStaleLogs(dir) call 1 failed
drop the filename-pattern guard (prune any file) 1 failed
drop the age check (prune every matching file) 1 failed

Suite: full tool-server run 3087 passed / 6 failed vs. main's 3083 passed / 6 failed on the same box (+3 new tests, same pre-existing boot-device-hotboot failures, which are an unrelated env-isolation defect - see the follow-up PR). tsc --noEmit -p tsconfig.test.json clean, eslint --max-warnings 0 clean, prettier clean.

Known limitation

This makes the already-handed-out path readable across a crash. It does not help an agent that first calls debugger-log-registry after the crash: it still gets a fresh session with a new file and no signal that a previous one ended or where its logs are. Worth a follow-up; it needs a design decision about session identity rather than a lifecycle fix.

Touches log-file-writer.ts, which #647 also edits (different region - that PR adds source attribution).

`LogFileWriter.close()` unlinks the log file, and both JS-runtime debugger
blueprints call it from `dispose`. When the CDP socket drops the registry's
terminated cascade disposes the service with no tool call involved, so the
console output captured before the crash is destroyed at the moment a
developer would read it — at the very path `debugger-log-registry` already
handed them:

    before crash: totalEntries=4  file=~/.argent/tmp/argent-logs-39841-*.log
      stat: 300 bytes   grep CRITICAL: 4 lines
    --- terminate the CDP socket ---
      stat: No such file or directory   grep CRITICAL: 0 lines

The skills prescribe exactly this sequence — "returns the log file path …
then read the returned file for details" — and the failure-scenarios row for
"the app may have crashed" says nothing about the logs being gone. Calling
the tool again mints a fresh session reporting `totalEntries: 0`, which its
own description defines as "no log data has been captured yet".

`dispose` now keeps the file when the teardown was caused by the runtime
dying, and still removes it on an explicit teardown. Since a kept file has
no owner left to clean it up — and a tool-server killed outright never
closed its writer either — the writer prunes log files older than a day when
it opens a new one. Age-based, because several tool-servers can run at once.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant