Files
platform/todo/known-bugs/background-job-logs-appeared-inside-an-open.md
T
pastilhasandClaude Opus 5 f16aa87d8b the code editor collides with itself: two panels, one model, two buffers
The "two Monaco editors sharing a model" elimination asked which OTHER
component mounts Monaco and never asked whether the code editor collides with
itself. It does — the panel is addable to a dashboard twice, and useEditorState
keeps files in a plain useState, so two panels hold two independent buffers for
one globally-keyed model.

Verified against @monaco-editor/react@4.7.0's built source rather than inferred.
Model lookup is getModel(uri) || createModel(value), so the second mount adopts
the first panel's buffer and ignores its own freshly-read value. The value-sync
effect then overwrites the shared model with executeEdits. And the flag that
suppresses onChange during that write is per-instance, so one panel's
programmatic write fires the OTHER panel's onChange — which CodeEditor answers
with setContent(path, value). The other panel's buffer becomes this one's
content and goes dirty with no keystroke: dirty tab, content nobody typed, disk
untouched, fixed by reopening. Exactly the reported shape.

Unmount also disposes the shared model (keepCurrentModel defaults false), and
key={activeFile.path} means that fires on every tab switch, not only on panel
close — so one panel can dispose a model another is still displaying.

Not yet a closed case: nothing in the app writes job output into
useEditorState, so this produces the right shape but does not explain where the
log text comes from. No fix shipped — the two candidates (per-panel model URIs
vs sharing the state to match the shared model) differ in whether Monaco's
TypeScript resolution keeps working, and that is the owner's call. Trap stays
armed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: f7a62308-0977-4cf4-aa29-c020853fd420
2026-08-25 07:16:22 +00:00

83 lines
5.7 KiB
Markdown

---
id: known-bugs/background-job-logs-appeared-inside-an-open
title: 'Background job logs appeared INSIDE an open file in the code editor'
theme: known-bugs
status: open
created: 2026-08-16
date_source: stated
order: 1
---
**Background job logs appeared INSIDE an open file in the code editor.** Reported 2026-08-16 by the
owner, on the production host, running as a member with a `bun dev` background job streaming. The
editor had a file open; the runner's log output was rendered into that file's buffer. **It was never
saved** — the file on disk was intact, and the editor was not marked dirty as far as the report goes.
So this is a DISPLAY-layer mixing, not a write path: the bytes reached a buffer that only exists in
the browser.
Not reproduced, not diagnosed, and deliberately not guessed at further than this. What is worth
recording is where the two streams could meet, because both were added in the same week and they are
the only two things writing text into a panel:
- `useTaskDetail` polls `GET /chat/tasks/:id` every 2s while a job runs and returns `{kind:'log', text}`.
`CodeSurface` renders it — and `CodeSurface` is shared with the editor's own surfaces.
- The editor keys open files by PATH (`useEditorState`), while the task panel keys by TASK ID, and
several panel-scoped stores in between are keyed by `panelId` through `useGlobal` /
`usePanelMemory`. A key collision between those namespaces would produce exactly this: one panel's
text arriving in another's buffer, with no server involvement and nothing persisted.
**Screenshot 2026-08-16 answers the first question and kills two theories.** The tab carries a DIRTY
dot, and the log lines have Monaco line numbers continuing the file's own sequence (…180, then eight
lines of dotenv/POSTGRES_URL output, then 189 `</Link>`). So the MODEL was mutated — this is the
editor's content state, not its render, and the earlier "the file was okay" means it was simply never
saved. Note the splice is mid-file, not a replacement: JSX text and log text share line 180
(`Forgot?--env-file=.env --watch src/server.tsx`).
Ruled out by that: `CodeSurface` renders with shiki, not Monaco, so the two cannot share an editor
instance. And the app's only other Monaco mount (`FileViewer/renderers/EditorRenderer.tsx`) passes no
`path`, so its model URI is `inmemory://model/N` and cannot collide with the editor's `file:///…`.
So the pollution arrives as TEXT through the editor's own content path — `openFile`, `setContent`, or
the end-of-turn refresh that re-reads every clean file. That last one is the prime suspect: it is the
only thing that writes a buffer without a keystroke, it runs exactly when a background job is
producing output, and it lands its result by path.
**Narrowed 2026-08-16, and a trap is now set.** `reloadFile` sets `originalContent` and clears
`isDirty`, so the end-of-turn refresh CANNOT produce the dirty dot in the screenshot — the refresh
path is out. Nothing in the app injects text into the page either: no `execCommand('insertText')`,
no synthetic keyboard events, no clipboard writes near the editor. That leaves Monaco's own change
event, which means it looks like a paste or a programmatic `executeEdits`.
**`docs/editor-buffer-corruption.md` is the procedure — read that when someone reports this.** A
trap is set: `reportSuspiciousEdit` in CodeEditor.tsx records any insertion over 200 characters
arriving while the editor is NOT focused, with the stack at that moment, into
`localStorage['officer:editor-buffer-trap']` (last five). It persists rather than only logging
because this arrives as a report hours later, when the console is long gone — the question becomes
"paste this key" instead of "what did you see?". Delete all of it once the cause is known.
One thing to rule out first, because it is mine and it is recent: `2b70a570` gave `<Editor>` a real
`path`, so models are now keyed by file URI globally rather than being per-mount `inmemory://`. Two
panels open on the same file now share one model. That should be an improvement, but it is a change
in exactly the layer that turned out to be involved.
**2026-08-25 — that suspicion was right, and the "ruled out" row above was wrong.** The elimination asked
which OTHER component mounts Monaco and never asked whether the code editor collides with **itself**. It
does: the panel is addable to a dashboard twice, and `useEditorState` keeps `files` in a plain `useState`,
so two panels hold two independent buffers for one globally-keyed model. Verified in
`@monaco-editor/react@4.7.0`'s source: model lookup is `getModel(uri) || createModel(value)` so the second
mount's value is ignored, the value-sync effect then overwrites the shared model with `executeEdits`, and
**the flag that suppresses `onChange` during that write is per-instance** — so one panel's programmatic
write fires the OTHER panel's `onChange`. `CodeEditor` responds with `setContent(path, value)`: the other
panel's buffer becomes this one's content and goes dirty with no keystroke. Unmount also disposes the
shared model (`keepCurrentModel` defaults false), and `key={activeFile.path}` means that happens on every
tab switch, not just on panel close.
That reproduces the symptom's exact shape — dirty tab, content nobody typed, disk clean, fixed by
reopening. It does **not** explain how job output becomes one of those buffers, and nothing in the app
writes job output into `useEditorState`. So the trap stays armed; it is still the only thing that can name
where the text came from. Full write-up in `docs/editor-buffer-corruption.md`.
Worth noting because it is the reason to look at all: an editor that can show content the file does
not contain is a correctness problem even when it never writes — the next person's instinct will be to
save the file to "fix" it.