chore: keep issue 824 investigation reports out of the PR

Remove the 21 mission reports from the tracked change set at the user's request. Keep their working copies and private evidence locally, and publish review findings, measurements and acceptance limitations through the pull request description and discussion instead of permanent repository documents.

No production, test, fixture, build or runtime input is changed by this commit. The completed browser cycle remains red and the existing independent reviews keep their original source HEAD attribution; this documentation-only cleanup does not constitute acceptance or resolve issue 824.
This commit is contained in:
Pascal André 2026-10-02 01:29:37 +02:00
parent f6a18d00bd
commit acac9be29a
No known key found for this signature in database
21 changed files with 0 additions and 3729 deletions

View file

@ -1,121 +0,0 @@
# Issue #824: native compaction renderer diagnosis
## Scope
This investigation covers the UI event path only. It does not exercise session
pruning, the shared daemon, provider traffic, Electron/Linux, or the remote
Windows backend. The fixture uses the real `SessionView`, native event
dispatcher, OpenCode Solid reducer, CodeNomad message store, and virtualized
transcript. Native client calls and `/api` traffic are isolated fakes.
The red reproduction is:
```powershell
$env:TEMP = 'C:\Users\Admin\AppData\Local\Temp\opencode'
$env:TMP = $env:TEMP
node --import tsx --test --test-concurrency=1 packages/ui/tests/browser/compaction-responsiveness.test.ts
```
## Confirmed behavior
A compaction for an inactive session still performs synchronous work in the
single renderer that owns the active session:
1. `SSEManager.handleEvent()` calls `handleInstanceInvalidation()` for every
native event.
2. `handleInstanceInvalidation()` calls `applyOpenCodeDataEvent()` before it
checks whether the session is active for visible projection.
3. `applyOpenCodeDataEvent()` creates/retains a per-session OpenCode Solid data
reducer and emits every `session.compaction.delta` into it.
4. The bundled `@opencode/client` reducer finds the running compaction and
appends every fragment to its `summary` in a Solid `produce()` update.
5. CodeNomad delays only `projectOpenCodeMessages()` to 250 ms. That avoids
repeatedly normalizing/hydrating the visible transcript, but it does not
coalesce the upstream reducer work or callback scheduling.
The active transcript stayed byte-for-byte unchanged in the fixture. The
timeline and virtualizer were therefore not the cause of the measured stall.
No `message.list` request occurred during any burst. The one `session.active`
call per sample comes from the synthetic terminal `compaction.failed` cleanup,
not from delta delivery.
### October 1 isolated Chromium sample
Each fragment was 1 KiB. The fixture schedules a zero-delay active-session
timer before dispatch and records browser Long Tasks. The final run reported:
| Delta events | Dispatch | Active timer delay | Long tasks |
| ---: | ---: | ---: | ---: |
| 4,096 | 14.1 ms | 14.2 ms | 0 |
| 8,192 | 21.9 ms | 22.0 ms | 0 |
| 16,384 | 42.6 ms | 42.8 ms | 0 |
| 32,768 | 82.5 ms | 82.6 ms | 83 ms |
CDP sampling attributed named self time primarily to
`applyOpenCodeDataEvent` (36.3 ms), OpenCode/Solid store access/update paths,
and `projectMessages` (11.8 ms). A large `(program)` bucket remained
unattributed by the sampling profiler. The near-linear count scaling points to
per-event reducer/callback overhead rather than a whole-transcript DOM redraw
or a superlinear summary concatenation in this corpus.
Two preceding runs also failed red at 32,768 fragments, with 80-85 ms timer
delay and one 82-85 ms Long Task. A 16,384-fragment run varied around the Long
Task boundary (approximately 42-70 ms), so the regression deliberately uses
the larger corpus and asserts behavior, not a comparative performance gain.
## Interpretation and limits
This is a confirmed renderer scalability defect: inactive compaction content
is reduced synchronously even though it cannot be displayed. It provides a
mechanism for cross-session UI interference and demonstrates delayed active
input/timers in the real UI stack.
It is not yet proof that normal EventSource delivery produces one callback
containing 32,768 events. The current browser transport invokes `onBatch` with
one event, while the reproduction deliberately presents a backlog directly to
the dispatcher to make callback work deterministic. Real network task
scheduling may interleave user input. The corpus is also 32 MiB of aggregate
summary fragments, larger than many compactions. Therefore this result must
not be presented as the sole cause of the reported multi-second freeze.
The test does not reproduce the daemon-side symptom where other native
sessions stop progressing, nor the Linux Electron to Windows HTTP persistence
after restart. Those require separate service/transport evidence. Native
compaction and CodeNomad's pruning RPC are unrelated paths here.
Current V2 UI events are `session.compaction.started/delta/ended/failed` plus
other granular `session.*` events. No current renderer handler for legacy
`session.updated`, `message.updated`, or `message.part.updated` was found.
## Smallest recommended product patch
Coalesce `session.compaction.delta` **before** the OpenCode Solid reducer, keyed
by instance and session, instead of only coalescing the later CodeNomad message
projection:
- append fragments to a plain pending accumulator;
- preserve an immediate revision fence on the first arrival so in-flight
native message reads cannot publish stale content;
- flush at most once per 250 ms to the reducer and visible projection;
- cancel the pending aggregate on `ended` because that event carries the
authoritative final text, and discard it on `failed`;
- clear pending aggregates on instance disposal/reconnect;
- do not materialize an inactive, unloaded transcript reducer solely for
compaction deltas. A later activation should hydrate authoritatively.
The regression should retain this two-session browser fixture and add exact
event-count instrumentation: thousands of inactive deltas must produce a
bounded number of reducer emissions/projections, zero message-list reads, no
active-transcript mutation, and no Long Task. Existing request-generation,
revert, pruning, rotation, and terminal-authority tests must remain green.
## Files added for reproduction
- `packages/ui/tests/browser/compaction-responsiveness.test.ts`
- `packages/ui/tests/browser/fixtures/compaction-responsiveness.tsx`
The initial timing-only test has since been replaced by deterministic reducer
and projection counters in `compaction-responsiveness.test.ts`. It is red on
the diagnosed implementation and green with the buffering correction. See
`issue-824-compaction-ui-fix.md` for the current gates and their limits. No shared
daemon, user profile, external network, or desktop restart is used.

View file

@ -1,484 +0,0 @@
# Issue #824 — freeze investigation
Mission baseline: `366830f6` (`origin/dev`, 2026-10-01). Integration worktree:
`D:\CodeNomad\.codenomad\worktrees\issue-824-performance`, branch
`fix/issue-824-performance`. Confirmed corrections are integrated and independently
reviewed, but final acceptance is not green. On 2026-10-02 the user authorized
committing/pushing the mission changes and opening a draft PR to `dev` for review.
Publication does not imply acceptance, merge, installation, deployment or a shared
daemon restart.
## Scenarios and evidence policy
- Issue report: Electron Linux `0.20.0-dev-20260930-47f9e43e`, OpenCode 2.0.19,
remote Windows backend over HTTP. Submitting a prompt often locks the UI;
reopening the client and reconnecting to the same server/session does not help.
- Additional user report: native compaction makes the UI and other sessions
unresponsive, despite earlier optimization work.
- Distinguish renderer CPU, HTTP connection occupancy, backend event-loop work,
shared native/plugin work and provider waiting. Compaction and technical-content
pruning are different operations.
- Use synthetic databases, loopback fixtures and separately launched native
runtimes. Never test against the user's OpenCode database or mutate their
sessions. A local fixture reproduction is not proof of the reporter's exact
root cause or Linux/Electron qualification.
## Parallel investigations
1. Prior audit continuity and broader unaddressed performance risks.
2. Compaction events, transcript hydration and renderer responsiveness.
3. HTTP/SSE, prompt admission and shared backend responsiveness.
4. Coordinator: plugin history/index reads and event-loop fairness.
The previous audit's reports remain in the separate `audit-resume-integration`
worktree. Its changes must be checked against this baseline before reuse.
## Confirmed: structural-index batches monopolize their event loop
`packages/server/src/opencode/session-pruning/outline-index.ts` originally read
512 checkpoint headers synchronously with `.all()`, then projected assistant JSON
for 128 rows before yielding. A row count is not a time budget: tool output size
varies considerably, and SQLite evaluates nested assistant JSON synchronously.
The routine is invoked by the bundled plugin's `outline` RPC.
### Reproduction
`scripts/profile-outline-responsiveness.mts` creates only an in-memory SQLite
database. It runs the real structural-index reader with 512 assistant messages,
each holding one 1 MiB textual tool result. The heartbeat is a separate
`setImmediate` callback on the same loop. Its maximum observed gap includes
synchronous SQL/JS work and host scheduling, not remote-client paint latency.
```powershell
node --import tsx scripts/profile-outline-responsiveness.mts 512 1048576 --baseline
node --import tsx scripts/profile-outline-responsiveness.mts 512 1048576
```
`--baseline` bundles the exact pre-mission `366830f6` reader without replacing
working-tree files. Seeding and module compilation are outside the measured scan.
The stress corpus is deliberately large (~512 MiB of payload); this is not an
estimate of a typical session or the reporter's data size.
First baseline run: maximum gaps **542 / 522 / 720 ms**, total scans
1717 / 1969 / 2209 ms, six heartbeat turns each. A subsequent immutable-source
baseline run observed **615 / 755 / 712 ms**, totals 2066 / 2409 / 2273 ms.
Initial modified run: maximum gaps **13 / 14 / 14 ms**, totals
1829 / 2680 / 2225 ms, 199 / 262 / 224 turns. Windows, Node/SQLite; uncontrolled
concurrent host work. These observations demonstrate improved cooperation, not
reduced total CPU or an application frame-rate claim. No wall-clock CI threshold.
### Correction and regression contract
Iterate the same checkpoint headers and retain the exact digest/page protocol.
Yield during both header reads and JSON projection when the slice has consumed
8 ms or 128 rows, and check cancellation between rows. Preserve read snapshots,
ownership, staged-undo visibility, known checkpoints and response bounds.
`outline-responsiveness.test.ts` uses real SQLite with a view that charges a
deterministic simulated cost per payload access. It schedules an actual pending
event-loop cancellation in the header or projection phase. The original header
regression failed after **896 payload accesses** before cancellation could run.
Both new tests pass after the patch and assert the read transaction is released.
The existing nine navigation tests also pass; server typecheck passes.
A single SQLite step remains synchronous and cannot be interrupted by this
cooperative budget. This patch does not move native storage work to another
thread, reduce all JSON parsing, or establish that this defect alone causes #824.
Native plugin qualification and independent review are tracked separately.
## Confirmed: subscriber failures cross feature/connection boundaries
The prior audit found these defects, but both source blobs were unchanged in
`366830f6`. They were reproduced again against the current mission baseline.
- Browser `ServerEvents` dispatch stopped at the first throwing callback. A
wildcard subscriber could prevent the real SSEManager and typed subscribers
from receiving valid native events; open/status callback failures could prevent
reconciliation or abort restart. Three browser regressions failed before the
patch; all four (including normal FIFO/unsubscribe) pass after per-callback
exception isolation. Faults are diagnosed and never replayed.
- `OpenCodeSharedService.invalidateAfterStream` invalidated the shared client in
`finally`, including on subscriber-local abort, iterator return/throw or an
already-aborted subscription. Four regressions failed with the real SDK and
loopback HTTP/SSE source. All seven now pass: local lifetime changes leave the
healthy source/client alone, whereas genuine upstream EOF/transport errors
still invalidate the originating generation. Late cleanup cannot invalidate a
replacement; obsolete iterators cannot publish queued events. The fourteen
existing shared-service tests also pass.
Commands:
```powershell
node --import tsx --test packages/ui/tests/browser/event-subscriber-isolation.test.ts
node --import tsx --test packages/server/src/workspaces/subscriber-lifetime.test.ts packages/server/src/workspaces/opencode-service.test.ts
```
The complete private native fixture also passed on OpenCode 2.0.21 with the
production shared-service wrapper: abort/return/throw retain the same client,
relay shutdown leaves the independent subscriber usable, slow-recipient FIFO
remains correct, and proxy/history/navigation/pruning checks pass. Retained
fixture: `C:\Users\Admin\AppData\Local\Temp\opencode\codenomad-pruning-native-Atf4fG`.
The 48 adjacent relay/SSE/proxy/navigation unit tests pass as well. No user
daemon was stopped or restarted.
These are functional isolation fixes, not measured CPU speedups, and no
production callback failure from the reporter has been captured. They prevent
one feature/consumer's failure from disturbing unrelated consumers; they do not
isolate synchronous CPU-heavy callbacks into different threads.
Independent event-boundary review approved these corrections without a
functional blocker, but reproduced a diagnostic gap: a deferred Solid memo
failure can occur at the final shared-batch flush, after callback-level catches.
The transport then mislabeled it as invalid JSON. A small follow-up separates
JSON decoding from event dispatch in `event-source-handlers.ts`. Its new unit
and real-SSE/browser-derived-error tests failed before the patch; all nine focused
tests now pass. The same lot's native/typed handlers receive each event once,
and subsequent events continue. This does not isolate every reactive computation
or change shared batching. Final UI review will recheck it with compaction work.
Residual limits from `issue-824-event-review.md`: these diagnostics use the
existing `sse` debug logger (not enabled by default); a concurrent local abort and
physical source failure has not been deterministically reproduced. No speculative
connection-invalidation change or new logging pipeline is introduced for them.
## Confirmed: orphaned HTTP preflight observers
The real Fastify/loopback proxy reproduction in `issue-824-transport` held the
native session-ownership read for prompt and compact requests. Closing the HTTP
client did not cancel those reads: listeners were installed only after preflight.
An independent session still responded before the held reads were released.
This proves an orphaned-request defect, not a global server CPU freeze or why a
real native read stalls in the reporter's case.
The dedicated correction (`d9598e42`, following red diagnostic `1ece148c`)
installs per-request cancellation at entry, propagates it to supported reads,
retires the local observer of uncancellable shared work, and fences late
continuations before environment writes or forwarding. It never treats ordinary
POST body completion as disconnection, adds no new execution timeout and never
interrupts/replays an admitted native mutation. Twelve real HTTP tests and three
scope tests passed in the source worktree and again after integration, along with
65 adjacent proxy tests and server typecheck. Independent review accepts the patch;
see `issue-824-http-admission-fix.md`.
## Confirmed: HTTP password derivation blocks unrelated work
The prior audit's synchronous-scrypt concern was reproduced with real
Fastify/AuthManager/auth routes and a private v1 auth file. In five baseline runs,
a witness endpoint could not run until all eight invalid logins or four password
changes finished. The async-scrypt correction admits the witness while those
operations are pending. Descriptive five-process medians: login witness
**168.63 → 3.37 ms**, password-change witness **82.43 → 0.80 ms**. Timing is not
the CI assertion: the tests require witness progress before the burst completes.
Hash format/parameters and constant-time comparison are retained; password
changes are serialized and recover after persistence failure. The CLI/env
override hashes synchronously only at startup, before HTTP serving. The shared
libuv pool remains a finite resource, and there is no claim of reduced password
CPU or a fix for compaction. Eight focused tests passed repeatedly in the source
worktree. Integration added a deterministic regression for an old login crossing
a password change: the initial async patch returned 200 after the new secret was
stored. The corrected verifier checks that its auth record remains authoritative
after the derivation; it returns 401 without a cookie or replay. All nine auth
tests and server typecheck pass after integration. Independent review accepts it;
see `issue-824-auth-responsiveness.md`.
## Confirmed: nested task tools remount on every delta
The prior audit's task-renderer correction was requalified against the identical
`366830f6` source blob and integrated with a minimal +29/-18 diff. It retains
stable child-tool keys/shells while reading their current payload. Two baseline
regressions fail on shell identity and stale Markdown; 63 adjacent browser tests
pass after correction, covering disclosure, nested scroller state, in-place
revision, removal, copy, errors/retry and tool images.
Current bounded A/B (30 samples per variant, instrumentation off): median
main-thread duration **353.39 → 22.72 ms**. Instrumented controls show 80 tool-shell
removals/reinsertions per delta becoming zero. The 80 clones/reloads remain; no
claim is made about desktop frame rate or eliminating all task-render costs.
See `issue-824-render-recovery.md` for artifacts, failed setup runs excluded from
statistics, and source-drift caveats. Final combined validation must use a frozen
composition; the prior measurements do not establish that the whole concurrent
worktree was unchanged. Independent combined UI review confirmed a membership
defect: an authoritative page removing one tool part from a retained message
leaves a phantom key/count, potentially hiding a later replacement. The renderer
correction therefore was not accepted for final qualification. The follow-up now
reuses the existing bounded structural scanner at each session revision, retaining
the prior ordered key list when unchanged. Keyed `For` preserves retained shells
through removal/reordering. Five new regressions fail on the preimage and all
68 adjacent browser scenarios pass naturally after correction, including exact
membership/count, tool-to-text identity changes and truncation shrink. The scan
still caps at 10,000 units and 201 tool discoveries/200 displayed tools; scanning
IDs/types on every revision is the explicit correctness tradeoff. Zero remounts
per delta remain instrumentally verified, but prior CPU A/B figures do not
qualify this new postimage. Independent re-review now closes M1 with no residual
blocker: 13 renderer and nine copy/image cases pass freshly and naturally, with
zero remounts per delta. See
`issue-824-task-membership-fix.md` and `issue-824-ui-review.md`, finding M1.
## Confirmed: compaction payload reduction precedes visible throttling
The existing 250 ms throttle limited only visible projection; every native
fragment still entered the reactive SDK reducer, even for an inactive session.
The correction aggregates by instance/session before that reducer, with a timer
started by the first chunk (later chunks do not postpone it). Incoming fragments
still advance revision/read fences immediately. Same-session ordering and SDK
page boundaries flush pending text; terminal authority, reconnect, deletion,
revert, pruning and disposal cancel obsolete buffers. Never-loaded inactive
sessions keep authority markers rather than materializing summary payloads.
The acceptance tests now use exact emission counts and text, not the original
artificial 32 MiB Long Task threshold: 128 active chunks produce one interval
reduction instead of 128, eight chunks delivered in separate 10 ms tasks produce
one reduction, and 128 never-loaded inactive chunks produce no payload reduction
or history request. All five browser tests discriminate the exact baseline and
pass on the patch. Activation mid-compaction, loaded-inactive sessions and remount
preserve exact authoritative text. Eleven new reducer/lifecycle tests and 130
neighbors pass; 67 real-renderer/history browser cases pass naturally.
See `issue-824-compaction-ui-fix.md`. Unit store gates use the repository's
`--conditions=browser --test-force-exit` convention for persistent imported
timers; this is not evidence of natural process cleanup. Spaced dispatcher tasks
are not real EventSource/provider cadence qualification. Interleaved same-session
events/read boundaries intentionally reduce the coalescing ratio, and sustained
arrivals can delay the existing revision-stable activation catch-up. Independent
UI review accepts this coalescing within those limits, without a reproduced text,
ordering or fence defect. Frozen-source validation results are recorded below.
## Browser HTTP/1 isolation: bounded non-reproduction
Five real Chromium Windows → Fastify Windows loopback HTTP/1.1 cases pass:
prompt/compact crossed with page/context close, plus a separate background-read
budget case. One real multiplexed SSE and a held SDK session A preflight leave
input/switch, B reads, metadata and a control endpoint available. Abandonment and
reconnection retire old HTTP/SSE observers before the held handler is released;
late completions produce no forward, environment write or shared invalidation.
Five catalogue intents admit only two held reads; queued work from the destroyed
page does not resume. Held upstream handler closures and idle pooled sockets are
reported separately from outstanding HTTP observers.
See `issue-824-browser-http.md`: 5/5 standalone, 10/10 combined with event
isolation, and 15/15 neighboring server tests pass naturally. No new product
correction follows from this non-reproduction. CDP and Fastify both verify the
HTTP/1.1 protocol, but controls are a minimal Solid fixture, not the complete
transcript/composer; upstream is disposable HTTP with native-shaped responses,
not native compaction. Linux/Electron, LAN and native 2.0.19 remain unqualified;
the report records the isolated cross-host experiment and evidence to collect.
## Native service isolation: bounded non-reproduction
The private native compaction fixture completed twice on OpenCode 2.0.21/Windows.
While A's real summary response was deliberately gated for ~1.5 s, B accepted a
prompt, ran its model request and completed successfully before A was released.
Native info/get/activity remained available (~1.8–7 ms observed). Both sessions
were simultaneously running; A then completed compaction with 29 native deltas
and no compaction-failed event. During a separate 32 MiB outline scan in the real
plugin host, six native info reads completed before the scan ended (final run).
See `issue-824-native-compaction.md` for commands, evidence and limits. This first
scenario used only 24,557 bytes of outgoing summary context; the 32 MiB tool
corpus belongs to outline work, not A's compaction context. Large-context assembly
was then qualified separately: two private runs each at 2/8/32 MiB verify every
tool payload byte at the actual provider boundary (32 MiB request JSON:
33,612,826 bytes). Submission-to-provider headers grows to 225–291 ms at 32 MiB;
unrelated pre-arrival reads show transient latency up to ~85 ms. B nevertheless
completes before A reaches the provider in both 8/32 MiB pairs, and completes
before A's held response is released at every size. No continuous global freeze
or specific native CPU hotspot is established. Provider JSON parse time is
separately recorded, not attributed to native assembly. No speculative upstream
patch follows. No inference about 2.0.19, Linux/Electron, remote TCP or the
reporter's exact data follows from this bounded evidence.
## Validation checkpoint (not final combined qualification)
`node --import tsx --test "packages/server/src/**/*.test.ts"` completed naturally:
**776 tests, 770 pass, 6 skip, 0 fail**, about 32.8 s. UI and server typechecks
also passed at this checkpoint. Auth/proxy corrections developed in separate
worktrees are not included in that run; fresh validation follows integration.
After auth and HTTP integration, a fresh strict-unhandled-rejection full server
run completed naturally: **799 tests, 793 pass, 6 skip, 0 fail**, about 32.8 s.
Command: `node --unhandled-rejections=strict --import tsx --test
"packages/server/src/**/*.test.ts"`. Log: managed shell
`sh_0f87b4309001T4M1Bgo7XpAvfz.out`. This includes the new login-authority race
regression; counts are not additive with earlier runs. Final review and frozen
composition validation remain outstanding.
The complete server build also passes (`npm run build --workspace
@neuralnomads/codenomad`), including production UI, TypeScript, auth assets and
bundled pruning/automation plugins. UI and Electron typechecks pass afterward;
diff check reports no whitespace defects. Vite reports chunks above 500 kB; no
unrelated bundling policy change or warning suppression was introduced. This
build did not start or deploy a desktop/backend process.
The first complete browser run finished naturally: **349 tests, 348 pass, 1 skip,
0 fail**, about 22.2 minutes (`npm run test:browser --workspace @codenomad/ui`,
log `sh_0f86cb7bb001g0xkd6q5qnv4G2.out`). It includes the five compaction cases,
task-shell retention, renderer/history/navigation and native Electron fixture
checks. The opt-in Electron tab-chrome check is skipped. Because this run began
while source owners were still integrating, it is not a frozen-source final
qualification; its event-isolation file executed before the additional deferred
Solid error case, and the later browser HTTP fixture was not included. Do not
reuse or add its count to the forthcoming fresh composition gate.
The independent server integration review accepts auth and HTTP changes without
a blocking finding; see `issue-824-server-review.md`. It preserves the distinction
between proven local defects and the unqualified reporter scenario. UI review
accepts compaction and SSE diagnostics; its task-membership finding was corrected
and closed by independent re-review. Dedicated HTTP browser checks are complete.
Fresh combined validation subsequently finished with before/after source fingerprints
covering tracked and untracked executable inputs. Its acceptance verdict and later
bounded follow-ups are below; documentation updates are excluded from its source
manifest.
## Reviewed-composition validation: acceptance remains blocked
The fresh validation uses 1,696 tracked and untracked executable inputs. Completed
gates have identical before/after manifests, SHA256
`16a3d4085dac90c27e892a480ae4c0735eb52782a9add8b0ca6732a640f1a4df`.
This verifies their boundaries, not a filesystem lock or installed dependencies.
Exact commands, logs and limitations are in `issue-824-combined-validation.md`;
private artifacts remain under
`C:/Users/Admin/AppData/Local/Temp/opencode/issue-824-combined-b91c8b66fcc840e1909170c5eeb846b9/`.
- Full server replay: **799 tests, 793 pass, 6 skip, 0 fail**, natural exit 0.
The first run's private Git-directory cleanup `EPERM` (792 pass, 1 fail, 6 skip)
is retained; the isolated nine Git tests and full replay pass with unchanged
sources. Its Windows cause is not established.
- UI/server/Electron typechecks and server build (including UI and bundled
plugins) pass naturally. A fresh complete private native fixture passes on
OpenCode 2.0.21; no user runtime or storage was used.
- Stores: **141/141 assertions pass with force-exit**. The natural-exit attempt
for the new compactage tests prints 11 passes but times out after 30 seconds;
lifecycle cleanup is not qualified. Four event-handler tests exit naturally.
- Full browser: **360 tests, 356 pass, 3 fail, 1 skip**, natural exit 1 in about
21.6 minutes. Failures are focus not returning to the composer after successful
Git diff insertion (line 609), MCP fixture readiness at reduced motion/150%
scale, and the expected legacy cached HTML not appearing after the literal-mode
toggle (line 104; earlier HTML/source checks passed).
All three pass in a targeted natural-exit replay without source edits; that
does **not** convert the full suite into a passing gate or establish the causes.
The existing focused replay has also finished naturally: **104/104 pass**, no
skip/cancelled, unchanged sources. Its eight suites include all mission browser
gates and the full session-rendering file. The validator's terminal report is
received; validation execution is complete, acceptance is still non-green.
- Fresh renderer A/B on the final membership-corrected postimage: three couples,
30 samples per variant, instrumentation off, natural exit 0. Median main-thread
**324.57 → 16.94 ms** (baseline range 168.06–743.53 ms, fixed 9.93–50.33 ms).
Raw metrics: `C:/Users/Admin/AppData/Local/Temp/opencode/codenomad-render-cost-srSQOf/metrics.json`.
This supersedes the earlier postimage's CPU qualification, not its historical
evidence; it measures the bounded fixture, not desktop FPS or all task costs.
No functional review blocker remains. However, the fresh full-browser gate is
non-green and the reporter's Electron Linux → Windows LAN/HTTP/OpenCode 2.0.19
scenario remains unqualified. The original freeze's exact cause is not established;
the private 2/8/32 MiB native compaction and loopback HTTP fixtures did not reproduce
a persistent global freeze. Do not mark #824 universally resolved or publish/install
these changes from this evidence alone.
The independent `browser-focus-cache-diagnosis` is now complete without editing
sources. It identifies a test synchronization gap: insertion updates the value
immediately, but composer focus/selection run in `setTimeout(0)`; the test asserts
focus immediately after reading the value. The original failed instant is allowed
by that contract; persistent loss of focus or draft is not demonstrated. Three
new complete focus passes and three HTML-cache passes are recorded separately.
A fourth focus attempt fails earlier, during a detached/intercepted Monaco overlay
click, and is retained as a distinct failure rather than attributed to #824.
The HTML cache transition is a synchronous local cache hit, so ordinary slow
Markdown highlighting is not its explanation; the original timeout remains
unattributed. See `issue-824-browser-focus-cache-diagnosis.md`.
The bounded `browser-mcp-bootstrap-diagnosis` is also complete: readiness timed
out in one of five natural-exit runs, with unchanged executable sources. The
network observations saturated the private harness's shared 4,000-entry budget
before terminal stages/errors/snapshots, so attribution is still impossible.
This is a confirmed diagnostic-capture weakness, not an identified product cause.
See `issue-824-browser-mcp-bootstrap-diagnosis.md`; all five outcomes are retained.
`browser-focus-test-sync` has now completed. The coordinator reviewed the diff:
after the unchanged text assertions, the test captures the textarea and waits for
its identity to equal `document.activeElement`, then retains the original identity
assertion. No sleep, new timeout, force-click, retry or product edit. Focus passes
2/2; the two-case run passes once and fails once on the independent HTML-cache
timeout. That timeout is now reproduced, not merely an unknown historical failure.
Opt-in `CODENOMAD_BROWSER_BOUNDARY_TRACE=1` prepares cache/overlay/focus diagnostics;
it was added after those two runs and has not yet captured the cache recurrence.
See `issue-824-browser-focus-test-sync.md`. Three authorized test/fixture inputs
changed; the earlier combined manifests remain historical, not fingerprints of
the revised test composition. Four selected product inputs remain unchanged in
the specialist's focused manifest; that subset is not a full-product manifest.
`browser-mcp-critical-capture` is now terminal: the private collector's overflow
is verified without a browser, then **5/5 targeted runs pass naturally** in 54.35 s
with complete critical traces, zero overflow and zero executable-source drift.
The 1,696-input comparison confirms that only the three authorized focus-sync
test/fixture paths differ from the earlier combined validation. Settings and render
finish in tens of milliseconds in these passes; most observed navigation time is
before the fixture body. That localizes cost in passes, not the historical timeout.
No sixth targeted run or product patch is justified. The prior red MCP and recurrent
HTML-cache timeout remain open; see `issue-824-browser-mcp-critical-capture.md`.
`browser-final-boundary-gate` has finished its **single normative full-browser
run**: **360 tests, 358 pass, 1 fail, 1 skip, 0 cancelled**, natural exit 1 in
20 min 10 s. All 1,696 inputs remain identical before/after, new manifest SHA256
`da74db30b68829edd40b44c069191cba8b21cc3e5e5d345862eec0cb9a14eb1c`.
The only failure is the final `errors=[]` assertion in the Git insertion test:
`ReferenceError: __name is not defined` from the anonymous opt-in browser script.
All earlier functional assertions in that case, including focus, pass in this
execution. The trace array is present but empty; it cannot prove absent gestures.
MCP and HTML-cache cases pass here, without attributing their earlier failures.
No retry or another full suite was run. See `issue-824-browser-final-boundary-gate.md`.
Acceptance remains **non-green**. `browser-trace-serialization-fix` is now
complete: the exact callback imported through the installed `node --import tsx`
serializes `const describe=__name(...)` without the module-scope helper. A minimal
Chromium page reproduces the same exception and empty trace. This is a confirmed
test-instrumentation defect introduced during this mission, not a product defect.
It is corrected with explicit, self-contained JavaScript passed to
`addInitScript({ content })`. The minimal Chromium check passes with no pageerror,
all observation kinds present and 200 entries retained after saturation. Snapshot
failure is isolated from the test assertion and page closure. UI typecheck and
diff-check pass; assertions, gestures, timeouts and product remain unchanged.
See `issue-824-browser-trace-serialization-fix.md`. The recorded full gate stays
red; there has been **no full-suite or feature-case rerun** after this correction.
Coordinator checkpoint, 2026-10-02 00:03 CEST: all **26 assigned tasks are
terminal completed**, no outstanding task, and no matching browser runner was
observed in the read-only Windows process check. A fresh hash comparison of all
**1,696 executable inputs**, including current tracked/untracked paths, against
the final gate finds exactly one difference: `git-history.test.ts`, SHA256
`dc7c378cc15e54f22f5d7e1a4c485313f0af9df536e1544690a33e3b2b2e5264`.
No product, configuration or script drift is found. This checkpoint is not a new
test execution or retroactive all-green verdict.
Next decision, not work running in the background: an acceptance run on the
corrected test composition is still needed for an all-green full-browser claim.
No automatic retry, extra audit or deployment is launched at this checkpoint.
The exact original #824 cause, earlier MCP/cache failures and natural store
cleanup remain unqualified; the mission is not finished as a successful fix.
Cross-host traces remain to be collected
according to `issue-824-browser-http.md`. No new product patch is justified solely
by a passing retry. No commit/push/merge/install/restart/session cleanup is
authorized by this checkpoint.
## Draft PR handoff authorized by the user
2026-10-02: the user requested a PR containing all mission changes. Deliver the
63 reviewed product/test/script/report paths on `fix/issue-824-performance` to
`dev` as a **draft**, without a closing keyword for #824. Preserve every recorded
failed run and qualification limit. The draft is the reviewable deliverable, not
a claim that all gates pass or the reported freeze is universally resolved.
All 26 investigation/correction/review tasks are complete and no test runner is
active at the handoff checkpoint. The durable mission's nonterminal status does
not mean that an agent or command is still executing: acceptance is incomplete.
No automatic retry or extra general audit accompanies publication. Acceptance
still needs the corrected test composition qualified and the cross-host limits
resolved or explicitly accepted; merge/install/restart/session cleanup remain
outside this publication authorization.
Modified source files above the size guideline (no size-only refactor):
`packages/server/src/server/http-server.ts` ~2,346 lines,
`packages/server/src/workspaces/manager.ts` ~1,240,
`packages/ui/src/stores/instances.ts` ~2,093,
`packages/ui/src/stores/opencode-data.ts` ~828,
`packages/ui/src/components/tool-call/renderers/task.tsx` ~569.

View file

@ -1,189 +0,0 @@
# Issue #824 — continuité de l'audit performance
- Date : 2026-10-01
- Baseline examinée : `366830f6` (`origin/dev`)
- Audit repris : worktree `audit-resume-integration`, baseline `47f9e43e`
## Portée et niveaux de preuve
Diagnostic en lecture seule du produit. Aucun daemon, profil, desktop ou stockage
utilisateur n'a été démarré ou modifié. Le seul exercice exécuté ici est un micro-
benchmark local de la primitive d'authentification, sans réseau ni fichier partagé.
Les affirmations ci-dessous portent explicitement l'un des niveaux suivants :
- **Mesure exécutée** : observation chronométrée pendant cette mission ou mesure A/B
conservée par l'audit précédent avec protocole et artefact décrits.
- **Reproduction fonctionnelle antérieure** : assertion rouge/verte exécutée par
l'audit précédent, sans nouvelle mesure de latence dans cette mission.
- **Analyse statique** : chemin de code observé ; coût ou impact encore à mesurer.
- **Hypothèse** : mécanisme plausible à tester, pas un défaut confirmé.
Les diagnostics spécialisés de compactage et de transport HTTP Linux/Windows sont
hors de cette carte. Les priorités proposées sont transversales et complémentaires.
## Continuité entre l'audit et la baseline courante
L'audit précédent n'était pas un commit : `audit/resume-integration` est toujours à
`47f9e43e` avec ses corrections en fichiers modifiés/non suivis. Entre `47f9e43e` et
`366830f6`, les 16 commits de `origin/dev` concernent notamment Files, responsive,
usage, OpenCode 2.0.20/2.0.21 et Tauri macOS. Ils ne constituent pas l'intégration
générale de l'audit.
Contrôle objet Git effectué : les dix fichiers sentinelles suivants ont exactement
le même blob à `47f9e43e` et à `366830f6` :
| Surface auditée | Blob encore présent à `366830f6` | État |
| --- | --- | --- |
| rendu des tâches imbriquées, `packages/ui/src/components/tool-call/renderers/task.tsx` | `dee90df8560b83958dbf6d6f7c40e013bbdb289f` | correction audit absente |
| distribution SSE UI, `packages/ui/src/lib/server-events.ts` | `29d8342071fee3fc1ab3a2f7bc71c847bc68f8a8` | correction audit absente |
| itérateurs/relay OpenCode, `packages/server/src/workspaces/opencode-service.ts` | `ad8cfbaa00ebce3cc47e9a70217244217cb8b3dd` | correction audit absente |
| Speech/Sidecars, `packages/ui/src/stores/speech.ts`, `sidecars.ts` | `45832e4c…`, `d52a8d5f…` | corrections audit absentes |
| fermeture multi-fenêtres Electron, `packages/electron-app/electron/main/multiwindow-lifecycle.ts` | `59995dfd1b36cc4d4eee742c4798477cb5b59b9b` | correction audit absente |
| historique pruning, `packages/server/src/opencode/session-pruning/history-database.ts` | `37318db55235fb9b50ffd2efb60a154495f6967a` | correction audit absente |
| polling provider, `packages/ui/src/components/provider-auth/provider-manager-modal.tsx` | `c1e977a70acaa649ab81d3dba5d6019c693f4f09` | correction audit absente |
| lecture conversation, `packages/ui/src/stores/conversation-speech.ts` | `67a12d80c070874f222b24b7b44dfd400beb51a6` | correction audit absente |
| auth manager, `packages/server/src/auth/manager.ts` | `4e234ace6fc1b94ebb8543617d7d8048239135db` | correction audit absente |
`packages/server/src/server/http-server.ts` a changé dans #811/#814, mais la
comparaison avec le worktree d'audit montre encore un delta : ces commits ne sont
pas l'intégration de la composition preview/redaction auditée.
### Ce que l'audit avait réellement qualifié
- **Rendu imbriqué** : mesure A/B historique sur 80 étapes `read` de 8 000 caractères,
30 échantillons par variante. Médiane main-thread `369,98 -> 20,64 ms` par delta,
remounts DOM `80 retirés + 80 insérés -> 0`; 80 snapshots natifs restent. Protocole,
variance et artefact dans `AUDIT_RENDERING_COST.md:40-80`. Deux assertions rouges
sur la source baseline et 63 tests navigateur verts sur la correction
(`AUDIT_RENDERING_COST.md:128-145`). **Mesure historique, pas rejouée sur 366830f6.**
- **Isolation événements** : 50 tests SDK/HTTP/relay/server et 4 régressions Chromium
verts sur la correction ; un destinataire relay lent n'y bloque pas le destinataire
rapide (`AUDIT_UPSTREAM_DEV_2026_09_30.md:193-208`). La correction isole exceptions
et fins locales d'itérateur, pas un callback UI qui consomme le CPU.
**Reproduction fonctionnelle antérieure, pas mesure de latence.**
- Les correctifs Speech/Sidecars, provider polling, pruning/history et fermeture
concurrente ont eux aussi été qualifiés dans le worktree d'audit, mais aucun signal
ne permet de les compter comme intégré à `366830f6`. Ils restent des correctifs de
robustesse/lifecycle, pas les premières reproductions performance de #824.
- L'audit a réfuté dans son fixture réel le montage de tout le transcript/timeline et
le DOM non borné des gros outputs (`AUDIT_RENDERING_COST.md:83-100`). Ne pas rouvrir
ces trois pistes sans nouvelle preuve contraire.
## Priorités résiduelles bornées
### P1 — Rejouer puis intégrer la correction de rendu des tâches imbriquées
**Niveau : mesure historique confirmée ; correction absente de la baseline.**
Le code courant recrée le rendu complet via un `createMemo` dans
`task.tsx:54-99`, et vide encore les clés à `task.tsx:236-243` pendant une
réindexation. Chaque `ToolCall` clone aussi profondément son entrée à la création et
à chaque changement de versions (`packages/ui/src/components/tool-call.tsx:630-640`).
Cela correspond exactement au chemin mesuré par l'audit. Le coût résiduel des 80
`structuredClone` est lui-même mesuré, mais son optimisation n'est pas validée.
**Plus petite prochaine reproduction :** récupérer sans l'élargir le fixture
`scripts/test-render-cost-audit.mjs` et `packages/ui/tests/browser/render-cost.test.ts`
du worktree audit, exécuter d'abord l'override rouge sur le blob
`dee90df8560b…`, puis A/B sur `366830f6` et sur la correction auditée. Conserver les
assertions d'identité DOM comme gate ; ne pas introduire de seuil timing CI instable.
### P2 — Isoler les abonnés SSE et mesurer le temps de distribution
**Niveau : défaut fonctionnel reproduit antérieurement ; risque CPU statique.**
La baseline distribue ouverture, événements et statuts avec des `Set.forEach`
synchrones sans frontière par callback (`packages/ui/src/lib/server-events.ts:69-76`,
`:151-170`). L'audit a confirmé qu'une exception d'un abonné supprime les abonnés
suivants et peut interrompre un restart. Cela peut ressembler à une application figée
alors que le transport reste vivant. En revanche, aucune preuve ne montre encore un
callback lent bloquant durablement le renderer ; l'audit précise que ce cas n'est pas
isolé.
**Plus petite prochaine reproduction :** réimporter le fixture
`event-subscriber-isolation.test.ts`, prouver rouge/vert sur l'exception, puis ajouter
un lot borné d'événements avec un abonné instrumenté volontairement lent. Mesurer
durée de `dispatchBatch`, retard d'un `requestAnimationFrame` sentinelle et abonnés
appelés. Le premier correctif utile reste la petite barrière `try/catch` déjà auditée ;
une planification asynchrone ne doit être envisagée qu'après mesure car elle change
l'ordre/la sémantique Solid `batch`.
### P3 — Retirer `scryptSync` du thread HTTP d'authentification
**Niveau : mesure exécutée et analyse du chemin HTTP.**
`verifyPassword` et `hashPassword` utilisent `crypto.scryptSync`
(`packages/server/src/auth/password-hash.ts:23-28`, `:37-48`). Le POST login appelle
ce chemin synchroniquement via `AuthManager.validateLogin`
(`packages/server/src/auth/manager.ts:74-79`; route dans
`packages/server/src/server/routes/auth.ts:99+`). Pendant ce calcul, le thread Node
partagé ne sert ni autres requêtes ni événements des autres sessions.
Micro-benchmark exécuté sur Windows, Node `v25.2.1`, paramètres de production
`N=16384,r=8,p=1`, 10 vérifications séquentielles : médiane **19,56 ms**, plage
**19,12–21,60 ms**. C'est une durée de blocage de primitive, pas une mesure Fastify,
desktop ou charge concurrente. Une rafale de tentatives multiplie directement ce
blocage ; l'impact réel et les limites de taux ne sont pas qualifiés ici.
**Plus petite prochaine reproduction :** fixture Fastify inject/loopback isolée avec
un heartbeat de latence pendant une rafale bornée de logins invalides. Mesurer p50/p95
du heartbeat et durée login avant/après passage à `crypto.scrypt` asynchrone, en
conservant comparaison constante, paramètres et réponses d'auth identiques. Couvrir
aussi `setPassword`, qui appelle `hashPassword` sur le même thread.
### P4 — Borner le coût renderer + disque de la persistance d'état client
**Niveau : analyse statique ; hypothèse de freeze à reproduire.**
Le renderer recapture l'état après 100 ms d'activité
(`packages/ui/src/lib/hooks/use-app-session-capture.ts:124-168`), puis la couche état
reprogramme une sauvegarde après 250 ms (`packages/ui/src/stores/client-state.ts:10`,
`:85-128`, `:139-145`). Chaque sauvegarde normalise, encode tout le graphe et contrôle
jusqu'à 4 096 partitions / 256 MiB autorisés
(`client-state-partitions.ts:8-15`, `:188-220`). Côté Electron, chaque mutation clone
l'enveloppe avec `JSON.parse(JSON.stringify(...))`, la resérialise, l'écrit, rename et
fsync le répertoire (`packages/electron-app/electron/main/client-state.ts:628-692`).
Le chemin normal est asynchrone côté disque ; les migrations/cross-host utilisent
encore des opérations sync, mais surtout au démarrage/récupération, pas à chaque save.
Aucun profil de gros état ou disque lent n'a été exécuté : **ne pas décrire cette
piste comme cause confirmée**.
**Plus petite prochaine reproduction :** fixture Electron/module isolée générant des
états réalistes à 1, 16, 64 et 256 MiB, avec churn de draft/layout à 10 Hz. Instrumenter
séparément `encodeClientSnapshotV2`, IPC, file d'écriture, retard event-loop renderer
et main, taille/compte de partitions et durée de flush. Simuler un writer lent injecté,
sans disque utilisateur. Le résultat dira s'il faut du partage structurel, un worker,
une fréquence moindre ou uniquement une borne plus basse.
### P5 — Mesurer la rafale périodique de réconciliation multi-instance
**Niveau : analyse statique ; hypothèse de contention réseau/réactive.**
Toutes les 30 secondes, `App.tsx:276-289` parcourt simultanément toutes les instances
`ready`. Même sans session active, `runPendingRequestLiveness` finit par
`syncPendingRequests` (`packages/ui/src/stores/instances.ts:864-887`), lequel lance en
parallèle scans permissions et Forms, avec jusqu'à trois reprises de stabilisation
(`instances.ts:795-817`). Les appels sont coalescés par instance, mais pas étalés entre
instances. Des horloges UI à 1 s existent aussi dans `session-list.tsx:117-121`,
`instance-shell2.tsx:375-379` et `instance-tab.tsx:28`; leur coût n'est pas mesuré.
**Plus petite prochaine reproduction :** monter 1/5/20 instances contre clients fake,
compter requêtes et concurrence au tick 30 s, puis mesurer retard RAF et commits Solid
avec zéro puis plusieurs Forms/permissions. Tester visibilité/inactivité séparément.
Ne modifier intervalle, staggering ou demande visible qu'après avoir établi une pente
avec le nombre d'instances.
## Ordre recommandé
1. Rejouer P1 : gain historique majeur et patch déjà borné/qualifié.
2. Rejouer P2 : évite les faux freezes d'état sans toucher au transport.
3. Corriger P3 si le test Fastify confirme le retard concurrent ; le mécanisme bloquant
est déjà mesuré.
4. Profiler P4 et P5 en parallèle conceptuel, mais ne promouvoir un correctif qu'avec
une mesure par phase et un fixture de non-régression.
Cette carte ne prétend pas expliquer le freeze de compactage ni le blocage HTTP
Linux/Windows persistant après restart. Elle évite également de compter comme
intégrées les corrections seulement présentes dans `audit/resume-integration`.

View file

@ -1,193 +0,0 @@
# #824 — coopération du serveur pendant les calculs de mot de passe
## Périmètre et résultat
Tâche initiale `auth-loop-responsiveness`, branche `fix/issue-824-auth`, worktree
`D:\CodeNomad\.codenomad\worktrees\issue-824-auth`, baseline `366830f6`.
Intégration `integrate-auth` dans
`D:\CodeNomad\.codenomad\worktrees\issue-824-performance` : sept fichiers
autorisés seulement, via patch tool, sans réinstallation des dépendances.
Défaut confirmé puis corrigé : les routes de login et de changement de mot de
passe exécutent une dérivation scrypt coûteuse sur le thread HTTP partagé.
Les dérivations HTTP utilisent maintenant `crypto.scrypt`, sur le pool libuv,
et leurs callers attendent le résultat. Aucun changement logout, compaction,
transport SSE, daemon OpenCode ou configuration utilisateur par ces tâches.
## Reproduction comportementale rouge
`packages/server/src/server/routes/auth-responsiveness.test.ts` utilise le vrai
Fastify, les vraies routes et un vrai AuthManager. Chaque fixture crée un
`auth.json` v1 privé sous `%TEMP%/opencode/codenomad-auth-responsive-*`, avec un
hachage produit indépendamment par l'ancienne API synchrone. Elle ferme Fastify
et supprime uniquement son propre répertoire à la fin.
Deux rafales bornées : 8 logins invalides, puis 4 changements authentifiés de
mot de passe. À l'entrée du premier calcul, un `setImmediate` programme un GET
sur un endpoint témoin, qui observe le nombre d'opérations encore en cours.
L'assertion exige que le témoin s'exécute **avant la fin de tous les calculs**.
Les durées ne sont que des diagnostics, sans assertion de seuil en ms.
Le corps des méthodes, le calcul cryptographique et les handlers ne sont pas
remplacés ; les wrappers ne font que compter et chronométrer.
Avant modification produit, les deux tests ont échoué : témoin après les
8/8 logins (~169 ms), et après les 4/4 changements (~84 ms). Réponses correctes
mais aucun travail témoin pendant la rafale. Chaque appel bloquait ~21 ms.
Après modification, l'ancien code a été recopié par `git show 366830f6:<path>`
dans une fixture privée sous le `node_modules/.cache` du worktree **source**,
avec les tests courants. Les deux mêmes assertions échouent dans chacune des
5 exécutions rouges. Aucun checkout ni fichier produit n'a été inversé pour
ce contrôle.
Fixture baseline retenue localement (non versionnée, dans `issue-824-auth`,
non recopiée dans le worktree mission) :
`node_modules/.cache/auth-baseline-4193325c8f9a4bcd973718259e94c2f6`.
Elle contient `auth/{auth-store,manager,password-hash,http-auth,token-manager,session-manager}.ts`,
`server/routes/auth.ts` de la baseline et le test de réactivité courant ; les
imports de dépendances utilisent seulement le node_modules du worktree source.
## Mesures exécutées rouge / vert (tâche initiale)
Windows x64, Node `v25.2.1`, pool libuv par défaut (4), mêmes paramètres scrypt.
Cinq processus Node distincts par variante, tests témoins seuls ; pas de seuil
de performance ni de conclusion statistique au-delà de ces échantillons.
| Mesure (médiane des 5 runs) | Baseline | Correctif initial |
| --- | ---: | ---: |
| Témoin pendant les 8 logins | 168,63 ms | 3,37 ms |
| Durée totale des 8 logins | 179,23 ms | 67,33 ms |
| Témoin pendant les 4 changements | 82,43 ms | 0,80 ms |
| Durée totale des 4 changements | 84,23 ms | 93,57 ms |
| État au témoin : logins encore en cours | 0/8 | 8/8 |
| État au témoin : changements encore en cours | 0/4 | 4/4 |
Échantillons témoin, en ms, dans l'ordre des runs :
- Rouge login : 187,0279 ; 163,5008 ; 161,1307 ; 168,6303 ; 173,3027.
- Vert login : 4,6347 ; 3,3659 ; 2,9695 ; 3,1687 ; 3,7206.
- Rouge password : 91,9619 ; 82,9719 ; 82,2616 ; 82,4302 ; 82,1526.
- Vert password : 1,1573 ; 0,6115 ; 0,6394 ; 0,7956 ; 1,6040.
La sérialisation des changements vise la cohérence, pas leur débit. Leur temps
total n'est pas amélioré ; le témoin et donc la coopération du thread HTTP le
sont. Le coût CPU scrypt n'est pas supprimé.
## Correction et garanties
- Format v1, sel aléatoire 16 octets, clé 64 octets, paramètres par défaut
`N=16384, r=8, p=1, maxmem=32 MiB` inchangés.
- Vérification avec les paramètres enregistrés et comparaison
`crypto.timingSafeEqual` conservées ; longueur incompatible et algorithme
inconnu restent des refus. Les erreurs crypto deviennent des rejets attendus.
- Login attend le booléen, sinon une Promise truthy aurait autorisé les mauvais
mots de passe. Password attend le calcul **et la persistence** avant succès,
et attrape les rejets pour conserver la réponse 409 text/plain.
- AuthStore sérialise uniquement les changements de mot de passe par ordre
d'admission. Un échec ne bloque pas les suivants ; le bootstrap ne perd pas
son identité entre deux calculs ; le dernier changement admis gagne.
- Lors de l'intégration, une fence d'identité du record d'autorité a été ajoutée
après la dérivation : `valid && this.load() === auth`. Un changement réussi
remplace le record cached ; un échec ne le remplace pas. Une dérivation sur
un ancien record ne peut plus accorder de session après son remplacement.
Aucun nouveau calcul ni replay automatique du login.
- Désactivation d'auth, override CLI/env et cookies conservent leur sémantique.
- Le hachage synchrone de l'override CLI/env reste explicitement nommé
`hashPasswordSync`, appelé seulement par `ensureInitialized` au constructeur,
avant le service HTTP. Cela évite d'élargir cette correction au cycle de
démarrage. Les lectures/petites écritures auth.json synchrones ne changent pas.
- Recherche des callers effectuée dans le dépôt : les deux handlers HTTP sont
les seuls callers produit de validateLogin/setPassword ; les await ont été
ajoutés. Le typecheck serveur valide cette propagation.
- Aucun rate limiter explicite n'a été trouvé dans les routes/auth courantes :
aucun mécanisme existant n'est retiré, et aucun nouveau n'est introduit ici.
## Validation et commandes
```powershell
node --import tsx --test packages/server/src/auth/password-hash.test.ts packages/server/src/server/routes/auth-responsiveness.test.ts packages/server/src/server/routes/auth.test.ts
npm run typecheck --workspace @neuralnomads/codenomad
git diff --check
```
Tâche initiale : 8 tests verts, répétés cinq fois (40/40 exécutions de tests),
puis 5 runs verts supplémentaires des seuls tests témoins. Typecheck serveur
vert. Diff check sans erreur (avertissements Git habituels LF/CRLF seulement).
Intégration : les sept chemins cibles étaient sans modifications au départ.
Les quatre fichiers produit et les deux tests ont été repris via patch tool ;
seuls `auth-store.ts` et le test de réactivité ont été enrichis pour la course
de changement de mot de passe. Le rapport source est repris et actualisé ici.
Les changements voisins de mission ne sont pas touchés.
### Course login / changement de mot de passe : rouge puis vert déterministe
Le test `rejects a pending login verified against a password replaced before
verification returns` fait réellement dériver l'ancien mot de passe par
`crypto.scrypt`, mais retient son callback. Il enregistre ensuite un nouveau
mot de passe par la vraie route `/api/auth/password` et vérifie le nouveau
secret, avant de libérer l'ancien callback. Il ne suppose aucune durée ni
ordre implicite du scheduler. Le mock est restauré et les requêtes privées
sont drainées avant fermeture.
Sur le correctif initial intégré sans fence : échec attendu, **200 au lieu de
401**, après le succès de la mutation password. Après ajout de la fence :
**401 Invalid credentials sans Set-Cookie**, nouveau secret valide, ancien
secret refusé. L'authentification ne réutilise pas une autorité remplacée.
La suite intégrée passe **9/9 tests**, dont les 8 tests précédents et ce nouveau
cas ; elle a été rejouée trois fois supplémentaires (27/27 verts).
Typecheck serveur vert. Exemple de diagnostics au run intégré : témoin
login 3,29 ms avec 8 opérations encore en cours ; password 0,52 ms avec 4 en
cours. Ces mesures sont descriptives et séparées du tableau A/B initial.
```powershell
node --import tsx --test --test-name-pattern='rejects a pending login' packages/server/src/server/routes/auth-responsiveness.test.ts
```
Commande témoin courte, utilisable sur le correctif ou avec le chemin du test
dans la fixture baseline source ci-dessus :
```powershell
node --import tsx --test --test-name-pattern='services a witness' packages/server/src/server/routes/auth-responsiveness.test.ts
```
Couverture : ancien hachage v1, paramètres personnalisés, nouveau hachage lu
par scryptSync indépendant, mot de passe et utilisateur invalides, login valide
et cookie, refus de password non authentifié, erreurs override/désactivation,
bootstrap concurrent, ordre des changements, échec persistence puis reprise,
réactivité login/password, autorité remplacée pendant une vérification et
compatibilité du test login HTML existant.
Installation isolée initiale des dépendances par `npm ci --ignore-scripts`
(lockfile inchangé). npm signale 49 vulnérabilités préexistantes ; pas de
`npm audit fix` ni de mise à jour hors périmètre. Aucune réinstallation lors
de l'intégration ; dépendances de mission existantes uniquement.
## Limites et suite minimale
- Les requêtes passent par `Fastify.inject`/light-my-request, pas par un socket
réseau ni Chromium. Ceci isole le défaut du thread HTTP, sans prétendre
reproduire la saturation HTTP Linux→Windows, SSE ou compaction.
- Scrypt utilise toujours du CPU/de la mémoire et partage le pool libuv avec
d'autres opérations. La saturation du pool et les rafales non bornées ne sont
pas qualifiées ici ; une politique d'admission serait un travail séparé.
- La fence compare l'autorité cached/override de ce processus, comme le store
existant. Elle ne crée pas de surveillance des changements externes du
fichier auth.json ni de révocation des sessions déjà accordées.
- Aucun serveur utilisateur, daemon partagé, profil, desktop ou base native
OpenCode utilisé ; aucun restart, déploiement, commit ou push.
- Intégration effectuée ; revue indépendante et validation finale de mission
restent nécessaires. Aucune correction logout ou politique rate-limit.
## Fichiers du patch
1. `packages/server/src/auth/password-hash.ts`
2. `packages/server/src/auth/auth-store.ts`
3. `packages/server/src/auth/manager.ts`
4. `packages/server/src/server/routes/auth.ts`
5. `packages/server/src/auth/password-hash.test.ts` (nouveau)
6. `packages/server/src/server/routes/auth-responsiveness.test.ts` (nouveau)
7. `dev-docs/issue-824-auth-responsiveness.md` (ce rapport)
Tous les fichiers source/test modifiés restent sous les seuils AGENTS.

View file

@ -1,198 +0,0 @@
# #824 — gate navigateur final unique
## Verdict terminal
**Gate non vert : 360 tests, 358 pass, 1 fail, 1 skip, 0 cancelled.**
Commande normative exécutée **une seule fois**, exit 1 **naturel**, sans timeout ni
force-exit. Zéro dérive des sources. Travail de validation terminé et rapporté,
pas acceptation globale. Aucun retry/ciblé ou second fullbrowser après ce rouge.
L'échec unique est `git-history.test.ts:684`, sur le contrôle final `errors=[]` :
un pageerror `ReferenceError: __name is not defined` vient d'un script navigateur
anonyme. Ce n'est **pas** l'ancien échec d'identité du focus ni le clic Monaco.
Le snapshot opt-in est présent mais sa chronologie est vide. Le cache HTML et le
cas MCP précédemment rouges passent ici, sans démontrer leur résolution causale.
Le gate historique 356/3/1 et les diagnostics ciblés restent des pièces séparées.
## Admission fraîche
Workdir exact : `D:/CodeNomad/.codenomad/worktrees/issue-824-performance`.
HEAD `366830f6b4373e6b283cabf7dd04764e87e1cc32`, changements mission non committés.
Rapports `issue-824-browser-focus-test-sync.md` et
`issue-824-browser-mcp-critical-capture.md` lus, leurs tâches terminées.
Contrôle processus Node read-only préalable : aucun runner navigateur concurrent
repéré ; résultat et heure conservés dans `process-preflight.json` et
`preflight-time.txt`.
Nouveau manifeste **1 696 fichiers**, tracked + untracked non ignorés : sources
produit, tests, scripts, configs et locks ; exclusions docs/Markdown/PDF,
dist/public/deps/cache/release/target. Comparaison avec `combined/.../initial.json` :
**exactement les trois inputs autorisés de focus-sync** ont changé :
- `packages/ui/tests/browser/git-history.test.ts` ;
- `packages/ui/tests/browser/session-rendering.test.ts` ;
- `packages/ui/tests/browser/fixtures/user-html.tsx`.
Comparaison des hashes de fichiers avec `mcp-critical/.../terminal-manifest.json` :
**aucun écart**. Aucun autre changement à déclarer ou autoriser ; hashes produit
inchangés depuis la validation combinée. `admission.json` conserve tous les hashes
anciens/nouveaux, chemins de référence et décisions. `browser-before.json` est
identique à l'admission ; le runner aurait refusé une dérive avant lancement.
SHA256 nouveau `initial.json` :
`da74db30b68829edd40b44c069191cba8b21cc3e5e5d345862eec0cb9a14eb1c`.
SHA256 combined initial historique :
`16a3d4085dac90c27e892a480ae4c0735eb52782a9add8b0ca6732a640f1a4df`.
Le manifeste MCP map-only est un format différent mais les hashes de fichiers
correspondent exactement : SHA
`4628acfc4df16f8e573da48dab23df3614a7a8a55643466e5e6e8e4f6e80105b`.
## Exécution normative et confinement
```powershell
$env:CODENOMAD_BROWSER_BOUNDARY_TRACE = '1'
npm run test:browser --workspace @codenomad/ui
```
Le script npm inchangé exécute
`node --import tsx --test --test-concurrency=1 tests/browser/*.test.ts`.
Pas de filtre, de liste privée substituée, de nouveaux délais/assertions/gestes,
ni de policy/cleanup du cache Vite. La capture MCP privée n'est **pas** insérée
dans ce fullbrowser : ses cinq passes restent ciblées/historiques, distinctes du
test MCP normal exécuté ici. Le trace opt-in préparé active uniquement les surfaces
focus/cache existantes. Aucun patch produit/test/fixture/script/config/deps/lock.
Le runner privé invoque la commande npm exacte via `cmd.exe /d /s /c`, depuis le
workdir exact, et conserve stdout/stderr sans filtrage dans `browser.log`.
Budget 60 minutes, exécution background avec notification de fin, pas de polling
répétitif. `started.json` créé en mode exclusif interdit un second lancement.
Si timeout, seule l'arborescence PID de ce runner npm privé est arrêtée, jamais le
daemon partagé ; timeout/forced se distinguent d'une sortie naturelle.
Artefacts :
`C:/Users/Admin/AppData/Local/Temp/opencode/issue-824-browser-final-0e81ac45520348328ab5bc06252a2010/`.
Runner `validate.cjs` SHA256 :
`2d31356e2f67899fc7a1207e145feb411907ff6f4bdc6bffa2842f12619e599d`.
Shell background : `sh_0f95dae060013DD7dMlptG7IAp`.
Aucun install, commit/push/merge, restart/déploiement, suppression de sessions,
accès au daemon/base/profil utilisateur, sous-agent ou autre gate. Les seuls edits
du validateur pour cette tâche sont ce rapport et les artefacts TEMP privés.
## Limites invariantes
Serveur/build/typechecks/A-B antérieurs restent historiques ; pas de nouvelle
exécution nécessaire ici puisque produit/scripts/locks sont identiques. Ce gate
ne qualifie ni Linux Electron → Windows LAN/OpenCode 2.0.19, ni la sortie naturelle
des stores. Un éventuel vert ne prouve pas l'élimination universelle des freezes
ni la cause des rouges précédents. Une panne HTML/Monaco/MCP reste rouge sans retry
et sans narration d'innocuité supposée.
Les snapshots sont ceux des tests existants. Si leur évaluation échoue, le log
complet doit conserver l'exception et le rapport distinguer `observation-error`
de l'échec produit/assertion ; un snapshot absent n'est pas un état vide démontré.
La mission et sa livraison terminale restent la décision du coordinateur.
## Résultat exact et captures
- Début UTC `2026-10-01T21:27:41.967Z`, fin `2026-10-01T21:47:52.008Z`.
- Durée runner **1 210 016 ms**, soit **20 min 10 s** ; durée Node test
`1 209 656.4827 ms`. Notification finale reçue, pas statut déduit du dernier test.
- Exit 1, signal null, launchError null, timeout false, forcedExit false,
naturalExit true. Aucune action d'arrêt du runner nécessaire.
- Total 360 = 358 pass + 1 fail + 0 cancelled + 1 skip, 0 todo, 0 suites.
- Skip : `Electron BrowserWindow native zoom retains tab seams and matching
scrollbars`, opt-in `CODENOMAD_TEST_ELECTRON` absent. Ne pas confondre avec les
autres fixtures Electron du corpus qui passent ; pas de qualification Linux/LAN.
- Node v25.2.1 Windows x64 ; Chromium par défaut Playwright, version 153.0.8010.12
confirmée par les logs HTTP. `NODE_OPTIONS=--enable-source-maps` hérité inchangé.
### Unique échec — assertion et limite observationnelle distinctes
Cas : `central diff inserts local lines and revision-qualified history into the
real session composer`, durée cas `3316.8763 ms`, déclaration ligne 590.
Assertion rouge ligne **684** : expected `[]`, actual :
```text
ReferenceError: __name is not defined
at <anonymous>:2:78
at <anonymous>:2:1004
at <anonymous>:3:7
```
Le cas a atteint le contrôle final après ses assertions de brouillon, références
Git, restitution de focus (`:630–632`), sélection de plage, historique de commit,
référence du worktree WSL, autorité de session et restauration de draft. Toutes
ces assertions ont donc passé **dans cette exécution**, malgré son statut final
rouge. Pas de timeout `activeElement`, de détachement/clic Monaco ou de replay.
Snapshot existant intégral, avant fermeture :
```text
Git focus boundary trace {
events: [],
active: { tag: 'BODY', id: '', className: 'enable-motion underline-links' },
widgets: [],
selections: []
}
```
**Observation-error distincte** : la collecte opt-in du script d'init n'a pas
produit de chronologie exploitable ; `__name` est un binding auxiliaire absent
dans le contexte navigateur. Le source de l'instrumentation (`:595–615`) initialise
le tableau puis définit un helper nommé `describe` et les listeners. Le mécanisme
attendu de transformation/sérialisation d'un helper par tsx/esbuild est cohérent
avec cette erreur, mais le JS exact sérialisé n'a pas été capturé : pas de preuve
supplémentaire par nouveau test ou expérience de transpilation dans cette tâche.
Le snapshot lui-même **n'a pas échoué à l'évaluation** : il est enregistré, sans
exception page.evaluate dans le résultat final. `events: []` ne démontre donc pas
l'absence de gestes/focus : la capture est défaillante. L'état BODY est celui de
la fin, après d'autres navigations ; il ne contredit pas l'assertion textarea passée
plus tôt. Les widgets absents en fin ne prouvent pas leur détachement pendant clic.
La régression observationnelle ne doit être ni attribuée au produit sur hypothèse,
ni ignorée pour déclarer ce gate vert.
### Frontières qui passent dans ce seul gate
- MCP `reduce, 150% scale` static rest : pass `1619.5347 ms` ; autres trois cas MCP
pass également. Test normal, aucune substitution/capture MCP privée insérée.
- HTML/cache : `user HTML mode preserves Markdown, pasted disclosures and code
source and fences legacy caches` pass `2196.9656 ms`. Aucune exception/snapshot
d'échec HTML imprimé, donc pas de nouvelle attribution du timeout ancien.
- Les 358 passes restent celles du corpus normal, pas additionnées aux
retries ni aux résultats historiques. Les erreurs de bootstrap antérieures
restent non attribuées ; aucun claim d'innocuité universelle.
## Empreintes et livraison terminale
`browser-before.json` et `browser-after.json` sont strictement identiques à
`initial.json`, SHA `da74db30b68829edd40b44c069191cba8b21cc3e5e5d345862eec0cb9a14eb1c`.
1 696 fichiers ; `drift: []`, `driftFromInitial: []`, HEAD inchangé.
Le runner exécuté conserve son SHA d'admission. L'immuabilité est constatée aux
bornes, pas un verrou filesystem pendant le run.
| Artefact privé | SHA256 |
| --- | --- |
| `browser.log` brut complet | `f24b7866e58109e029acdda5947caa24ce282f78818ff4ddd0a69b220f845eea` |
| `failures.log` extrait intégral du résumé rouge | `7db7e5c1ecfaf693f0540ea5d732df0c1f34435b55465012a5cf7bced8a3b5f9` |
| `boundary-output-through-end.log` trace puis suite jusqu'à fin | `fa7ef5a8b580e05cc810ac80af67fce3f18949c5d600e47e4981bdaad3b45b5f` |
`browser-result.json` conserve commande/statut/heures/durée/exits/manifests ;
`summary.json` vérifie compteurs et hashes ; `git-focus-snapshot.log` conserve le
snapshot exact, `boundary-observation-status.json` distingue l'observation-error
du contrôle `errors=[]` rouge et du snapshot évalué avec succès. Le postprocesseur
privé `summarize.cjs` ne lance aucun test/browser et ne filtre pas le log brut.
Serveur/build/typechecks/A-B ne sont pas rejoués ; leurs preuves historiques sur
produit identique restent qualifiées avec leurs limites déjà rapportées. Aucun
nouveau gate, correction, cleanup ou déploiement ajouté pour fabriquer du vert.
## Prochain minimum utile — décision coordinateur
Remettre ce **gate rouge unique** et les captures au propriétaire de focus-sync :
rendre le script opt-in navigateur autosuffisant à la sérialisation (par exemple
source JS explicite sans helper Node externe), sans modifier contrat produit,
assertions, délais ou gestes. C'est une recommandation bornée, **pas un patch ou
une relance autorisée ici**. Le coordinateur décide s'il faut un suivi séparé ou
s'il clôture avec cette limite ; aucune nouvelle boucle d'audit/retry du validateur.

View file

@ -1,172 +0,0 @@
# Issue 824 — attribution bornée des échecs navigateur focus et cache HTML
## Verdict
Diagnostic borné terminé, sans correction produit ni test.
- **Focus après insertion Git : course de synchronisation du test confirmée par
le contrat du produit; défaut produit #824 non démontré.** Le texte est inséré
immédiatement, mais la restitution du focus est volontairement programmée dans
une tâche `setTimeout(0)`. L'assertion du test ne synchronise pas cette tâche.
- **Cache HTML : non reproduit et non attribué.** Le chemin attendu après
`literal(false)` est un hit de cache local synchrone, pas une attente du renderer
Markdown. Les preuves disponibles ne permettent pas de choisir entre défaut de
réactivité ponctuel, cache Vite/optimisation du harness ou scheduling du run
complet.
- Une panne différente a été reproduite une fois avant la frontière focus : le
widget Monaco a été détaché pendant le clic. Elle confirme que cette fixture a
une frontière overlay/DOM instable, mais n'explique pas l'assertion
`activeElement` originale et ne doit pas être renommée en régression #824.
Le gate navigateur complet de la validation combinée reste **non vert**. Les
succès ciblés présents ne le remplacent pas.
## Inputs et identité
Sources du worktree `issue-824-performance`, HEAD `366830f6`, modifications mission
non committées. Node `v25.2.1`, Chromium `153.0.8010.12`, rejets non gérés stricts,
concurrence de tests 1. Aucun produit, test, fixture, dépendance ou configuration
n'a été modifié.
Artefact privé :
`C:/Users/Admin/AppData/Local/Temp/opencode/issue-824-focus-cache-diagnosis/diagnosis.json`,
SHA256 `F9ABE8BEC0A7A4940B9357448A37AC3AAB1A497BC4C8D8267E0495752ADDB5EA`.
Il enregistre les quatre répétitions et les SHA256 des huit inputs lus/exécutés.
Les quatre hashes test/fixture pris avant les runs sont identiques à ceux de
l'artefact final :
- `git-history.test.ts` : `1F779351772617138C12694D2236FFADCB557BA85E070E64A04C1CBEE7CC049F`;
- `fixtures/git-history.tsx` : `6C73B4FC43D1D4C0B32AB52733919751EBA9FB89BA4E905B542D9B8423C1E1EC`;
- `session-rendering.test.ts` : `DE5F5AE9E6F70428860FBD9089E7CC8EF3F87965B6DB6E164EDEEE000626B578`;
- `fixtures/user-html.tsx` : `904C3CA2B72B4CE7A2272762557C3BD2D3E06FCC7BDDC0310B9D044F5538E660`.
Les inputs produit effectivement inspectés et fingerprintés sont
`monaco-diff-viewer.tsx`, `session-view.tsx`, `prompt-input.tsx` et `markdown.tsx`;
leurs hashes complets sont dans l'artefact. Aucune instrumentation source n'a été
nécessaire.
## Focus Git : attribution
### Frontière exacte
1. Le bouton overlay empêche le `mousedown` par défaut afin de ne pas voler la
sélection Monaco, puis appelle synchroniquement `onRequestInsertContext`.
2. `GitDiffView` produit le texte qualifié; `SessionView.handleInsertPreviewComment`
appelle `PromptInputApi.insertComment`.
3. `insertComment` appelle `insertBlockContent`. Celui-ci fait immédiatement
`setPrompt(nextValue)`, puis programme seulement dans `setTimeout(..., 0)` :
`textarea.focus()` et `setSelectionRange()`.
4. Le test clique, lit deux fois `composer.inputValue()` puis évalue immédiatement
`element === document.activeElement`. Les lectures de valeur ne constituent pas
une attente contractuelle de la tâche timer.
Le run complet original a donc démontré exactement ce que ce séquençage autorise :
les deux assertions de contenu passent, puis l'identité de focus est encore fausse
à l'instant observé. Il n'a démontré ni perte de brouillon, ni mauvaise cible, ni
absence durable de restitution du focus.
### Replays présents et nouveaux
La validation combinée avait déjà un replay exact vert et une suite voisine verte.
Sur quatre nouvelles répétitions bornées :
- répétitions 1 et 2 : test complet vert;
- répétition 3 : n'atteint pas l'assertion; timeout de 30 s dans `insert.click()`.
Playwright rapporte que `<html>` intercepte les pointer events, puis que le
bouton overlay est détaché pendant le retry;
- répétition 4, focus seul : test complet vert.
Ainsi l'échec `false !== true` original reste non reproduit dans ce suivi, mais sa
cause de test est expliquée par une absence de synchronisation observable dans le
code. La panne de clic séparée interdit de qualifier la fixture comme parfaitement
stable.
### Disposition minimale
Ne pas changer le produit sur cette preuve. Si un gate navigateur entièrement vert
est exigé, le plus petit ajustement de test est d'attendre causalement que le
textarea devienne `document.activeElement`, dans le délai existant, puis conserver
l'assertion d'identité. Ne pas ajouter de sleep, force-click ou retry global.
La panne overlay distincte mérite, seulement si elle récidive au gate de livraison,
une trace opt-in de `widgetPosition`, sélection Monaco, bounding box et événements
`focusin/focusout`/pointer autour du clic. Elle ne doit pas être masquée par
`click({ force: true })`.
## Cache HTML : attribution
### Frontière exacte
Le fixture monte un `Markdown` avec une part dont `renderCache` contient :
```text
mode = 1:escaped:wrap
html = <div class="cached-html">old sanitized HTML</div>
```
En mode initial `literal(true)`, le mode résolu est `1:literal:wrap`; ce cache est
donc correctement refusé. Après `literal(false)`, avec `escapeRawHtml` toujours
vrai, le mode devient exactement `1:escaped:wrap`. L'effet de `Markdown` doit alors
prendre `snapshot.part.renderCache`, exécuter `setHtml(localCache.html)` et retourner
avant `renderSnapshot()`. L'apparition de `.cached-html` ne dépend donc ni du
chargement du module Markdown, ni de Shiki, ni d'une promesse de highlighting.
Le timeout original signifie que cette transition synchrone n'a pas été observée
dans le DOM. Sans trace de la valeur du signal, du nombre d'exécutions de l'effet,
de l'identité du nœud `#cache`, des erreurs page/module ou des mutations DOM, il est
impossible de distinguer :
- effet Solid non relancé ou runtime optimisé incohérent dans ce serveur Vite;
- fixture/nœud remplacé pendant le run complet;
- autre anomalie de scheduling ou de cache du harness.
Une lenteur ordinaire de rendu Markdown est écartée par le chemin de cache local.
Un défaut produit persistant est affaibli par les preuves : replay exact vert,
suite `session-rendering` complète verte (46 tests), puis trois nouvelles
répétitions cache **3/3 vertes** sur sources identiques.
### Disposition minimale
Aucun patch produit justifié. Ne pas demander davantage de retries ciblés. Si le
même timeout réapparaît dans un prochain gate complet autorisé, capturer dans le
failure artifact, sans modifier assertion ni délai :
- valeur courante de `literal` et compteur d'exécutions de l'effet;
- `#cache.innerHTML`, connectivité/identité du nœud et MutationObserver;
- `pageerror`, console, `requestfailed` et URLs des chunks Vite/Solid chargés;
- mode/cache key résolus avant et après le setter.
Cette trace permettra de tester causalement l'hypothèse de cache d'optimisation
Vite partagé. Introduire un `cacheDir` privé peut ensuite être évalué par contrôle
négatif; le succès isolé seul ne prouverait pas cette cause.
## Exécutions bornées
Commande à deux noms exacts exécutée trois fois, puis focus seul une fois :
```powershell
node --unhandled-rejections=strict --import tsx --test --test-concurrency=1 \
--test-name-pattern="^(central diff inserts local lines and revision-qualified history into the real session composer|user HTML mode preserves Markdown, pasted disclosures and code source and fences legacy caches)$" \
packages/ui/tests/browser/git-history.test.ts \
packages/ui/tests/browser/session-rendering.test.ts
```
Résultats : focus 3 passages complets verts et 1 timeout de clic préalable; cache
3/3 vert. Durées des quatre processus : environ 24,9 s, 22,8 s, 54,7 s et 15,0 s.
Sorties naturelles; la troisième sort avec code 1 sur timeout Playwright. Aucun
fullbrowser, stress, installation, daemon/profil/base utilisateur, restart,
déploiement, commit, push ou sous-agent.
## Décision pour le gate de livraison
Conserver séparément :
1. le fullbrowser 356/3/1 à exit 1;
2. ses retries et suites ciblées vertes;
3. ce diagnostic, qui attribue la course focus au test mais laisse le timeout
cache non qualifié et révèle une panne overlay distincte.
Si le critère de livraison exige un fullbrowser vert, appliquer uniquement la
synchronisation causale du focus après décision du coordinateur, instrumenter le
cache/overlay de manière opt-in au prochain gate complet, puis exécuter **un** gate
figé. Ne pas présenter les replays ciblés comme un gate final vert.

View file

@ -1,127 +0,0 @@
# Issue 824 — synchronisation du test focus et traces de frontière
## Résultat
La seule correction fonctionnelle est dans
`packages/ui/tests/browser/git-history.test.ts` : après les deux assertions de
contenu, le test attend maintenant causalement que le textarea déjà résolu soit
`document.activeElement`, puis conserve l'assertion d'identité existante.
Cette attente utilise le délai Playwright existant. Aucun sleep, timeout explicite,
retry, force-click, changement de sélection ou changement du contrat focus produit
n'a été ajouté.
Le focus corrigé passe **2/2**. Le second run a toutefois reproduit exactement le
timeout `.cached-html` indépendant. Le résultat ciblé global est donc un run vert
puis un run rouge; il ne constitue pas un gate navigateur vert et ne démontre pas
une correction universelle de #824.
## Diff exact
### Synchronisation focus
Après les assertions inchangées du brouillon et du contexte Git :
```ts
const composerElement = (await composer.elementHandle())!
await page.waitForFunction(element => element === document.activeElement, composerElement)
assert.equal(await composer.evaluate(element => element === document.activeElement), true)
```
Le textarea est capturé après l'insertion. L'attente observe uniquement la
restitution asynchrone du focus déjà prévue par `insertBlockContent()`.
### Instrumentation observationnelle, désactivée par défaut
`CODENOMAD_BROWSER_BOUNDARY_TRACE=1` active désormais :
- pour la fixture Git, les événements `focusin`, `focusout`, `pointerdown` et
`click`, le retrait éventuel du widget Monaco, l'élément actif final, la
connectivité/position des widgets et la sélection Monaco disponible ;
- pour la fixture HTML, la valeur `literal`, le DOM/connectivité de `#cache`, les
mutations, les points avant/après setter, le `renderCache`, les ressources de
module pertinentes, les `pageerror`, consoles et requêtes échouées.
En mode normal, la fixture HTML expose directement le setter Solid original. Les
observers et wrappers de trace ne sont créés que lorsque la variable opt-in ajoute
`trace=1`; seul un snapshot en lecture est disponible pour le dump d'échec déjà
existant. Assertions, délais et chemin normal restent inchangés.
Cette instrumentation prépare le prochain gate unique; elle n'a pas été utilisée
pour relancer le cas après le timeout, conformément à la limite de deux runs.
## Validation bornée
Commande exacte, concurrence 1 et noms exacts, exécutée deux fois :
```powershell
node --unhandled-rejections=strict --import tsx --test --test-concurrency=1 \
--test-name-pattern="^(central diff inserts local lines and revision-qualified history into the real session composer|user HTML mode preserves Markdown, pasted disclosures and code source and fences legacy caches)$" \
packages/ui/tests/browser/git-history.test.ts \
packages/ui/tests/browser/session-rendering.test.ts
```
Résultats :
1. exit `0`, 2 pass : focus 12,3 s; cache HTML 9,4 s;
2. exit `1`, focus pass 12,7 s; cache HTML timeout 38,8 s sur
`page.locator("#cache .cached-html").waitFor()`.
Le dump du second run est celui d'avant instrumentation et indique
`state: undefined`; il ne qualifie donc toujours pas la cause cache. Il établit en
revanche que le timeout antérieur est reproductible sur les mêmes sources et doit
rester une frontière ouverte, séparée de la correction focus.
Autres contrôles :
- `npm run typecheck --workspace @codenomad/ui` : exit `0` après instrumentation ;
- `git diff --check` sur les trois fichiers de test/fixture : exit `0`, avec
avertissements de conversion LF/CRLF seulement ;
- aucun fullbrowser relancé.
## Empreintes
Artefacts privés :
- `C:/Users/Admin/AppData/Local/Temp/opencode/issue-824-focus-test-sync/manifest.json`
— SHA256 `8440F3A61D0B59C870E1A64F9AAEFF90F68B5969FD5C77510F82AB2C899C543D` ;
- `run-1.log` — SHA256
`5354F36A00380DC16397A394C09B58FBD25D4AE3D613EF026024986077080E69` ;
- `run-2.log` — SHA256
`0499BDEFAB8EC34349CD2C63E1D0402CB1324AF98FF6567E984F5A1F6B15E63B`.
Le manifest contient les SHA256 avant/après de huit inputs. Seuls les trois inputs
de test autorisés ont changé :
- `packages/ui/tests/browser/git-history.test.ts` ;
- `packages/ui/tests/browser/session-rendering.test.ts` ;
- `packages/ui/tests/browser/fixtures/user-html.tsx`.
Les quatre inputs produit fingerprintés sont strictement identiques avant/après :
`monaco-diff-viewer.tsx`, `session-view.tsx`, `prompt-input.tsx` et `markdown.tsx`.
`fixtures/git-history.tsx` est également inchangé.
Le manifest de la validation combinée et le manifest du diagnostic précédent sont
désormais des preuves **historiques**, car `git-history.test.ts` a volontairement
changé. Ils ne fingerprintent pas ce nouvel état de test.
## Limites et prochain gate
- La panne distincte de clic/détachement du widget Monaco n'a pas récidivé dans ces
deux runs et n'est ni corrigée ni masquée.
- Le timeout cache a récidivé une fois; aucun patch Markdown/Solid/Vite n'est
justifié sans la nouvelle trace opt-in.
- Aucun test au-delà des deux noms ciblés n'a été exécuté. Le fullbrowser précédent
reste non vert.
Après la fin des diagnostics encore en file, exécuter un seul gate navigateur sur
sources figées avec `CODENOMAD_BROWSER_BOUNDARY_TRACE=1` et conserver l'artefact
complet si cache ou overlay échoue. Ne pas ajouter de retry pour fabriquer un gate
vert. Toute correction ultérieure du cache ou de Monaco doit partir de cette trace
causale et rester hors de cette tâche.
## Fichiers volumineux touchés
- `packages/ui/tests/browser/session-rendering.test.ts` : environ 910 lignes, sous
le seuil d'alerte test de 1000 lignes mais proche ; aucun refactor de portée n'a
été entrepris.

View file

@ -1,155 +0,0 @@
# Issue #824 — bounded Chromium HTTP/1 admission qualification
## Result and scope
**No global freeze reproduced in these five isolated scenarios.** Chromium
Windows can progress in session B while session A's prompt/compact ownership
preflight is held. Closing a page or browser context retires the old HTTP/SSE
observers before the upstream handler is released. A new page connects and reads
B before that release; late completion does not forward the abandoned mutation.
No additional product correction follows from this result.
This is **Windows Chromium → Windows Fastify over private loopback HTTP/1.1**,
not Electron Linux → Windows LAN qualification, not a desktop process restart,
and not qualification of an OpenCode 2.0.19 runtime. No OpenCode daemon, database,
provider, shared profile, production backend or installed desktop is used.
Added paths only:
- `packages/ui/tests/browser/http-admission-responsiveness.test.ts`
- `packages/ui/tests/browser/fixtures/http-admission-responsiveness.tsx`
- this report
## What is real versus supplied
- Playwright 1.63.0 launches installed Chromium **153.0.8010.12** on Windows;
Node **25.2.1** runs private ephemeral Fastify listeners.
- The browser addresses Fastify directly. Vite serves fixture modules on another
origin; there is **no Vite/Node proxy** in the business-request/SSE path.
- CDP `Network.responseReceived` asserts `http/1.1` for the real SSE response,
B response, CodeNomad metadata and control witness. Fastify also records request
`httpVersion === "1.1"`. These are not intercepted Playwright responses.
- Production `registerEventRoutes`, `EventBus`, `ClientConnectionManager` and
browser `serverEvents` provide one multiplexed EventSource subscription per
page. Event payloads are fixture-controlled; there is no native event relay or
native compaction event sequence in this experiment.
- Production `registerInstanceProxyRoutes`, request admission, runtime fetch and
`WorktreeDeletionFence` implement admission/cancellation. The official
`@opencode/client@2.0.21` Promise SDK runs in browser **and** backend. Real
browser `createInstanceFetch` performs classification/background scheduling.
- A disposable upstream Fastify service supplies native-shaped HTTP responses,
including the session `{ data }` wire envelope. Its A GET handler deliberately
remains held even after its HTTP response socket closes. This is an induced
preflight wait, **not** real native compaction or provider execution.
- A small ownership manager supplies only the `/fixture` identity and invokes a
real SDK location read. No discovery, filesystem or actual worktree scan is
performed. Shared production primitives are imported rather than copying the
server disconnect fixture's complete fake SDK tree.
- Draft input/session switching are minimal Solid fixture controls. Their switch
performs the real SDK B read. This measures browser event/input/network progress,
not rendering of the full CodeNomad transcript, session store or composer.
- “Info” is the real CodeNomad `/api/meta` route, not native `server.info()`.
`/api/info` is not exposed by the workspace proxy allowlist and was not added.
## Scenarios and causal assertions
Four cases cross prompt/compact with page/context close. Each starts exactly one
SSE plus one A mutation whose real backend SDK session read is held. Before release,
the browser edits a draft, switches/loads B, reads `/api/meta` and reads a control
witness. A stays pending throughout those successful operations.
A **separate fifth case** adds five distinct agent catalogue read intents through
the existing `createInstanceFetch` policy. Only **two** reach upstream and remain
held; three stay queued. Distinct agent URLs avoid Chromium's same-URL cache
coalescing. This is a scheduling stress witness, not a claim that normal UI issues
five such reads on any particular cadence. No six-held-foreground-request fanout
is used to manufacture an ordinary-workflow freeze.
Before releasing any held upstream handler, closing the page/context must produce:
- zero SSE registrations, old downstream mutation responses, backend SDK A
observers, native A HTTP responses and (fifth case) catalogue HTTP responses;
- one observed A admission abort, restored request-abort listener count and no
residual admission/forwarding close listener;
- a completed deletion-fence operation; A is held **before** fence admission,
so this checks non-reservation, not cancellation of an already admitted write;
- successful new-page SSE/B/control reads with no old observer restarted.
Upstream handler closures deliberately remain alive until the test gate is opened
(one handler, or three with catalogues). They are counted separately from HTTP
observers: their intentional retention must not be mistaken for an orphaned HTTP
watcher. After release the test waits for those continuations to finish and checks
zero A forwards, environment writes and connection invalidations, with no new
catalogue dispatch from the destroyed page.
## Executed validation
Existing dependencies/browser reused without installation. From the mission root:
```powershell
node --unhandled-rejections=strict --import tsx --test packages/ui/tests/browser/http-admission-responsiveness.test.ts
node --unhandled-rejections=strict --import tsx --test packages/ui/tests/browser/http-admission-responsiveness.test.ts packages/ui/tests/browser/event-subscriber-isolation.test.ts
node --unhandled-rejections=strict --import tsx --test packages/server/src/server/__tests__/instance-proxy-disconnect-diagnostic.test.ts packages/server/src/server/request-admission.test.ts
npm run typecheck --workspace @codenomad/ui
```
Standalone: **5/5 pass**, zero skips, natural exit ~4.0 s. After adding explicit
held-handler continuation accounting, combined gate: **10/10 pass**, zero skips,
natural exit ~5.1 s (five new cases plus five existing subscriber-isolation cases).
UI source typecheck passes; that tsconfig excludes browser tests/fixtures, which
are instead executed via tsx/Vite/Chromium. Neither a full browser-suite gate nor
full desktop/native-runtime validation is claimed.
The existing server disconnect/admission cases also pass **15/15**, zero skips,
natural exit ~0.8 s, without editing their fixture or product sources. Scoped
no-index whitespace checks on the three new files emitted only Git's LF/CRLF
warnings, no whitespace diagnostics (no-index diff status 1 for new contents).
Descriptive measurements from the combined run (not CPU latency requirements):
| Scenario | Input/B/meta/control progress while held | Close cleanup | Backend peak TCP sockets |
| --- | ---: | ---: | ---: |
| prompt, page close | 92 ms | 13 ms | 3 |
| compact, page close | 79 ms | 16 ms | 3 |
| prompt, context close | 68 ms | 5 ms | 3 |
| compact, context close | 74 ms | 5 ms | 3 |
| compact + background budget, context close | 72 ms | 7 ms | 5 |
Socket counts include idle/preconnect connections and are **not** asserted as busy
connection counts. Page close left one idle pooled TCP socket but no pending HTTP
observer; context close left zero in this run. New pages had one SSE and two TCP
sockets. Assertions are causal (B succeeds before gate release, cleanup occurs
before release); 10/30/60-second harness bounds detect deadlock only, not latency
regression thresholds.
## Remaining Linux → Windows qualification
The smallest useful next experiment needs an approved isolated Windows backend
and native storage/provider, plus the actual Linux Electron build. Bind that private
backend on a test interface restricted to the Linux host; never expose or use the
shared daemon. Record backend/UI/native/Electron/Chromium versions, exact scheme,
proxy/VPN configuration and whether authentication uses the remote-window path.
Concrete read-only external control from Linux, using the approved private URL:
```sh
curl --http1.1 --max-time 10 -sS -D private-meta.headers \
-o private-meta.json "$WINDOWS_PRIVATE_URL/api/meta"
```
Provide the isolated backend's normal authentication if required, without saving
credentials in the report. This curl process does not consume the renderer's
connection pool; compare its progress with renderer Network timings, not as proof
that the renderer is healthy. In Electron collect the owned window's existing
automation/CDP Network events (no fixed debugging port), protocol, connection IDs,
request initiator, queue/stall/connect/TTFB timings and one SSE subscription count.
Start with one real A compaction and B switch/draft/meta reads; count background
reads separately. Compare close/reopen page, close/reopen remote window and full
isolated Electron restart **without restarting Windows backend/native runtime**.
Capture admission abort, pending SDK reads, relay backlog, upstream execution and
backend event-loop progress before/after each boundary. Do not replay a timed-out
mutation. A genuine native 2.0.19 run must identify its binary/storage/provider;
the pinned 2.0.21 SDK is not evidence about that runtime.
The current harness is intentionally loopback-only and self-terminating; changing
`CODENOMAD_BROWSER_PATH` selects a local browser, not a remote backend. A persistent
LAN-controllable version requires a separately authorized fixture/launch change.

View file

@ -1,134 +0,0 @@
# #824 — diagnostic borné du bootstrap MCP navigateur
## Verdict
**Timeout de readiness reproduit une fois sur cinq ; cause non attribuée.**
Quatre reprises passent et une échoue avant les assertions MCP, avec le même
`page.waitForFunction(window.mcpFixture)` à 30 s que le fullbrowser antérieur.
Les cinq processus sortent naturellement ; pas de skip/force-exit/force-click.
Le suivi est terminé dans son budget, mais n'accepte ni la mission ni le gate
fullbrowser, qui conserve ses 356 pass / 3 fail / 1 skip.
La capture a une faiblesse explicite : son plafond commun de 4 000 observations
est saturé par les requêtes de modules **avant les diagnostics terminaux**. Elle
ne permet pas d'exclure une erreur/rejection tardive ni de dire si le module est
bloqué avant son corps, dans `applyUiSettings` ou dans `render`. Aucun défaut
produit confirmé, aucun correctif produit ou harness proposé comme déjà validé.
## Périmètre et méthode
2026-10-01, Windows x64, Node `v25.2.1`, Chromium `153.0.8010.12`, worktree
`D:/CodeNomad/.codenomad/worktrees/issue-824-performance`, HEAD `366830f6`.
`combined-validation` et `validation-closeout` sont déjà rapportées terminées.
**Aucun fichier produit, test ou fixture source édité.** Seul ce rapport est créé
dans le dépôt. Aucun audit ou fullbrowser supplémentaire, aucune dépendance/config/
lock modifiée, installation, commit/push/merge, restart/déploiement, suppression de
sessions, accès au daemon/base/profil utilisateur ou sous-agent.
Artefacts privés :
`C:/Users/Admin/AppData/Local/Temp/opencode/issue-824-mcp-bootstrap-4770677d4da549e98e255c79c7ecdba3/`.
Le script `diagnose.cjs` copie le texte exact de `mcp-motion.test.ts` en privé,
préserve assertions, timeouts, clics et options Chromium/Vite, puis compile cette
copie TypeScript avec l'esbuild déjà installé. Seuls les imports Node des packages
et la racine Vite sont rendus absolus, puisqu'elle n'est plus dans le dossier du
test original. Vite sert toujours les vrais modules/stylesheet et la fixture source.
Observations ajoutées en mémoire dans cette copie, sans catch qui transforme un
échec en succès ni `preventDefault` d'erreur :
- console, pageerror, request/requestfinished/requestfailed ;
- statut, headers/content-type, URL et type des réponses des modules Vite ;
- erreurs et unhandledrejection dans un init-script de page ;
- marqueurs privés par transform Vite : entrée du corps de fixture, avant/après
settings, avant render et après publication de `mcpFixture` ;
- tentative de snapshot terminal dans le finally, puis fermeture originale.
Le cache Vite conserve son emplacement/options ordinaires (`node_modules/.vite`),
pas une nouvelle politique de cache. Le plugin d'observation privé et l'observer
peuvent toutefois influencer timing et clé d'optimisation : ceci n'est **pas** un
A/B causal contrôlé du cache ou du coût de bootstrap. Aucune suite complète
concurrente repérée lors du contrôle read-only préalable ; les ciblés indépendants
du reviewer ne constituent pas une charge de l'hôte maîtrisée.
## Résultats exécutés — cinq reprises, pas davantage
Filtre exact :
`^eight connected MCP servers stay static at rest \(reduce, 150% scale\)$`.
Chaque processus exécute un seul cas avec rejets stricts et concurrence 1. Le
budget total du runner est **84,7 s**, sous dix minutes ; aucune relance après
ces cinq reprises et aucune assertion/durée artificiellement assouplie.
| Reprise | Résultat du cas | Exit | Naturelle | Processus, durée observée |
| --- | --- | --- | --- | --- |
| 1 | pass | 0 | oui | 11,25 s |
| 2 | **fail readiness 30 s** | 1 | oui | 41,03 s |
| 3 | pass | 0 | oui | 12,00 s |
| 4 | pass | 0 | oui | 9,63 s |
| 5 | pass | 0 | oui | 9,46 s |
Le runner global sort exit 1 car une reprise échoue : il ne masque pas le rouge.
Ces cinq exécutions d'un même scénario ne sont pas cinq scénarios de couverture
nouveaux et ne remplacent pas le gate complet antérieur.
## Ce que la capture démontre / ne démontre pas
Dans la reprise rouge :
- Le document `/mcp-motion` et `/tests/browser/fixtures/mcp-motion.tsx` répondent
**200**, respectivement HTML et JavaScript. Le serveur a appliqué la transform.
Une absence totale de réponse de ces deux entrées n'explique donc pas ce run.
- Le préfixe enregistré comprend **1 645 requêtes**, **1 175 réponses 200** et
**1 175 requestfinished**. Les modules optimisés Solid/debug répondent aussi 200.
De nombreux modules sont des icônes `lucide-solid/dist/source/icons/*.jsx`,
conformément à `optimizeDeps.exclude: ['lucide-solid']` existant.
- Les seuls messages console retenus sont `[vite] connecting...` et `connected.`.
Aucun pageerror, requestfailed ou statut >=400 n'apparaît **dans ce préfixe**.
Ceci ne prouve pas leur absence dans la totalité du run.
- Le plafond de 4 000 événements est atteint vers 10,14 s après début du processus
de test. Il est aussi atteint dans les quatre passes (8,42–10,93 s), donc sa
saturation n'est ni un discriminateur du timeout ni une cause de freeze établie.
- Le log Node démontre le timeout après `goto`, sur la readiness, pas une assertion
« huit serveurs ready », ni une animation perpétuelle du composant.
**Limite de capture confirmée** : les snapshots/stages, événements ready et erreurs
tardives partagent le même plafond avec le réseau. Ils ont été perdus, y compris
le snapshot terminal qui aurait permis de localiser la frontière. Ne pas convertir
cette absence d'observations en absence d'erreur produit. La quantité de modules,
une collision/rebuild du cache Vite, l'ordonnancement, la charge de l'hôte ou une
attente settings/render restent des hypothèses, pas des causes démontrées.
Le fixture source substitue l'inventaire Shell et le client MCP ; aucune compaction
native ou connexion à un vrai MCP/OpenCode n'est testée ici. Rien dans ces traces
n'attribue le freeze original #824 à MCP ni à Electron Linux → Windows.
## Empreintes et artefacts
Avant/après chaque reprise : **1 696 fichiers** tracked + untracked non ignorés,
sources/tests/scripts/configs/lock inclus, hors docs/générés/dist/public/deps/cache.
Pas de dérive et mêmes hashes de fichiers que la validation combinée. Le format
du manifeste privé (map path->SHA seulement) diffère du manifeste combiné enrichi.
`results.json` confirme `originalTestUnchanged: true`, `fixtureUnchanged: true`.
- `initial.json`, `<n>-before.json`, `<n>-after.json`, `<n>-result.json` : empreintes
et statuts. SHA initial `c4c4ff2bbc0c022b27258bda2ba085f79062f0cb982341b3ced09d0cd07adaa9`.
- `test-original.ts`, `fixture-original.tsx`, `test-observed.ts` et
`test-observed.mjs`, plus `harness.json` : provenance et modifications privées.
- `<n>.log`, `<n>-trace.json` : logs/traces complets **jusqu'au plafond**, pas traces
complètes de la vie de page. `trace-summary.json` identifie la saturation.
- `2-module-prefix.json` : réponses d'entrée et modules optimisés du rouge.
- `2.log` SHA256 `0830b1ee6d0b74c4640c434529227ce61193fcfe8ca01f41251bef114e264284`.
- `2-trace.json` SHA256 `d414677c96cb7ab5b4e2a79b18eff64fa6bf07539e5e205333c94725157e4332`.
- `diagnose.cjs` SHA256 `a6f4d4702d13624cf8c1123819ec0877da83b8381a07addbd7436fa84f195c1b`.
## Prochain minimum utile — décision du coordinateur, pas une relance ici
Autoriser un nouveau suivi **seulement si l'attribution reste nécessaire** : séparer
les événements critiques dans une réserve indépendante du réseau, conserver toujours
un snapshot terminal/stages/rejections, et agréger les réponses modules par URL/status
plutôt que laisser leur volume éjecter le diagnostic. Avec ce harness corrigé, capturer
une nouvelle occurrence avant toute décision de patch ou expérience causal-cache.
Ne pas changer les assertions, les 30 s, les gestes ni la politique produit pour
obtenir artificiellement du vert. Aucun patch produit minimal ne peut être recommandé
sur les preuves actuellement disponibles.

View file

@ -1,165 +0,0 @@
# #824 — capture critique du bootstrap MCP
## Verdict terminal
**5/5 reprises ciblées passent naturellement, timeout non reproduit.** La faiblesse
du collecteur est corrigée et vérifiée, et les cinq traces critiques sont complètes
jusqu'à fermeture, sans aucun overflow. L'attribution du timeout historique reste
ouverte : aucune nouvelle occurrence rouge n'a permis d'en localiser la frontière.
Travail de capture terminé, pas preuve d'élimination de panne ni gate global vert.
Le fullbrowser historique reste 356 pass / 3 fail / 1 skip ; le rouge MCP précédent
1/5 reste conservé, distinct de ce suivi. Aucune sixième reprise n'est lancée.
## Périmètre
Suivi borné de `browser-mcp-bootstrap-diagnosis`, pas une nouvelle validation
complète. Les dépendances `browser-mcp-bootstrap-diagnosis` et
`browser-focus-test-sync` sont terminales `completed`, sans exécution restante,
avant le lancement. Rapport focus-sync lu : ses deux ciblés sont finis, focus
2/2, mais timeout cache HTML indépendant au second run. Ses trois modifications
de tests/fixture font désormais partie de la nouvelle identité à fingerprinter.
Sources exclusivement :
`D:/CodeNomad/.codenomad/worktrees/issue-824-performance`.
Artefacts exclusivement privés :
`C:/Users/Admin/AppData/Local/Temp/opencode/issue-824-mcp-critical-20261001/`.
Le diagnostic précédent reste intact dans `issue-824-mcp-bootstrap-4770677d4da549e98e255c79c7ecdba3`.
Aucune édition produit/test/fixture/deps/config/lock ni politique de cache modifiée.
Seul ce rapport est créé dans le dépôt. Aucun install, commit/push/merge, restart,
déploiement, suppression de sessions, accès au daemon/base/profil utilisateur ou
sous-agent. Contrôle read-only des processus Node avant lancement : aucun ciblé
browser, runner de validation ou fullbrowser concurrent repéré.
## Seule modification expérimentale : le collecteur privé
Le harness précédent est recopié sans altérer ses imports/racine absolus,
assertions, délais 30 s, gestes, options Vite/Chromium ou marqueurs privés de
fixture. Il sert toujours les vrais modules et styles depuis le worktree.
Le réseau est désormais agrégé par événement/URL/status/type, avec compteur,
première/dernière heure et content-type pour les réponses. Plafond de 8 000 clés
distinctes, compteurs totaux et overflow réseau explicites. Pas de body HTTP lu
ni de modification de chargement ou optimisation lucide.
Réserves indépendantes : erreurs 512, lifecycle/ready 256 et consoles 256. Chaque
réserve compte son propre overflow. Les réponses >=400 sont aussi classées comme
erreurs ; les requestfailed/pageerror/test-error ne dépendent pas du plafond réseau.
Le snapshot terminal a son propre emplacement hors de tous ces plafonds, y compris
un résultat `observation-error` explicite si son évaluation ne réussit pas.
Les erreurs/rejections de page disposent d'un buffer 512 et d'un overflow explicite,
indépendants du réseau ; les cinq marqueurs de fixture restent conservés.
Le finally capture le snapshot avant fermeture, sans absorber l'échec du test.
Les observations peuvent influencer le timing : ce n'est pas un A/B causal du
cache, de l'ordonnancement ou du coût d'import. L'emplacement et les options du
cache Vite ordinaires sont conservés, pas nettoyés ou isolés artificiellement.
## Préflight sans navigateur
`collector-selftest.cjs` passe naturellement : 10 001 réponses synthétiques sur
un plafond réseau de deux entrées provoquent 9 999 overflows. Malgré cela,
pageerror, réponse 504, ready-before et snapshot terminal restent présents dans
leurs réserves ; l'overflow erreur indépendant est également vérifié.
`collector-selftest-result.json` conserve les résultats. Syntaxe du runner vérifiée.
## Budget et qualification
Filtre unique inchangé :
`^eight connected MCP servers stay static at rest \(reduce, 150% scale\)$`.
Rejets stricts, concurrence 1, au plus cinq runs / dix minutes, arrêt automatique
sur la première occurrence rouge avec snapshot/stages critiques sans overflow.
Pas de force-exit, skip, force-click ou assouplissement des assertions.
Le runner fingerprint avant/après les sources produit, tests, scripts, configs et
locks tracked + untracked non ignorés, hors docs/générés/deps/caches. Il conserve
les statuts exacts, heures, logs, traces et leurs SHA256 ; une dérive n'est jamais
présentée comme un gate immutable. Son texte, collecteur, copie observée et code
compilé ont leurs propres hashes dans `harness.json`.
## Résultats frais et localisation observée
Runner `sh_0f9561ee5001s5HSuUnJ0dLtTb` terminé, exit 0 naturel ; notification finale
reçue. Windows x64, Node v25.2.1, Chromium 153.0.8010.12. Cinq processus, un cas
chacun, zéro fail/skip/cancelled. Budget total 54,35 s ; pas d'arrêt sur rouge car
aucun rouge dans ce suivi. Ce ne sont pas cinq nouveaux scénarios de couverture.
| Reprise | Exit naturel | Durée processus | Entrée corps fixture, horloge page | Settings | Render → publication |
| --- | --- | --- | --- | --- | --- |
| 1 | 0 | 12,04 s | 10,323 s | 29,4 ms | 46,0 ms |
| 2 | 0 | 10,20 s | 8,619 s | 32,0 ms | 43,5 ms |
| 3 | 0 | 10,42 s | 8,819 s | 26,2 ms | 41,8 ms |
| 4 | 0 | 10,11 s | 8,637 s | 30,3 ms | 45,0 ms |
| 5 | 0 | 10,40 s | 8,880 s | 29,1 ms | 44,9 ms |
Dans les cinq passes : `fixture-body → settings-before → settings-after →
render-before → fixture-published`, puis `ready-after` observé. Snapshot final
`mcpFixture: true`, document complete, huit ready dots, 66 caractères au root.
La lecture ready prend 87,3–97,7 ms après son lancement ; `goto(domcontentloaded)`
dure 8,625–10,329 s sur l'horloge Node. Ne pas soustraire les horloges Node/page.
**Localisation du coût dans les passes**, pas de la panne : l'essentiel du temps
de navigation observé précède l'entrée du corps de fixture. Cette région comprend
fetch/transform/parse et évaluation des dépendances/imports, éventuellement leurs
attentes ; les traces ne répartissent pas ce coût. Settings et render arrivent
ensuite et achèvent en quelques dizaines de millisecondes. Cela ne prouve pas
que le timeout rouge antérieur bloquait dans cette même région plutôt que dans
settings/render/ready. Aucun profil CPU ni expérience causal-cache exécuté.
Chaque run conserve **1 837 requêtes, 1 837 réponses toutes 200, 1 837 finished**, soit
5 511 clés événement/URL/status/type, au-delà du vieux plafond commun 4 000 sans
perdre le snapshot. Zéro requestfailed, pageerror, test-error, erreur/rejection dans
les stages ; overflow réseau/erreurs/lifecycle/console/stages tous zéro. Ceci porte
sur ces cinq runs jusqu'au finally, pas sur l'ancien rouge ni sur une vie de page
illimitée après fermeture.
Les consoles **ne sont pas exemptes d'erreurs** : un message EventSource MIME
`application/json` au lieu de `text/event-stream` apparaît dans chaque passe,
avec quatorze warnings Solid « computations created outside a createRoot or render »
et deux messages Vite debug. Le MIME est cohérent avec la route fixture existante
qui remplace toutes les requêtes `/api/**` par `{}` JSON. Sa présence dans les cinq
passes ne démontre ni sa causalité dans le timeout ni son innocuité universelle.
Ces consoles et leurs emplacements sont conservés intégralement ; aucun filtre
pour fabriquer du vert.
## Identité fraîche et pièces terminales
Les **1 696 fichiers** ont les mêmes hashes à l'initial, avant/après chaque run et
au terminal ; zéro drift, y compris entre runs. Le SHA256 des manifests privés
`initial.json` et `terminal-manifest.json` est
`4628acfc4df16f8e573da48dab23df3614a7a8a55643466e5e6e8e4f6e80105b`.
Les seuls écarts avec le diagnostic précédent sont les trois fichiers attendus
de focus-sync (`git-history.test.ts`, `session-rendering.test.ts`, `user-html.tsx`).
Ils ne sont pas édités par ce suivi ; les anciens manifests restent historiques.
`harness.json` et `terminal-receipt.json` vérifient les SHA256 exécutés inchangés :
| Input privé | SHA256 |
| --- | --- |
| `diagnose.cjs` | `4ffab45b45c46e5d09e6d8f0e2b2259fe04cdfb41a02689826119fbba9b583bc` |
| `collector.cjs` | `914cfc1b5b8ab02a98b20157a5c1706056f6ab928b37f0f32bee03a8fe4d08b0` |
| `test-observed.ts` | `a76ae27a9fa8ed04c1f0536ab58b809208fda7edb90c7b27b9fff012db0f30da` |
| `test-observed.mjs` | `cce160f88fbba195c702e3134bc5a1a638936054dbe8148fe6c774357b380ca1` |
Test source original SHA256
`5820fe789d7470a5c6607998fbf7d068ed25df61611e496e4eec9968537669ec`,
fixture original SHA256
`f883fa1acd7318b10c5a517e4b1da0983fde384d2581e0046c7232312e299824`.
Les copies originales et leurs fichiers source sont inchangés.
`<n>.log`, `<n>-trace.json`, `<n>-result.json` conservent chacun leur hash vérifié ;
`results.json`, `trace-summary.json` et `terminal-receipt.json` indexent les cinq
runs, phases, réserves/overflows et sorties. `collector-selftest-result.json`
conserve le préflight. Aucun artefact antérieur n'est écrasé.
## Décision minimale suivante
**Pas de nouvelle boucle de diagnostic ni patch produit justifié ici.** Le
coordinateur peut utiliser ces traces complètes et le harness privé exact comme
capture MCP du gate unique s'il l'autorise. Sans occurrence rouge instrumentée,
l'attribution reste explicitement non qualifiée ; si elle est un critère obligatoire
de livraison, c'est un blocage honnête de ce critère, pas un succès déduit des retries.
Ni imports/lucide, cache Vite, charge hôte, settings ou render n'est une cause prouvée.
Conserver séparément cache HTML ouvert, Linux Electron → Windows LAN/2.0.19 et
sortie naturelle stores non qualifiés. La clôture de mission appartient au coordinateur.

View file

@ -1,156 +0,0 @@
# Issue 824 — correction de sérialisation de la trace Git
## Verdict
Le défaut introduit par l'instrumentation opt-in est corrigé uniquement dans
`packages/ui/tests/browser/git-history.test.ts`.
La cause est démontrée : sous le loader `tsx` réellement installé, le callback
TypeScript passé à `page.addInitScript(callback)` devient une fonction dont
`toString()` contient :
```js
const describe = __name(target => /* ... */, "describe")
```
Playwright sérialise le corps de la fonction mais pas le helper module-scope
`__name`. Une page Chromium minimale reproduit alors exactement
`ReferenceError: __name is not defined`, avec le même stack et une trace vide que
le gate final.
Le script est maintenant fourni par `page.addInitScript({ content })` sous forme
de source JavaScript explicite et autosuffisante. Aucun helper global n'est injecté
pour cacher l'erreur. Une validation Chromium minimale du contenu exact extrait du
fichier est verte : aucune erreur page, 200 entrées bornées après saturation, et
les observations focus, pointer/click et `widget-removed` restent actives.
Le gate final précédent demeure **non vert** : 360 total, 358 pass, 1 fail,
1 skip, exit 1 naturel. Aucun fullbrowser ni test ciblé Git/session/MCP n'a été
relancé dans cette tâche.
## Changement exact
Le callback transformable de `addInitScript` est remplacé par la constante
`gitFocusBoundaryInitScript`, un `String.raw` contenant une IIFE JavaScript sans
types ni closure externe. `describeElement` et `pushTrace` vivent entièrement dans
ce contexte navigateur.
Toutes les observations passent par `pushTrace`, y compris `widget-removed` ; la
limite de 200 entrées couvre donc désormais toute la chronologie, et pas seulement
les événements focus/pointer.
Le snapshot final est entouré d'un `try/catch` observationnel. Une évaluation de
snapshot indisponible est journalisée comme telle et ne masque plus une assertion
déjà en cours ; `page.close()` reste exécuté après ce bloc. Aucun changement aux
assertions, à `errors[]`, aux clics, textes, sélections, délais ou à la
synchronisation focus précédemment acceptée.
## Preuve rouge causale
Artefact privé :
`C:/Users/Admin/AppData/Local/Temp/opencode/issue-824-browser-trace-serialization-fix/red.json`.
Le fichier `callback-before.ts` recopie exactement le callback remplacé. Il est
importé par `node --import tsx`; la fonction obtenue est passée à
`page.addInitScript` dans une page `data:` privée.
Résultat :
```json
{
"hasExternalName": true,
"errors": ["ReferenceError: __name is not defined ..."],
"snapshot": { "trace": [] }
}
```
Le champ `serialized` de l'artefact conserve le JavaScript exact, dont
`const describe=__name(...)`. SHA256 `red.json` :
`1A3271EB26B265784BD84EBF2D4A7546508551BEF04237156CAEF779586C117F`.
Ce mécanisme est certain, et non une simple corrélation : la transformation exacte,
le binding externe absent, l'exception Chromium et l'état vide correspondent tous
au gate. L'ajout de `__name` vient de la transformation `tsx` effective ; aucune
hypothèse sur Vite, Monaco ou le produit n'est nécessaire.
## Preuve verte bornée
`validate.cjs` lit `git-history.test.ts`, extrait le contenu exact du
`String.raw`, puis l'injecte avec `{ content }` dans une page Chromium minimale. La
page alterne le focus 260 fois, émet `pointerdown` et `click`, puis retire un widget
`.git-change-context-widget` après installation du MutationObserver.
Résultat :
```json
{
"hasExternalName": false,
"errors": [],
"traceLength": 200,
"hasFocus": true,
"hasPointer": true,
"hasWidgetRemoved": true,
"lastKinds": ["focusout", "focusin", "pointerdown", "click", "widget-removed"]
}
```
Le budget saturé à 200 et l'entrée `widget-removed` finale démontrent à la fois la
borne globale et la persistance des listeners/observer après saturation.
Artefacts :
- `green.json`, SHA256
`D3024DB5252E029100F2C5D99F28A22F5F7AA186B43C254C0F1AC622CB6554F0` ;
- `validate.cjs`, SHA256
`9AB9E82D41BF8FAE98913DBF2BB9F22C8DA48C61335C47A32B04D7857F336087`.
Deux premiers lancements du runner privé n'ont pas atteint Chromium : le premier
ne résolvait pas `playwright` depuis TEMP, le second contenait des séparateurs de
ligne PowerShell littéraux. Ils ont échoué respectivement avec `MODULE_NOT_FOUND`
et `SyntaxError`; le runner final résout explicitement les dépendances depuis le
worktree. Ces erreurs de préparation ne sont pas comptées comme rouge/vert produit.
## Contrôles statiques et empreintes
- `npm run typecheck --workspace @codenomad/ui` : exit 0.
- `git diff --check -- packages/ui/tests/browser/git-history.test.ts` : exit 0,
avec le seul avertissement LF/CRLF existant.
- Aucun fichier produit modifié par cette tâche.
Manifest privé :
`C:/Users/Admin/AppData/Local/Temp/opencode/issue-824-browser-trace-serialization-fix/manifest.json`,
SHA256 `90405BE5AE1A1BF9C328B226AA3634FB7B49B7C5D754A4B23F569193FD551F32`.
Par rapport au gate final :
- `git-history.test.ts` :
`CD4776C4B035FE2C19A80BC7F562FC916A0B4F3388DAD7F1C0A13F683E18DEE4`
→ `DC7C378CC15E54F22F5D7E1A4C485313F0AF9DF536E1544690A33E3B2B2E5264` ;
- `session-rendering.test.ts` reste
`D50A2932039D3E44FA06BF1E02AA75D108A6C80520CE15724D229A1FEBA4E982` ;
- `fixtures/user-html.tsx` reste
`3D7616C20EDCFA32A7388E473C877B89089DA76FA482CD2E91C1160F95AAD631`.
Les quatre empreintes produit suivies restent celles du gate/focus-sync :
- `monaco-diff-viewer.tsx` : `3EB01E2C6D255E41A92FDCACBF946455B7A87293B19F3389D4FAE5C1381C3142` ;
- `session-view.tsx` : `733C02DE156A75ECD0D2F7C6DCD90A25050C0A1FC5D8EFD2F0E975D649C4E9AB` ;
- `prompt-input.tsx` : `9C8BE5BE1336A11E3188F94716DC3BAE6B63C1EB96BF20DF1F31913318345328` ;
- `markdown.tsx` : `51EE39FE0CC2FF5CF4258221B48919EF41AE9C3592450DF9D3FAF4EF0D2C3456`.
Ainsi, parmi les trois inputs qui distinguaient le gate final de la validation
combinée, seul le script Git opt-in change dans cette tâche. Le manifeste immuable
1 696 fichiers du gate reste une preuve historique, pas le manifeste du nouvel
état.
## Limites et prochain pas
La validation prouve la sérialisation, l'absence de `pageerror`, la borne et
l'activité de la capture dans un DOM minimal. Elle ne rejoue pas le composant
Monaco, les assertions Git ni le corpus complet, conformément à l'interdiction.
Le plus petit prochain pas est une décision du coordinateur : conserver le bilan
terminal comme non vert avec ce défaut de test causalement corrigé, ou autoriser
ultérieurement un unique gate d'acceptation sur une composition figée. Aucun retry,
patch produit ou promesse d'élimination universelle de #824 ne découle de cette
preuve.

View file

@ -1,261 +0,0 @@
# #824 — validation combinée fraîche
## Verdict terminal
**Le gate navigateur complet frais n'est pas vert** : 356 pass, 3 fail, 1 skip.
Serveur (après replay), typechecks, builds, assertions stores et fixture native
passent, sur sources inchangées. A/B frais positif. Le replay des trois cas passe
3/3 naturellement ; les huit suites ciblées passent **104/104** naturellement.
Le travail de validation et sa clôture sont **terminés**, pas « tous les gates verts ».
Aucun verdict
« tout vert » ne peut être déduit d'un succès ciblé. Les résultats ci-dessous ont
été exécutés ici, et ne recyclent ni les anciens 348 passes navigateur ni les
mesures des audits précédents.
2026-10-01, worktree `issue-824-performance`, HEAD
`366830f6b4373e6b283cabf7dd04764e87e1cc32`, modifications mission non committées.
Les quatre dépendances de revue sont completed ; la re-review indépendante a clos
M1. Aucun produit/test/config/lock/dependency édité par ce validateur ; seul ce
rapport et les artefacts privés de validation sont sa livraison. Aucun déploiement,
restart utilisateur, commit/push, install ou sous-agent.
## Identité des sources
Artefacts sous :
`C:/Users/Admin/AppData/Local/Temp/opencode/issue-824-combined-b91c8b66fcc840e1909170c5eeb846b9/`.
`initial.json` enregistre **1 696 fichiers** : Git tracked **et** untracked non
ignorés, y compris nouveaux tests/modules/scripts, configs et lockfiles. Parmi eux,
1 379 chemins dans les sources packages, 43 suites browser et 69 scripts. Hash des
octets bruts, pas de normalisation des fins de ligne. Exclusions : Markdown/PDF/docs,
dist/public/node_modules/cache/release/target. Les dépendances installées ne sont
donc pas fingerprintées ni modifiées ; environnement/exécutable Node enregistré.
SHA256 initial :
`16a3d4085dac90c27e892a480ae4c0735eb52782a9add8b0ca6732a640f1a4df`.
Chaque gate publie son manifeste avant/après, son statut, son log et leurs SHA256
dans `<gate>-result.json`. Sur les seize gates/tentatives terminés, **aucune dérive**,
y compris par rapport au
manifeste initial. La comparaison constate l'immutabilité aux bornes ; ce n'est pas
un verrou filesystem interdisant une modification transitoire suivie d'un retour.
## Gates frais terminés
Windows x64, Node `v25.2.1`. Tous les exits ci-dessous sont enregistrés, pas inférés
de l'apparition d'un dernier test vert. Les sous-gates inclus dans une suite complète
ne s'additionnent pas à sa couverture.
| Gate | Résultat | Sortie | Temps |
| --- | --- | --- | --- |
| serveur complet, premier run | 792 pass / 1 fail / 6 skip, 799 tests | naturelle, exit 1 | 27,8 s |
| test Git concerné isolé | 9/9 pass | naturelle, exit 0 | 14,1 s |
| serveur complet, replay | **793 pass / 0 fail / 6 skip**, 799 tests | naturelle, exit 0 | 30,8 s |
| typecheck UI | pass | naturelle, exit 0 | 8,2 s |
| typecheck serveur | pass | naturelle, exit 0 | 4,0 s |
| typecheck Electron | pass | naturelle, exit 0 | 1,2 s |
| build serveur, incluant UI + plugins pruning/automation | pass | naturelle, exit 0 | 26,0 s |
| stores/reducer/admission/pruning | **141/141 pass** | **force-exit**, exit 0 | 7,2 s |
| nouveau test store compactage sans force-exit | 11 assertions affichées pass, pas de résumé final | **timeout 30 s**, SIGTERM privé | non qualifiant lifecycle |
| event-source-handlers | 4/4 pass | naturelle, exit 0 | 0,23 s |
| diff-check tracked | pass, avertissements LF/CRLF seulement | naturelle, exit 0 | 0,04 s |
| fixture native complète, CLI explicite 2.0.21 | pass | naturelle, exit 0 | 63,4 s |
| navigateur complet | **356 pass / 3 fail / 1 skip**, 360 tests | naturelle, exit 1 | 21 min 38 s |
| A/B renderer, trois couples | 30 samples par variante | naturelle, exit 0 | 90,1 s |
| trois échecs navigateur, replay par noms exacts | **3/3 pass** | naturelle, exit 0 | 33,1 s |
| huit suites navigateur ciblées | **104/104 pass**, zéro skip/cancelled | naturelle, exit 0 | 306,4 s |
Le premier échec serveur est conservé intégralement dans `server.log` :
`git-worktree-config.test.ts`, test « checks a redirected main checkout when opened
from an ordinary linked checkout », échoue dans le **afterEach rmSync** ligne 40
sur un répertoire privé avec `EPERM`. Ce n'est pas une assertion Git échouée.
Les mêmes sources passent ensuite le test isolé et le replay complet. La cause du
refus Windows (processus Git/antivirus/autre) n'est pas démontrée ; ne pas effacer ce
run ni revendiquer deux suites complètes vertes.
Les six skips serveur restent les quatre opt-in WSL write (mode 0600, symlink loop,
conflit mode-only, directory-before-rename) et les deux permissions/symlink POSIX.
Ils ne sont pas couverts sur cet hôte. Le gate store emploie la convention existante
`--conditions=browser --test-force-exit`, avec rejets stricts : il qualifie ses
assertions, **pas le nettoyage du processus**. La tentative naturelle du fichier
nouveau n'a pas résolu cette limite connue de revue. Aucun timer arbitrairement
annulé/unrefé, aucun test/produit modifié pour obtenir un exit vert.
Le build réexécute `npm run build --workspace @neuralnomads/codenomad` : clean du
dist généré, Vite UI, copie public, compilation serveur et bundling des deux plugins.
Avertissements non bloquants : imports à la fois statiques/dynamiques, chunks >500 kB
et données Browserslist anciennes. Aucun rebuild/relaunch de l'application installée.
## Qualification native privée fraîche
Commande exacte avec rejets stricts :
```powershell
node --unhandled-rejections=strict scripts/test-session-pruning-native.mjs 'C:/Users/Admin/AppData/Roaming/npm/node_modules/@opencode/cli/bin/opencode.exe'
```
Le script vérifie **2.0.21**, lance son propre enfant `serve`, provider, config,
HOME/XDG et DB privés, et se termine naturellement. Il ne découvre/redémarre/arrête
pas le daemon utilisateur. Fixture conservée :
`C:/Users/Admin/AppData/Local/Temp/opencode/opencode/codenomad-pruning-native-BRHy0F`.
`native.log` confirme subscriber abort/return/throw sans invalider la connexion,
relay FIFO/slow recipients/shutdown, worktrees et cache, vraies routes proxy
prompt/command/Shell/inbox/cancel/Forms/permissions, pièce jointe 5 MiB, counts/search,
outline 1 501 messages, cleanup/retry/restore/fork, découverte et présence du plugin
bundlé. C'est **un fixture complet**, pas un nombre de tests à ajouter aux 799.
Le restart mentionné dans son log ne concerne que son enfant jetable.
Cela confirme le bundle et les intégrations sur runtime privé Windows, pas une vraie
charge utilisateur/provider, ni Electron Linux → Windows LAN/OpenCode 2.0.19.
Les deux anciens fixtures complets et la matrice native compaction du coordinateur
restent des preuves historiques distinctes, non additionnées aux résultats ici.
## Navigateur et A/B
Replay complet lancé avec `npm run test:browser --workspace @codenomad/ui`, budget
60 minutes, sans force-exit/force-click. Avant lancement : demande au coordinateur,
journal ancien non trouvé à son chemin exact, puis vérification read-only des
processus Node : aucune suite `tests/browser|test:browser` encore en cours. Aucun
processus partagé arrêté. Ce replay utilise les sources courantes, pas le vieux gate.
Le run complet termine naturellement (exit 1), aucun cancelled, aucun drift. Le
skip reste « Electron BrowserWindow native zoom retains tab seams and matching
scrollbars » : opt-in `CODENOMAD_TEST_ELECTRON` absent (`tab-chrome.test.ts:116`).
Échecs exacts conservés dans `browser.log` :
1. `git-history.test.ts:590`, « central diff inserts local lines and revision-qualified
history into the real session composer » : assertion ligne 609 `false !== true`
sur l'identité textarea/`document.activeElement` après clic Ajouter au prompt.
Les deux assertions de texte/référence précédentes passent ; pas de perte de
brouillon démontrée par cet échec.
2. `mcp-motion.test.ts:47`, « eight connected MCP servers stay static at rest
(reduce, 150% scale) » : timeout 30 s sur `window.mcpFixture` avant assertion
de géométrie/animation. Cause du non-montage non démontrée.
3. `session-rendering.test.ts:82`, « user HTML mode preserves Markdown, pasted
disclosures and code source and fences legacy caches » : timeout 30 s ligne
104 sur visibilité `#cache .cached-html` après `fixture.literal(false)`.
Les gates compactage/SSE/HTTP/membership et le sample instrumenté sont verts dans
ce même run, mais une régression session-rendering voisine ne l'est pas. Les scopes
ne s'additionnent pas aux 360 tests. Le replay ciblé n'altère ni code, timing bounds,
clics, exits ou assertions et doit être rapporté séparément.
### Disposition des trois échecs au replay
`browser-failures.log` et `browser-failures-result.json` : 3 tests, 3 pass, zéro
fail/skip/cancelled, rejets stricts, exit 0 naturel en 33,1 s. Manifeste avant/après
identique à l'initial. Filtrage des noms exacts dans les trois mêmes fichiers,
**sans modifier leurs fixtures, délais, clics ou assertions**. Ce constat suffit
pour dire « non reproduits dans ce replay », pas « résolus » ni « causes connues ».
| Échec complet | Démontré | Pas démontré / prochain élément minimal |
| --- | --- | --- |
| insertion Git, focus | contenu et référence insérés ; focus non revenu au textarea à l'instant de l'assertion ; replay pass | Ordre/timing exact de focus non capturé. En cas de récurrence, enregistrer activeElement et les callbacks de restitution de focus autour du clic, sans changer l'assertion. |
| MCP readiness | `window.mcpFixture` absent après 30 s dans le complet ; replay pass | Cause d'échec de bootstrap non capturée. Capturer pageerror/console/requestfailed et réponses modules Vite au montage, avant d'attribuer au cache ou au produit. |
| user HTML/cache | `#cache .cached-html` pas visible après passage literal(false) ; replay pass | DOM/cache/promise de rendu à cette frontière non capturés. Capturer DOM, état literal et résolution du rendu avant/après toggle au prochain échec. |
Scheduling/charge de l'hôte, optimisation Vite ou état de cache sont des pistes,
pas des causes démontrées. Aucun patch proposé pour fabriquer un gate vert. Une
nouvelle suite complète éventuelle relève de la décision du coordinateur après
attribution ; le suivi de clôture ne lance aucune suite supplémentaire.
### Suites ciblées existantes récupérées à la clôture
Le runner déjà lancé `validate.cjs browser-failures focused` (shell
`sh_0f93a730b001Ofu9KyF5KkL76a`, PID 41540 lors du contrôle) est terminé ; son
résultat final a été reçu, pas inféré du dernier test affiché. Aucun nouveau run
de validation ou audit n'a été lancé lors de `validation-closeout`.
`focused-result.json` : exit 0 naturel, rejets stricts, 306,4 s, sources avant/après
et par rapport à l'initial inchangées. `focused.log` : **104 tests, 104 pass, 0 fail,
0 skip, 0 cancelled**. Détail non additif : compactage 5, isolation SSE 5, HTTP/1
admission 5, render-cost 13, history-navigation 21, session-rendering 46, task-copy 6,
tool-images 3. Le cas user HTML passe donc aussi dans sa suite voisine complète,
pas seulement sous le filtre des trois retries. Sample 80 shells sans remount et
compteurs/contenus compactage exacts restent vérifiés. Ni ce gate, ni les trois
retries ne changent le statut du fullbrowser à exit 1.
## Pièces terminales et décision suivante
`terminal-gates.json` indexe les seize statuts, commandes exactes, heures, sorties
naturelles/forcées, SHA256 des logs/manifests et dérives. Le runner privé final est
conservé sous `validate.cjs`, SHA256
`66d119cfe89bd824e3568e5a0447106e06fc5633c2cb0456a86515d9caecb064`.
Principaux logs SHA256, dans le répertoire d'artefacts ci-dessus :
| Fichier | SHA256 |
| --- | --- |
| `browser.log` | `19099de5e3c3ec694334c4667575715d8dc2dc906f80e558cf54773ea0880a1b` |
| `browser-failures.log` | `6035e5c53b01dc30cc936d6183d8625ebda890908c5ffe7b24d63e9c8010f130` |
| `focused.log` | `bac7cadaf82a5cbf238851a1f5f86eac1b6b431352c0dc5e3161e9db75752628` |
| `server.log` (premier échec) | `d8660dcba0935f9a94fdb965eb9a7723a0f9d8000db18e6ea79bbcce96c55247` |
| `server-rerun.log` | `8fafb4e813b85cfd8abc74bd7e1e1f6f4b4a9df38bb4a6f7934d9781923f84de` |
| `native.log` | `e74886aee42300ee4543a6f2518bc8b714aeec4922f428f4f8ea113efa994ee7` |
| `ab.log` | `0d2e7cb864ff89060d552a4aafa3ed0d1da7f1ba2ca17c7fa7068b1d32be9205` |
Le suivi propriétaire a récupéré ces artefacts, finalisé seulement ce rapport et
son index privé, sans edit produit/tests/config/deps, installation, merge, commit,
push, suppression de sessions, restart ou déploiement. Les travaux de validation
peuvent être rapportés `completed` ; **la qualification globale reste non verte**.
Le coordinateur décide de la clôture de mission, pas ce validateur.
Prochain minimum utile : conserver les trois preuves d'échec global et les retries
séparés ; si un fullbrowser vert est un critère de livraison, autoriser un suivi
instrumenté des trois frontières décrites plus haut, puis décider d'un nouveau
gate complet figé. Ne pas élargir les corrections #824 sur simple hypothèse de
flake. La sortie naturelle stores et Linux Electron → Windows LAN/2.0.19 demeurent
des limites explicites, pas des gates implicitement acquis.
### A/B CPU du postimage M1 actuel
`node scripts/test-render-cost-audit.mjs 3` passe après le fullbrowser : trois
couples alternés baseline/fixed, 80 outils × 8 000 caractères, trois warmups puis
dix deltas chacun, **instrumentation off**. SHA produit testé
`f07ce171a6b1d3394ce94be496b0abd67f8bc0bb48e946eb9173a681c844f401`.
Baseline `47f9e43e` a le même blob renderer que `366830f6` (preuve précédente).
| ms/delta, médiane [min–max], écart-type échantillon | Baseline | Postimage courant |
| --- | --- | --- |
| main-thread TaskDuration | 324,57 [168,06–743,53], 173,67 | 16,94 [9,93–50,33], 11,27 |
| ScriptDuration | 14,51 [7,88–46,20], 9,03 | 0,25 [0,11–32,37], 9,34 |
| dispatcher sync | 12,05 [6,70–19,90], 3,70 | 0,20 [0,10–0,50], 0,08 |
| layout | 2,88 [1,51–4,27], 0,55 | 0,08 [0,07–0,23], 0,04 |
| recalcul style | 10,20 [5,74–13,91], 1,82 | 0,13 [0,10–0,30], 0,05 |
30 échantillons par variante. CDP cumule jusqu'à deux animation frames ; ce n'est
pas une mesure de latence-frame, FPS, heap, débit ou Electron production. Host
activity non contrôlée ; pas de seuil absolu CI. Ces chiffres sont nouveaux et
séparés des A/B antérieurs au correctif membership. Le fullbrowser instrumenté
indépendant compte **80 clones / 0 added / 0 removed** dans chacun de dix deltas,
avec assertion anti-remount ; les valeurs zéro du run instrumentation off ne sont
pas des compteurs de travail réel.
Artefact A/B :
`C:/Users/Admin/AppData/Local/Temp/opencode/codenomad-render-cost-srSQOf/metrics.json`,
SHA256 `c72a491eba14d5e71178ba2895a7304ce07f98a02d340a53ef9f75f4424b7cd0`,
six logs bruts adjacents. `generated-and-ab-hashes.json` identifie aussi les deux
plugins bundlés utilisés par la reprise native.
## Limites et tailles signalées
- Pas de qualification Electron Linux, LAN/VPN ou runtime 2.0.19, ni preuve
d'élimination universelle du freeze signalé. Le test HTTP/1 Chromium est Windows
loopback ; native et browser constituent des périmètres distincts.
- Buffer compactage sans plafond explicite octets/événements dans une tâche sans
yield ; le délai 250 ms n'est pas une garantie de mémoire bornée.
- Scan task structurel à chaque révision conserve les bornes 10 000/200 mais peut
coûter du CPU même sans changement de membership. Le sample 80 est borné, pas
une mesure du pire cas ni de heap/FPS/production desktop.
- Un pas SQLite reste synchrone malgré le budget coopératif du scanner outline.
- Le hashing async utilise toujours le pool libuv et le CPU/mémoire disponibles.
Surfaces modifiées par la mission au-dessus du seuil de signalement (~500 lignes),
sans refactor de taille par cette validation :
- `packages/server/src/server/http-server.ts` ~2 346 lignes ;
- `packages/server/src/workspaces/manager.ts` ~1 240 ;
- `packages/ui/src/stores/instances.ts` ~2 093 ;
- `packages/ui/src/stores/opencode-data.ts` ~828 ;
- `packages/ui/src/components/tool-call/renderers/task.tsx` ~569.

View file

@ -1,124 +0,0 @@
# Issue #824: compaction delta reduction correction
## Outcome and scope
Native compaction fragments now aggregate **before** the OpenCode Solid reducer,
not merely before the visible transcript projection. No server, transport,
`task.tsx`, daemon, user profile or desktop deployment changes belong to this
correction. The preceding large synchronous backlog is diagnostic evidence,
not an acceptance gate or proof of real provider delivery frequency.
The implementation follows installed `@opencode/client@2.0.21` declarations and
its actual Solid reducer: `session.compaction.delta.data.text` appends to the
running row's `summary`; `ended.data.text` replaces it authoritatively; `failed`
replaces the running row with an error record. Durable start/end metadata is not
changed or synthesized.
## Product changes
- `stores/compaction-delta-buffer.ts`: one plain accumulator and one 250 ms timer
per instance/session. It uses the exact native delta type and joins chunks in
arrival order. The interval begins on the first chunk, so later chunks cannot
postpone publication indefinitely.
- `stores/opencode-data.ts`: incoming fragments immediately advance CodeNomad
message/full-data revisions and fence SDK reads, but emit one aggregate to
the reducer per interval. Aggregate reentry does not advance admission twice.
Same-session non-delta events flush preceding chunks to retain native order;
unrelated sessions/instances cannot force a flush. End/failure, delete,
committed revert, pruning invalidation, disposal and reconnect cancel pending
content where its authority has ended. Idle flushes before reducer retirement.
- SDK history reads flush pending chunks before establishing read authority and
before accepting responses. Otherwise a native page fetched after admission
could already contain buffered text that would be appended a second time.
A stale pre-delta page is rejected and receives the existing trailing read.
- Inactive sessions with neither a resident transcript nor a native observer
do not materialize compaction payloads. Only their scalar admission fences and
an unfinished-compaction marker remain. Final activation uses the native page.
Activation during an unfinished skipped start recovers the running row via the
existing authoritative resync path, rather than inventing a start/summary.
The SDK's 20-row resync preserves the already hydrated native 200-row window.
Subsequent streaming resumes interval reduction without duplication.
- `stores/instances.ts`: remove the old post-reducer timer. Deferred aggregate
application projects once immediately; inactive loaded views remain invalidated
but are not hydrated. Client replacement/removal destroys the owned native
projection, including its pending buffer, rather than cancelling only the
now-removed visible projection timer.
## Deterministic red/green evidence
The browser fixture instruments the real native emission and visible projection
boundaries through a test-only Vite transform. There is no product telemetry API,
mock reducer or render reimplementation. Both the full stylesheet and real
`SessionView` are mounted. HTTP/client responses are private fakes, and browser
requests outside loopback are blocked.
| Contract | Baseline `366830f6` | Corrected |
| --- | ---: | ---: |
| 128 tiny active chunks, immediate reducer emissions | 128 | 0 |
| Same burst, after first interval | 128 native emissions | 1 emission + 1 visible projection |
| 8 chunks spaced by 10 ms, first interval | 8 emissions | 1 emission + 1 projection |
| Second spaced interval | another 8 emissions | another 1 emission + 1 projection |
| 128 chunks for never-loaded inactive compaction | 128 emissions | 0 emissions, 0 projections, 0 message-list reads |
| Running compaction activated midway, subsequent 2 chunks | 2 emissions | 1 emission + 1 projection, exact cumulative text |
| Loaded but now inactive session, 4 chunks | 4 emissions | 1 emission, 0 hidden-view projections |
All five baseline browser assertions fail by exact counts, not timing or timeout.
All five corrected cases pass, including exact authoritative final text,
remount, immediate per-arrival revisions, no duplicate chunks, and active-session
isolation. Total active burst text is only 530 characters, not the preceding
32 MiB corpus. Spaced delivery uses genuine distinct task/timer intervals through
the native dispatcher, not one giant callback.
Descriptive ingress sample: baseline 128 chunks took **1.6 ms**, corrected
**0.4-0.5 ms** on the same host with other mission activity. These are sampled
development-build dispatcher durations, not controlled CPU benchmarks, frame
latency, or a claim of eliminating every freeze. No Long Task threshold remains.
## Executed gates
```powershell
# Current browser regression (5 pass)
node --import tsx --test --test-concurrency=1 packages/ui/tests/browser/compaction-responsiveness.test.ts
# Neighboring real-renderer/window/navigation regressions (67 pass)
node --import tsx --test --test-concurrency=1 packages/ui/tests/browser/history-navigation.test.ts packages/ui/tests/browser/session-rendering.test.ts
# Native reducer/admission/pruning gates (141 pass, including 11 new compaction cases)
node --conditions=browser --import tsx --test --test-force-exit packages/ui/src/stores/opencode-compaction.test.ts packages/ui/src/stores/opencode-data.test.ts packages/ui/src/stores/opencode-data-idle.test.ts packages/ui/src/stores/opencode-data-settlement.test.ts packages/ui/src/stores/session-native-events.test.ts packages/ui/src/stores/session-request-authority.test.ts packages/ui/src/stores/session-pruning-pagination.test.ts packages/ui/src/stores/session-pruning-projection.test.ts packages/ui/src/stores/session-pruning-events.test.ts
npm run typecheck --workspace @codenomad/ui
git diff --check
```
All commands above pass. The first combined browser invocation had a 120 s
harness timeout after 17 passing cases; it is not counted as complete. Re-running
the neighboring suites with a 360 s allowance completed all 67 cases naturally.
Unit gates use the repository's browser-condition/force-exit convention because
imported store timers persist. Diff check has only ordinary LF/CRLF warnings.
To reproduce baseline red without changing the checkout, extract the two
`366830f6` blobs `packages/ui/src/stores/instances.ts` and
`packages/ui/src/stores/opencode-data.ts` into a private temporary directory.
Set `CODENOMAD_COMPACTION_BASELINE` to it, then run the five browser cases. Vite
substitutes those blobs read-only and applies the same exact counters. Leave the
variable unset for ordinary corrected runs. Both old and corrected code use the
same dependencies, components and fixtures.
## Remaining limits / review targets
- This establishes avoided reactive work under modest separate event delivery;
it does not qualify real EventSource wire traffic or Electron/Linux against a
Windows backend. The companion transport/native diagnostics remain distinct.
- Same-session non-delta events and native reads intentionally force a flush.
A workload continuously interleaving those boundaries can reduce the coalescing
ratio; ordering and read authority win over a universal four-flushes/s promise.
- Mid-compaction activation uses the existing quiet/revision-stable native resync
policy. Sustained arrivals can delay its authoritative catch-up; the already
hydrated visible native page remains displayed. This patch does not replace
that broader starvation policy or merge arbitrary in-flight history snapshots.
- No user database, daemon, provider calls, native-host timing, heap soak or
deployment/restart was used. Source/token loss boundaries are covered by mocked
deterministic native records, not an end-to-end real provider compaction run.
- File-size warning: touched `stores/opencode-data.ts` is approximately **828**
lines and `stores/instances.ts` **2,093** lines. The new narrow buffer module is
56 lines; no size-only refactor was attempted.

View file

@ -1,112 +0,0 @@
# Issue 824 — revue des barrières d'événements
## Verdict
Revue indépendante, bornée au diff depuis `366830f6`, de
`packages/ui/src/lib/server-events.ts`,
`packages/server/src/workspaces/opencode-service.ts` et de leurs nouveaux tests.
La correction serveur est sûre pour le modèle de partage réel de
`@opencode/client` 2.0.21 : l'arrêt d'un abonné ne détruit plus la connexion
logique partagée, tandis qu'une vraie fin ou erreur amont invalide encore la
génération qui l'a produite. La correction UI isole correctement une exception
synchrone levée directement par un callback et conserve l'ordre FIFO. Aucun
blocage fonctionnel n'a été trouvé dans ces deux corrections.
Il reste toutefois une limite UI confirmée et une faiblesse de diagnostic à ne
pas présenter comme entièrement résolues.
## Constats
### M1 — une erreur Solid différée au flush échappe encore à l'isolation par abonné
`ServerEvents.notify()` entoure l'appel direct du callback, mais
`dispatchBatch()` entoure tous les callbacks d'un `solidBatch`. Une dérivation
Solid déclenchée par un setter peut donc lever seulement lors du flush final de
`solidBatch`, après le retour de `notify()`. Cette erreur sort à la ligne du
`solidBatch`, puis est capturée par le `catch` englobant de
`attachEventSourceHandlers`, qui la journalise à tort comme `Failed to parse
event`.
Reproduction isolée avec la distribution navigateur de Solid : un
`createMemo` qui lève après `setSignal()` imprime `after-set`, puis l'exception
est capturée autour de `batch()`, pas autour du setter. Le fixture navigateur
actuel ne couvre qu'un `throw` direct dans le callback et affirme justement
l'absence de `Failed to parse event`; il ne détecterait pas cette variante.
Impact borné : les callbacks natifs et typés du même lot sont invoqués avant le
flush, donc l'événement n'est pas supprimé pour ces abonnés. En revanche, le
flush réactif peut être interrompu et le diagnostic accuse le transport. Le
plus petit suivi utile est un test navigateur où un abonné écrit un signal dont
une dérivation lève, puis une décision explicite sur la frontière de batch
(isoler aussi le flush par abonné, ou au minimum séparer le diagnostic de
dispatch du parsing).
### L1 — le diagnostic ajouté est désactivé par défaut
`log.error()` utilise le logger `debug` de l'espace `sse`; les espaces sont
désactivés par défaut. Les tests remplacent directement `getLogger("sse").error`
et prouvent que la méthode est appelée, mais pas qu'un utilisateur ou un rapport
de diagnostic voit l'échec en configuration normale. De plus, `kind` distingue
le type d'événement mais pas le bucket wildcard/typé ni l'abonné fautif.
La capture est préférable à la rupture du flux et ne constitue pas un bug de
réactivité. Si ces exceptions doivent être exploitables sur le terrain, ajouter
un canal borné réellement collecté et un identifiant de catégorie d'abonné,
sans contenu d'événement ni rejeu.
### L2 — course abort local / panne amont non déterminisée
Le choix `if (!signal?.aborted) invalidateConnection(connection)` donne à
l'abort local la priorité lorsqu'il est simultané à une erreur de `next()`.
Avec `SharedEvents`, l'abort retire l'abonné local; une panne physique peut alors
être observée seulement par un autre abonné direct, sans invalider immédiatement
`OpenCodeSharedService`.
Le risque paraît faible et auto-récupérable : la tentative suivante du bridge
rouvre le flux partagé; son échec non annulé invalide alors la connexion, et les
mutations ordinaires invalident également sur erreur. Ne pas modifier la logique
sans reproduction. Ajouter au besoin un test à barrières contrôlées où abort et
rejet amont concourent, puis vérifier qu'une reconnexion échouée renouvelle bien
le client.
## Propriétés vérifiées
- Le SDK 2.0.21 partage un seul flux physique et maintient des files par abonné;
`return()`/abort suppriment seulement l'abonné et arrêtent la source uniquement
quand il ne reste aucun abonné.
- `invalidateAfterStream()` distingue correctement :
- erreur/EOF de `iterator.next()` non annulé : invalidation;
- `return`, `throw` consommateur ou abort local : nettoyage par
`iterator.return()` sans invalidation;
- événement d'une ancienne génération : `assertCurrent()` rejette avant le
`yield`, et l'invalidation conditionnelle ne peut pas évincer le remplacement.
- Le `finally` appelle toujours `iterator.return()`; avec le SDK réel cet appel
est résolu sans rejet.
- Le dispatch UI continue après un `throw` direct, sans rejeu, pour les abonnés
wildcard, typés, open et status; le test FIFO/unsubscribe traverse le vrai
EventSource, les routes SSE et le réducteur natif.
- Les callbacks enregistrés dans le produit sont synchrones; aucun abonné
`async` actuel ne crée de rejet de promesse non capturé à cette frontière.
## Validation exécutée
- `node --import tsx --test --test-concurrency=1 packages/ui/tests/browser/event-subscriber-isolation.test.ts` — **4/4 vert**.
- `npm exec --yes tsx -- --test packages/server/src/workspaces/subscriber-lifetime.test.ts packages/server/src/workspaces/opencode-connection-generation.test.ts` — **10/10 vert**.
- `node --import tsx --test packages/server/src/workspaces/opencode-service.test.ts` — **14/14 vert**.
- `node --import tsx --test packages/server/src/workspaces/instance-events.test.ts packages/server/src/workspaces/instance-event-routing.test.ts` — **31/31 vert**.
- `npm run typecheck --workspace @neuralnomads/codenomad` — vert.
- `npm run typecheck --workspace @codenomad/ui` — vert.
- `git diff --check` sur les fichiers revus — vert (seuls avertissements de
conversion LF/CRLF du worktree).
Le lancement global `test:browser` avec un filtre placé après le glob a été
interrompu à 120 s car le runner a commencé d'autres fixtures; il ne constitue
pas un échec de test. Le fichier navigateur ciblé a ensuite été exécuté seul et
est vert.
## Taille des fichiers touchés
Aucun fichier produit revu ne dépasse le seuil d'avertissement :
`server-events.ts` ~219 lignes et `opencode-service.ts` ~360 lignes. Les nouveaux
tests font respectivement ~123 et ~116 lignes.

View file

@ -1,139 +0,0 @@
# Issue #824: HTTP preflight cancellation
## Scope and result
Baseline: `366830f6`; original red diagnostic retained in commit `1ece148c`.
This fixes **orphaned HTTP preflight observers**, not the original global freeze.
Initial implementation is in `issue-824-transport`; the seven paths are now
integrated uncommitted in `issue-824-performance`. No shared daemon, database,
profile, desktop restart or deployment is used.
When prompt/compact waits on a native ownership read, disconnecting the desktop
previously left that read active because disconnect tracking began only after
authorization. The original HTTP fixture failed for both operations while an
independent session answered in 12/3 ms. This shows a cancellation defect, not
proof that native compaction stalls that read in the user's environment.
## Small correction
- `server/request-admission.ts` observes request `aborted` and incomplete
response `close` from the start of the instance proxy handler. It does not
observe ordinary request `close`, which can mean a completely read POST body.
- `http-server.ts` propagates the signal to profile negotiation, session,
project, PTY/Shell and supported location-ownership native reads. Waiting on
shared acquisition or non-cancellable filesystem/inventory work races only
this observer against cancellation; it neither cancels nor invalidates the
shared service. Late continuations cannot advance admission.
- The scope is disposed on every return/error. Mutation fence acquisition and
environment/native dispatch have abort guards. The existing 15-second
environment deadline and two-second advisory Git-context deadline remain;
there is no new timeout on prompts, compaction or execution.
- The existing forwarding scope installs its response-close handler before
dispatch, without an asynchronous gap; the early scope remains until that
handoff settles. `compatibility/proxy.ts` now refuses dispatch when its initial
downstream check already found a disconnected response.
- No interrupt, replay or daemon cancellation is added. Disconnect after an
accepted prompt/compact continues to abandon its HTTP response as before;
an upstream operation already admitted can finish independently.
- `WorkspaceManager.ownsLocation` accepts an optional signal for its native
location check. Existing directory and identity authorization is unchanged.
## Executed validation
Windows, Node 25.2.1, private ephemeral loopback listeners. The junction created
for the earlier diagnosis was removed with `cmd /c rmdir node_modules` (junction
only), followed by a clean `npm ci` in this worktree. The prior missing-dependency
typecheck was an installation-state limitation, not a demonstrated source defect.
`npm ci` reports 49 dependency vulnerabilities; no out-of-scope upgrade is made.
Commands:
```powershell
npm ci
npm run typecheck --workspace @neuralnomads/codenomad
node --unhandled-rejections=strict --import tsx --test packages/server/src/server/__tests__/instance-proxy-disconnect-diagnostic.test.ts packages/server/src/server/request-admission.test.ts packages/server/src/server/__tests__/instance-proxy.test.ts packages/server/src/server/__tests__/instance-proxy-legacy.test.ts packages/server/src/opencode/compatibility/proxy.test.ts
$env:TEMP='C:\Users\Admin\AppData\Local\Temp\opencode'
$env:TMP=$env:TEMP
node --unhandled-rejections=strict --import tsx --test "packages/server/src/**/*.test.ts"
git diff --check
```
- Server typecheck passes after clean installation.
- Focused run: **65/65 pass**, natural exit. Twelve real-HTTP regressions and
three scope tests include actual Promise SDK HTTP cancellation for prompt and
compact, independent-session response while a preflight is held, delayed
connection/profile/ownership, pre-environment snapshot, environment write,
no forwarding after abort, no connection invalidation/replay, admitted
operation completion, deletion-fence release and listener disposal.
- Timing thresholds were removed from the independent-session assertion: it
must finish while the first read is causally held. Descriptive final aggregate
latencies are **22/12 ms**, not a performance guarantee.
- Entire server gate: **782 tests, 776 pass, 0 fail, 6 declared skips**, natural
exit in ~34.7 s. Skips: four opt-in native WSL write tests and two POSIX file
permission/symlink tests. This count includes the focused cases, not additional
distinct coverage. Full log: `sh_0f8691e64001fwNIml1REbA1tC.out` in the harness's
managed shell storage.
- No full desktop/UI build or native Linux-to-Windows qualification is claimed.
## Remaining limits
Shared service acquisition and shared inventory work may continue after one
observer leaves; cancelling those would harm other consumers. The local route
retires immediately and refuses any late mutation admission. A buffered request
body is parsed before the route handler; this patch does not change parser cost.
Next: independent review of cancellation/forwarding ownership, followed by
integration validation. Separately measure real compaction, native session-read
latency and Chromium HTTP/1 connection occupancy using a private runtime. This
patch alone does not establish or eliminate the reported application-wide freeze.
Touched oversized sources: `server/http-server.ts` ~2346 lines,
`workspaces/manager.ts` ~1240 lines. No unrelated size-driven refactor is made.
## Integration validation
Source: `d9598e42` on `fix/issue-824-transport`, including the original diagnostic
from `1ece148c`. Destination:
`D:\CodeNomad\.codenomad\worktrees\issue-824-performance` at baseline `366830f6`.
All seven paths were applied with the patch tool, without cherry-pick, commit or
push. The three modified production files matched the untouched destination
baseline before integration. All seven Git blob hashes then matched the source
commit exactly; only this report was subsequently extended with integration
evidence. No neighboring concurrent changes were rewritten.
Integrated paths:
- `packages/server/src/server/http-server.ts`
- `packages/server/src/workspaces/manager.ts`
- `packages/server/src/opencode/compatibility/proxy.ts`
- `packages/server/src/server/request-admission.ts`
- `packages/server/src/server/request-admission.test.ts`
- `packages/server/src/server/__tests__/instance-proxy-disconnect-diagnostic.test.ts`
- `dev-docs/issue-824-http-admission-fix.md`
The existing clean integration dependencies were reused without installation.
Executed from the destination, Windows / Node 25.2.1:
```powershell
node --unhandled-rejections=strict --import tsx --test packages/server/src/server/__tests__/instance-proxy-disconnect-diagnostic.test.ts packages/server/src/server/request-admission.test.ts
node --unhandled-rejections=strict --import tsx --test packages/server/src/server/__tests__/instance-proxy-disconnect-diagnostic.test.ts packages/server/src/server/request-admission.test.ts packages/server/src/server/__tests__/instance-proxy.test.ts packages/server/src/server/__tests__/instance-proxy-legacy.test.ts packages/server/src/opencode/compatibility/proxy.test.ts
npm run typecheck --workspace @neuralnomads/codenomad
git diff --check -- packages/server/src/server/http-server.ts packages/server/src/workspaces/manager.ts packages/server/src/opencode/compatibility/proxy.ts
```
Results: **15/15** scope/disconnect and **65/65** focused pass with natural exits;
server typecheck and scoped diff-check pass. The 15 cases are included in the
65-case run, not distinct extra coverage. Descriptive witness latencies are
12/9 ms in the first run and 10/9 ms in the focused run. No full integration
server gate was rerun here; the coordinator's fresh aggregate validation follows
the remaining integrations.
Guard verification: the outer entry checks abort before acquisition begins;
`wait()` checks again after each resolved boundary before the caller continues;
`clientForRequest()` checks before allocating shared-client work. Explicit guards
precede mutation-fence entry, environment write, fetch acquisition handoff and
forward dispatch. There is no asynchronous gap between the final forwarding
guard and installation of its close listener. Late shared completion is exercised
by held connection/profile/ownership/environment tests and cannot advance to a
later effect. Work already admitted to the upstream remains outside this local
observer's interruption authority.

View file

@ -1,280 +0,0 @@
# Issue #824 — private native compaction isolation
## Result
**No cross-session service freeze reproduced in this bounded scenario on
OpenCode 2.0.21 / Windows.** A genuine OpenCode session compaction waited on a
deliberately held loopback summary response. While it was still waiting,
session B accepted a prompt, executed its model request and completed
successfully. Native session reads, activity and server info remained available.
The subsequent large-context qualification below sends verified 2/8/32 MiB
tool payloads from **A itself** to its compaction provider. It observes transient
pre-provider latency (up to ~85 ms in the outline-disabled matrix), but no
persistent cross-session freeze. This supersedes only the original size limit,
not its separate outline measurements.
This result separates provider waiting from service starvation; it does not
identify or disprove the reporter's root cause. It does not qualify OpenCode
2.0.19, Electron Linux, a remote Windows HTTP backend, or compaction work beyond
the bounded synthetic sizes below. Only the explicitly supplied 2.0.21 executable was tested; no other
runtime was installed or discovered.
## Fixture and isolation
Added `scripts/test-compaction-isolation-native.mjs` (383 lines). It follows the
existing isolated native-pruning fixture architecture:
- Requires an absolute CLI path. Runs `--version` and a child
`serve --hostname 127.0.0.1 --port 0`, never `service start/restart` or service
discovery. Authenticated server metadata must match the created child's PID
and executable version.
- Gives that child fresh HOME, USERPROFILE, XDG roots, config directory and
explicit database, all beneath the approved temporary directory. Project
config discovery and model-catalog fetching are disabled.
- Uses only synthetic prompts/data and an HTTP loopback provider. Fixture hooks
add request-kind/session headers and one inert tool; they do not override
compaction results. A real provider request with kind `compaction` is held and
then streams the summary through OpenCode's normal completion path.
- Builds the current session-pruning source into a temporary plugin bundle,
explicitly configured against the private database. This tests outline work
in the real native plugin host, not just an in-memory standalone reader.
- Retains private `result.json` and `server.log` evidence. Finally aborts its
event subscriber, releases fixture gates, stops only its child, awaits its
exit and closes the provider connections/server. The script exits naturally.
- No product edits, shared daemon/base access, user-profile mutation, commit,
deployment or restart occurred.
Contract sources consulted before fixture adaptation: V2 client/API/plugin
guides and the installed `@opencode/client@2.0.21` generated declarations
(`session.compact/wait/get/active`, import/export and event timestamps), plus
`@opencode/plugin` session HTTP hook declarations. Configuration was checked
against the V2 config guide, not the legacy JSON schema.
## Procedure and causal assertions
1. Create and warm A/B with real native assistant/tool messages.
2. Outside the measured intervals, import a separate outline corpus of
128 assistant tool messages × 256 KiB textual output (~32 MiB).
3. Submit A's compaction and observe its real provider request and
`session.compaction.started`. Keep the response gated.
4. Sample native info, A/B get and activity concurrently four times.
5. Submit B's prompt and separately gate its primary provider request. Assert
native activity reports **both sessions running**. Release B only; await its
successful outcome and assistant conclusion while A's wait remains pending
and no A compaction-ended event exists.
6. Run the real outline RPC against the separate corpus during A's provider
wait; concurrently sample native `/api/info` as an external heartbeat.
7. Keep A held for another second, verify B get/info again, then release A.
Require a completed compaction message in native context and an ended event,
with no compaction-failed event.
The test has request/condition deadlines, but does not assert a fragile
millisecond performance threshold. Its main assertion is causal: B completes
**before the only mechanism that releases A is invoked**.
## Original small-context measurements
Two complete measured runs with the one-second extended hold:
| Measurement | Run `qAF00E` | Final run `cb4Y79` |
| --- | ---: | ---: |
| A compaction inbox admission | 17.1 ms | 8.1 ms |
| Total interval A held, from compact submission | 1485.7 ms | 1473.4 ms |
| Four info reads during hold | 3.0–7.0 ms | 2.0–4.5 ms |
| Four A get reads during hold | 3.3–5.4 ms | 3.2–5.7 ms |
| Four B get reads during hold | 2.6–4.6 ms | 3.3–5.7 ms |
| Four native activity reads during hold | 2.0–4.3 ms | 1.8–5.6 ms |
| B prompt admission while A held | 6.3 ms | 6.7 ms |
| B completion wait after releasing B, A still held | 21.0 ms | 9.9 ms |
| A completion after releasing A | 11.9 ms | 12.7 ms |
| Outline RPC, 128 × 256 KiB | 132.0 ms | 96.6 ms |
| Concurrent outline native-info requests | 7 | 7 |
| Maximum outline info-request latency | 12.4 ms | 10.5 ms |
In the final run, six info requests completed before the outline finished; a
seventh began near its end and completed afterward. These are native HTTP round
trips, not a main-thread deadline or renderer frame-rate measurement. The
working-source pruning bundle SHA-256 was
`3222a741d8707ac672c4b6e0c199993437372fbccabac769c7bbf3821e1a510a`.
Both runs observed one compaction started, 29 real compaction deltas, and one
compaction ended. A fast observer records `received - event.created` using
same-host wall clocks: held-compaction/both-held session-event maximums were
23 ms; summary-release maximum was 10 ms. No growing delivery backlog was
observed in those phases. This is **delivery-lag evidence**, not an inspection
of an internal queue, a losslessness guarantee, or absence of all backlog.
The seed/import phase had maximum event-delivery lag of 264/193 ms respectively.
It is kept separate: bulk import, local JSON encoding/GC and native work can
contribute, and this fixture does not attribute that delay to compaction.
## Commands and retained evidence
```powershell
$env:TEMP='C:\Users\Admin\AppData\Local\Temp\opencode'
$env:TMP=$env:TEMP
node scripts/test-compaction-isolation-native.mjs `
'C:\Users\Admin\AppData\Roaming\npm\node_modules\@opencode\cli\bin\opencode.exe'
node --check scripts/test-compaction-isolation-native.mjs
```
Final run: PASS, OpenCode 2.0.21, child PID 19312, private endpoint
`http://127.0.0.1:63236` (stopped afterward).
Evidence directories beneath
`C:\Users\Admin\AppData\Local\Temp\opencode\opencode\`:
- `compaction-isolation-qAF00E/result.json` and `server.log`
- `compaction-isolation-cb4Y79/result.json` and `server.log`
An earlier successful development run used a shorter hold. The first setup
attempt failed before creating a server because `--version` had already made
the private config directory; directory creation is now idempotent. Neither is
included in the measured table above.
## Original investigation limits
- A's outgoing summary request was only 24,557 bytes. The separate 32 MiB tool
corpus stresses outline reads, **not** summary-context assembly. This result
excludes neither large-context serialization/parsing nor tool/provider-specific
synchronous work during actual compaction.
- Manual summary compaction under a synthetic compatible provider was exercised,
not provider-native checkpoint APIs or automatic context-overflow recovery.
- No CodeNomad HTTP proxy, renderer, reconnection or remote TCP path is present.
Their independently reproduced defects must not be dismissed by this result.
- The current cooperating outline source was tested; this is not a native
baseline/patch A/B or a general CPU-throughput comparison.
No product correction followed from that small-context non-reproduction. The
requested bounded large-A qualification is recorded next; runtime/platform
qualification and exact CPU attribution remain separate.
## Follow-up: real 2/8/32 MiB A context
### Fixture changes and size validation
`--context-mib 2|8|32` imports native user/assistant-tool pairs into A, using
templates generated by real native tool execution. Each tool has a distinct
canary and 256 KiB of ASCII text; one small trailing turn is retained separately.
The fixture rereads native context and asserts the exact message/tool counts
and total stored output bytes before measuring anything.
In this stress mode only, automatic compaction is disabled, `keep.tokens=0` and
the synthetic model advertises a 16,777,216-token context limit. This avoids
testing a silently shortened request instead of the requested byte budget. It
is deliberately not a typical production model policy. The V2 compaction guide
documents the retained tail and model-budget shortening behavior.
An initial development check exposed exactly that distinction: without a small
trailing turn, the first 2 MiB request contained only 7 of 8 payload markers.
The fixture failed rather than reporting the stored size as the compacted size.
Putting the current turn after the large summarized prefix corrected the corpus;
all subsequent runs assert all markers **and every payload-fill byte** at the
actual provider boundary. There is no direct SQLite seeding or mutation.
| A tool payload | Native messages (pairs + tail) | Payload markers received | Provider JSON bytes | Provider messages |
| --- | ---: | ---: | ---: | ---: |
| 2 MiB (2,097,152 bytes) | 18 | 8/8 | 2,123,428 | 26 |
| 8 MiB (8,388,608 bytes) | 66 | 32/32 | 8,421,286 | 98 |
| 32 MiB (33,554,432 bytes) | 258 | 128/128 | 33,612,826 | 386 |
Provider message counts differ from stored native counts because lowering adds
separate assistant tool-call/tool-result messages and the summary instructions.
These are compaction request sizes, not the earlier separate outline corpus.
`--skip-outline` omits that separate 32 MiB import/RPC so the assembly matrix is
not confused with outline load. Allowed A sizes are capped at 32 MiB, sequential
runs create only one private native child/provider each, and no size escalation
or runtime installation was performed.
### Phase measurements and causal checks
Immediately upon compact submission, info and B-get probes start, together with
a B prompt/wait. Their start/finish times are retained, with explicit booleans
for completion before provider headers/body. The provider records its handler
start (headers observable), complete body receipt and JSON parse separately.
The output distinguishes submission-to-arrival from **admission response** to
arrival; neither is mislabeled completed compaction duration.
All three witness calls must be issued before A's provider handler begins.
Their issuance is a client-call timestamp, not an upstream route-entry trace.
B succeeds before releasing A in every run. An additional B prompt is then
tested during the provider hold, both sessions must be running simultaneously,
and A's completed context/ended event is required after release. No timing
threshold was introduced in product or fixture assertions.
Two sequential runs at each size (no simultaneous fixture launch):
| Size / run | Compact admission | Submission → headers / full body | Max info / B-get wholly before headers | Early B prompt admission | Early B wait | B done before headers? |
| --- | ---: | ---: | ---: | ---: | ---: | --- |
| 2 / `IO0hKV` | 6.4 ms | 31.2 / 33.9 ms | 2.5 / 24.6 ms | 26.4 ms | 27.2 ms | no |
| 8 / `mrXUsj` | 8.7 ms | 119.7 / 130.4 ms | 10.2 / 55.9 ms | 59.0 ms | 38.7 ms | yes |
| 32 / `I1Py1V` | 31.8 ms | 224.8 / 309.5 ms | 47.4 / 57.3 ms | 9.4 ms | 32.1 ms | yes |
| 2 / `gomUTk` | 8.5 ms | 56.2 / 66.1 ms | 3.5 / 36.9 ms | 48.7 ms | 57.1 ms | no |
| 8 / `ShUeBE` | 24.3 ms | 163.5 / 173.4 ms | 83.7 / 83.2 ms | 28.5 ms | 100.5 ms | yes |
| 32 / `jKWuew` | 45.8 ms | 290.9 / 332.7 ms | 83.2 / 82.3 ms | 11.3 ms | 52.7 ms | yes |
Requests *started* before headers can straddle arrival; their maxima were
84.7/84.3 ms for info/B-get in the final 32 MiB run. The table's wholly-before
filter excludes that fixture's provider body parsing from those latency values.
For the final 2/8/32 runs, admission-response → provider headers was
47.6 / 139.0 / 244.9 ms. Local provider JSON parsing was separately
2.7 / 7.3 / 29.9 ms; it is not native assembly CPU.
During the held-response phase of the matrix, native reads were generally a few
milliseconds (maximum 18.1 ms); B prompt admission was 6.1–10.0 ms. B completed
without releasing A at every size, then A completed 12.4–19.4 ms after release.
All runs observed started, 29 compaction deltas and ended, with no failed event.
Observed session-event lag in the assembly sampling phase reached 128 ms, versus
28 ms maximum during the B/provider-held phase. These are delivery observations,
not internal queue-depth or losslessness measurements.
### Attribution and limits
Confirmed: the pre-provider preparation/dispatch window grows with this stress
corpus, and unrelated native operations sometimes experience transient latency
inside that window. It cannot be attributed to the deliberately held summary
response: the provider hold has not begun for the wholly-before samples. B also
completes before A reaches the provider in the 8/32 MiB runs, ruling out a
continuous global stall throughout that preparation window in these executions.
Not confirmed: one particular synchronous SQLite step, tokenizer, JSON encoder,
lock or upstream function as the cause. The distribution differs by operation
and run, with fast info responses alongside slower B-get/admission in some
samples. Host scheduling, native GC/storage work and fixture scheduling remain
possible contributors. There is no native CPU profile or route-entry trace;
HTTP latency alone does not prove uninterrupted event-loop blocking. No upstream
or CodeNomad production patch is justified by this bounded evidence alone.
A compatibility smoke run with **2 MiB A plus the separate outline enabled**
also passed (`6OnLuq`). It verified all A bytes plus 13 completed native
heartbeats during a 218.2 ms outline RPC. That extra run is outside the matrix:
its pre-header B-get/prompt were 89.1/93.5 ms and seed-phase event lag reached
2.56 s. This additional variability reinforces the host/seed/GC qualification
limits; it must not be hidden or mislabeled a persistent compaction freeze.
The corpus is synthetic repeated ASCII, with imported historical templates,
fixture token usage and an artificially permissive model budget. It tests real
assembly of bounded bytes, not the reporter's exact token/JSON/provider workload,
automatic recovery, provider-native checkpoints or remote Linux/Windows HTTP.
### Reproduction / next step
```powershell
node scripts/test-compaction-isolation-native.mjs `
'C:\Users\Admin\AppData\Roaming\npm\node_modules\@opencode\cli\bin\opencode.exe' `
--context-mib 32 --skip-outline
```
Use `2` and `8` for the other bounded sizes; retain the TEMP/TMP setup above.
All seven successful runs retain `result.json` and `server.log` under the same
private `compaction-isolation-<run>` namespace. Every child was stopped and its
exit awaited; the script naturally exited. `node --check` passes. Only this
script and this report were edited for the follow-up.
Smallest useful next step: profile the **native pre-provider preparation window**
and correlate native route-entry/exit and event-loop work under a controlled
host, at the same fixed byte budgets. Preserve the verified transmitted bytes
and the wholly-before-header filter. Do not patch an unproven upstream hotspot
or reinterpret provider waiting as freeze; runtime 2.0.19 and the real remote
platform remain separate, untested qualifications.

View file

@ -1,85 +0,0 @@
# Issue #824 — independent outline-reader review
Baseline reviewed: `366830f6`. Scope: the current `outline-index.ts` change,
its responsiveness regression test and profiler, and the related performance
claims. No product code was changed during this review.
## Verdict
Approved with no blocking finding. The patch addresses the demonstrated
event-loop starvation while preserving the outline protocol and its existing
correctness fences. The documentation correctly limits the result to improved
cooperation: it does not claim lower total CPU, a hard 8 ms deadline, or that
this defect alone explains issue #824.
## Correctness review
- `.all()` was replaced by iteration over the same header statement; the SQL,
parameters, ordering and 512-row limit are unchanged. Projection likewise
retains its existing SQL and response mapping.
- A direct baseline/current comparison produced byte-equivalent JSON results,
including checkpoint digests, for an initial mixed outline, a persisted-known
delta after delete/update/append, and an ownership/revert-bounded read.
- Existing navigation coverage still verifies the 16,384-entry page bound,
cursor snapshot (`through`) behavior, deletion holes, tail checkpoint growth,
pruning detection, tool metadata, ownership and staged-revert visibility.
- The read transaction still begins before ownership and bounds are read and is
held across all cooperative yields, so a page observes one snapshot. An abort
from either new loop rejects through `yieldTurn`; iterator cleanup is followed
by the existing unconditional `ROLLBACK`. Both new tests observe
`db.isTransaction === false` afterward.
- Each RPC obtains its own read-only `DatabaseSync` connection in
`withHistoryDatabase`. Yielding with this transaction open therefore does not
expose a shared connection to another RPC. It can retain a read snapshot for
longer in wall-clock time, but work remains bounded and promptly cancellable.
- `EXPLAIN QUERY PLAN` for the header query, both with and without the revert
`id < ?` predicate, uses
`session_message_session_seq_idx (session_id=? AND seq>? AND seq<?)`. The
patch does not change query plans or introduce a sort scan.
## Responsiveness evidence
The regression design is discriminating rather than timing-sensitive. Its real
SQLite view charges deterministic synthetic time whenever payload data is
accessed, while `setImmediate` supplies an actual pending cancellation.
- Current code: both header and projection cancellation cases passed.
- The same test copied outside the worktree with only `outline-index.ts`
replaced by the exact `366830f6` source failed both cases: the header case
consumed 896 expensive payload accesses before cancellation ran; the
projection case consumed 384 after its 512 free header accesses.
- A smaller independent profile (`128` rows × `256 KiB`) observed current
maximum turn gaps of `8/8/8 ms`, versus baseline `40/43/38 ms`. Total scan
times were comparable (`43–45 ms` current, `39–43 ms` baseline), supporting
the documented cooperation claim without implying throughput improvement.
- One SQLite iterator step remains synchronous. A single exceptionally costly
row can exceed the 8 ms budget before JavaScript regains control; this
limitation is explicitly documented in both code and investigation notes.
## Validation performed
```text
node --import tsx --test \
packages/server/src/opencode/session-pruning/outline-responsiveness.test.ts \
packages/server/src/opencode/session-pruning/navigation.test.ts
=> 11 passed, 0 failed
npm run typecheck --workspace @neuralnomads/codenomad
=> passed
git diff --check 366830f6 -- \
packages/server/src/opencode/session-pruning/outline-index.ts
=> passed (only Git's existing LF/CRLF working-copy warning)
```
The coordinator's full isolated OpenCode 2.0.21 fixture result was not rerun in
this read-only review; the focused native-SQL and contract checks above are
independent of that reported qualification.
## Residual risk / next step
No adjustment is required for this patch. Retain the explicit limitation that
cooperative iteration cannot preempt one SQLite step. If real-session evidence
later shows individual JSON projection steps exceeding the acceptable latency,
the smallest follow-up is architectural (worker-thread/off-loop projection or a
less parse-intensive stored index), not a tighter JavaScript timer threshold.

View file

@ -1,160 +0,0 @@
# #824 — récupération du correctif de rendu des tâches imbriquées
2026-10-01, worktree `issue-824-performance`, HEAD `366830f6`.
## Résultat et portée
Le correctif minimal de `task.tsx` est maintenant présent dans le worktree mission,
sans commit/push ni déploiement. Deux régressions rouges sont reproduites sur la
source baseline ; la correction passe les **63 scénarios** des quatre suites
navigateur ciblées, naturellement, sans force-exit, skip ou force-click ajouté.
La mesure A/B finale, 30 échantillons par variante, observe une médiane de temps
main-thread **353,39 -> 22,72 ms** par delta dans le fixture, pas dans l'application
desktop. Le contrôle instrumenté confirme **80 retraits + 80 insertions -> zéro**,
avec **80 snapshots conservés**. Ce patch n'élimine pas les relectures natives.
Seuls ces six fichiers appartiennent à cette tâche :
| Fichier | Delta / taille |
| --- | --- |
| `packages/ui/src/components/tool-call/renderers/task.tsx` | +29/-18, 647 lignes |
| `packages/ui/tests/browser/render-cost.test.ts` | nouveau, 245 lignes |
| `packages/ui/tests/browser/fixtures/render-cost.tsx` | nouveau, 150 lignes |
| `packages/ui/tests/browser/fixtures/render-cost-timeline.tsx` | nouveau, 44 lignes |
| `scripts/test-render-cost-audit.mjs` | nouveau, 57 lignes |
| `dev-docs/issue-824-render-recovery.md` | ce rapport |
Aucun changement de cette tâche dans les stores, `server-events`, `opencode-service`,
styles, dépendances ou serveur. Les modifications concurrentes de ces surfaces dans
le worktree commun ne sont pas notre livraison. Aucun daemon partagé, profil,
desktop, réseau utilisateur ou base utilisateur utilisé ; aucune dépendance
réinstallée/symlinkée. Les HTTP fixtures sont loopback et bloquent les autres origines.
## Provenance et logique
Source : `audit-resume-integration`, correction du précédent audit de rendu.
Le blob baseline de `task.tsx` est identique à `47f9e43e` et `366830f6` :
`dee90df8560b83958dbf6d6f7c40e013bbdb289f`.
Le postimage produit canonique Git est identique au correctif audit :
`9a427900afcad8f274db5d9a77600836f24a728e`.
Les SHA bruts diffèrent pour ce seul fichier à cause des fins de ligne du checkout ;
comparaison LF-normalisée et `git diff --no-index` : aucun delta de contenu.
Les deux fixtures et le runner A/B sont repris byte-for-byte de l'audit. Le test
était aussi identique avant une adaptation de harness : chaque serveur utilise
désormais un cache Vite temporaire unique, supprimé après fermeture. Cela sépare
ses modules optimisés des autres fixtures concurrents, sans changer les assertions.
La correction conserve les clés affichées pendant une relecture autoritaire,
réindexe dès son retour, et maintient une shell par identité de tool avec getters
réactifs. Un changement de clé remonte bien une shell neuve. Une page autoritaire
vide supprime les étapes ; une erreur de lecture conserve le dernier affichage et
le retry explicite. Markdown utilise son hash existant plutôt qu'une révision
normalisée à zéro ; la révision de part reste suivie pour les updates in-place.
## Reproductions fonctionnelles exécutées ici
Node `v25.2.1`, Windows, Chromium Playwright `153.0.8010.12`, Vite development.
Contrôle rouge par override Vite de la source exacte baseline, sans réécrire le
renderer du checkout : deux assertions échouent avec messages attendus, pas timeout :
1. `authoritative reprojection must not recreate the tool shell` ;
2. `the native record changed but the displayed Markdown is stale`.
Le sample instrumenté baseline passe son contrôle de 80 étapes et compte 80 ajouts,
80 retraits et 80 clones à chacun des dix deltas. La commande rouge a donc un exit 1
attendu (2 FAIL + 1 PASS) ; le wrapper de collecte garde cet exit dans le log.
La suite verte finale passe **63/63**, zéro skip/failure, exit 0 naturel (~202 s) :
render-cost (8), session-rendering (46), task-copy (6), tool-images (3). Elle exerce
les vrais composants Solid, stores et dispatcher SSE, avec seulement le transport
natif et les réponses API synthétiques. Le maintien du scroller pendant MMB, du
scrollTop et disclosure, les copies exactes, l'update in-place, l'erreur/retry,
la suppression et le changement de clé sont réellement vérifiés.
`npm run typecheck --workspace @codenomad/ui` passe, y compris au dernier replay.
`git diff --check` des fichiers de livraison passe.
Le test est admis par le glob existant `tests/browser/*.test.ts` du script package ;
aucune modification de gate/manifest n'est nécessaire.
## A/B CPU actuel — séparé des résultats historiques
Protocole inchangé : 80 étapes read terminées × 8 000 caractères, collapsed ;
3 warmup deltas puis 10 deltas isolés dans chacun des trois couples baseline/fixed.
Processus/pages frais, alternance baseline puis fixed ; compteur clone et observer
DOM **désactivés** pour les timings. CDP cumule les durées jusqu'après hydration et
deux animation frames. Les coûts sync seuls n'incluent pas le travail différé.
| ms/delta, médiane [min–max], écart-type échantillon | Baseline | Correction |
| --- | --- | --- |
| main-thread tasks | 353,39 [189,76–737,96], 141,77 | 22,72 [11,00–68,03], 12,39 |
| script | 18,65 [14,75–57,85], 11,10 | 0,92 [0,29–40,30], 10,04 |
| layout | 3,58 [2,93–5,39], 0,63 | 0,14 [0,09–0,36], 0,05 |
| recalcul style | 11,69 [10,39–14,35], 0,94 | 0,20 [0,11–0,32], 0,06 |
| dispatcher sync | 17,25 [11,20–33,60], 4,88 | 0,25 [0,10–0,50], 0,11 |
L'audit antérieur rapportait 369,98 -> 20,64 ms sur 30 échantillons par variante.
Ce ne sont pas les échantillons actuels et ils ne sont pas fusionnés avec eux.
Le premier A/B actuel était 353,90 -> 17,03 ms ; le replay suivant 378,59 -> 15,61 ms.
Ces variations ne sont pas une garantie de latence/FPS et aucun seuil CI absolu
n'est ajouté.
## Tentatives et limites conservées
Deux reprises A/B supplémentaires ont échoué avant le montage du fixture fixed,
sur `page.waitForFunction(window.fixture)` après 30 s. Elles restent dans
`codenomad-render-cost-lehbGS/3-fixed.log` et `codenomad-render-cost-FN0wKE/1-fixed.log`
sous le temp approuvé. Pas de gain CPU calculé depuis ces lots incomplets.
Leurs erreurs sont de démarrage du harness, pas une assertion renderer. La
collision du cache Vite partagé est une **hypothèse**, pas une cause démontrée :
après isolation du cache, les six invocations A/B et 63 tests passent, mais il n'y
a pas eu de contrôle négatif causal du cache en concurrence.
Des snapshots SHA avant/après des sources ont aussi détecté des mutations de tâches
concurrentes (stores compaction, event handlers, serveur et ressources générées).
Les cinq fichiers exécutables de notre livraison sont restés inchangés pendant
le dernier A/B + suite verte. Ces runs **ne sont pas une qualification d'une
composition intégrée entièrement immuable** : refaire le replay indépendant sur
une composition figée après intégration des autres correctifs. Les wrappers
marquent cette dérive par un exit 1, distinct des enfants A/B et tests, tous exit 0.
Pas de qualification production-build, Electron/WebView2, Linux/macOS, heap,
GPU/FPS, backend réel, compactage natif ou freeze applicatif universel. Le coût des
clones/relectures demeure. `task.tsx` reste **647 lignes**, au-dessus du seuil de
signalement ~500 ; aucun refactor de taille seul n'a été introduit.
## Artefacts et commandes
Tous les chemins ci-dessous sont sous
`C:/Users/Admin/AppData/Local/Temp/opencode/` :
- `issue-824-render-isolated-xn3MnK/red-final.log`
SHA256 `947bb2f6beae52db032288e8f855af27fcaa7deb90a3787a85fdf49f82a06c18`.
- `issue-824-render-isolated-xn3MnK/green.log`
SHA256 `15b7d31be5b99cbc8e3bf62f322fa5b14a1f710e740a5cd0b2a8e0d4c718a7bd`.
- `issue-824-render-isolated-xn3MnK/{before,after,result}.json` : dérive de composition.
- `codenomad-render-cost-dD0phn/metrics.json` et six logs adjacents : A/B final.
SHA256 metrics `f1a2a4a7ce5ca6f042328bc5d8e84fc315be49c90a5f26a67759859d3e801ba8`.
SHA256 produit brut livré : `task.tsx` =
`d130c5fb2219772c5de0c63393744783cdb659734024f88cbe73cafeec818100`.
SHA256 test final = `4abe4345a3a5f1a00d0e94544fd02b70851d07efa6b294a55466bc51ef7d2fde`.
```powershell
$env:TEMP='C:\Users\Admin\AppData\Local\Temp\opencode'
$env:TMP=$env:TEMP
# Dans packages/ui, override baseline pour les assertions rouges uniquement :
$env:CODENOMAD_RENDER_TASK_BASELINE='<temp>/task-baseline.tsx'
node --import tsx --test --test-concurrency=1 --test-name-pattern='^(unrelated child text|native page output|audit sample:)' tests/browser/render-cost.test.ts
Remove-Item Env:CODENOMAD_RENDER_TASK_BASELINE
node --import tsx --test --test-concurrency=1 tests/browser/render-cost.test.ts tests/browser/session-rendering.test.ts tests/browser/task-copy.test.ts tests/browser/tool-images.test.ts
# À la racine :
node scripts/test-render-cost-audit.mjs 3
npm run typecheck --workspace @codenomad/ui
```
Prochaine étape minimale : revue indépendante des six fichiers de cette livraison,
puis replay des deux assertions rouges, 63 contrôles verts et A/B sur une composition
figée. Aucun restart/déploiement ni élargissement du patch n'est nécessaire.

View file

@ -1,137 +0,0 @@
# Issue #824 — revue indépendante auth et admission HTTP
## Verdict
**Gates auth et admission HTTP : acceptés, sans finding bloquant.**
La revue porte sur le diff intégré non commité de
`D:\CodeNomad\.codenomad\worktrees\issue-824-performance` depuis `366830f6`,
et non sur les worktrees sources. Les trois fichiers produit transport intégrés
ont exactement les mêmes blobs Git que `d9598e42`. Les changements UI, relais
d'événements, outline et rendu présents simultanément dans le worktree sont hors
périmètre.
Ces corrections ont une causalité plus étroite que l'intitulé général #824 :
- l'auth retire les dérivations scrypt HTTP du thread principal Node ;
- l'admission HTTP retire un observateur local abandonné pendant ses préflights
et empêche ses continuations tardives ;
- elles ne démontrent ni la cause du blocage natif pendant une vraie compaction,
ni une saturation HTTP/1 Electron Linux → backend Windows, ni l'élimination du
freeze global rapporté.
Les deux rapports d'intégration conservent correctement cette limite et ne
promettent pas une correction universelle.
## Findings prioritaires
### Aucun finding P0/P1/P2
Je n'ai trouvé ni autorisation accordée sur un ancien mot de passe, ni replay de
mutation, ni invalidation de connexion partagée sur abandon local, ni forwarding
après annulation de préflight dans le périmètre revu.
### Risques résiduels non bloquants
1. **Saturation du pool libuv non qualifiée.**
`packages/server/src/auth/password-hash.ts:58-64` rend le thread HTTP
coopératif via `crypto.scrypt`, mais ne supprime pas le coût CPU/mémoire et ne
borne pas une rafale de logins. Il n'existait pas de rate limiter sur ces
routes dans la baseline et le patch n'en retire aucun. Une politique
d'admission serait un changement séparé, non requis pour accepter ce patch.
2. **Persistance auth toujours synchrone.**
`packages/server/src/auth/auth-store.ts:176-184` conserve les petites
écritures synchrones existantes. Les mesures et le correctif concernent la
dérivation scrypt, pas une garantie d'absence totale de pause filesystem.
3. **Qualification réseau/OS encore absente.**
Les tests transport utilisent de vrais sockets loopback et le client SDK,
mais pas Electron Linux vers Windows, suspension/relogin, proxy intermédiaire
ou limite de connexions Chromium. Le correctif prouve la fuite d'observateur
et ses fences, pas le scénario utilisateur complet.
## Revue auth
- `packages/server/src/auth/password-hash.ts:23-55` conserve le record v1, le
sel 16 octets, la clé 64 octets, les paramètres enregistrés et
`crypto.timingSafeEqual`. Le seul chemin synchrone restant est explicitement
`hashPasswordSync`, appelé pendant l'initialisation CLI/env avant service.
- `packages/server/src/server/routes/auth.ts:101,150` attend les résultats login
et password ; la recherche dépôt ne trouve pas d'autre caller produit oublié
de `validateLogin`, `validateCredentials`, `setPassword` ou `hashPassword`.
- `packages/server/src/auth/auth-store.ts:117-160` sérialise les changements par
ordre d'admission. La queue absorbe un rejet uniquement pour permettre le
changement suivant, tandis que le caller original reçoit bien l'erreur. Une
persistence bootstrap échouée ne supprime pas `bootstrapUsername` et ne publie
pas de nouveau cache.
- `packages/server/src/auth/auth-store.ts:101-115` relit l'identité du record
cached après l'await scrypt. Un login dérivé sur l'ancien record est refusé si
une mutation a persisté entre-temps, sans recalcul ni replay.
- Les réponses 401/409, le cookie, le statut bootstrap, l'override CLI/env et
l'auth désactivée sont couverts. Aucun mécanisme de rate-limit existant n'est
modifié.
## Revue admission HTTP
- `packages/server/src/server/request-admission.ts:8-14` observe dès l'entrée
l'upload `aborted` et le `close` d'une réponse incomplète. Il n'interprète pas
le `close` normal de l'IncomingMessage comme une déconnexion.
- `packages/server/src/server/request-admission.ts:16-36` fait courir uniquement
l'attente locale contre l'abandon. La Promise partagée reste observée ; sa
génération n'est ni annulée ni invalidée et un rejet tardif ne devient pas une
unhandled rejection.
- `packages/server/src/server/http-server.ts:699-975` transmet le signal aux
lectures natives qui le supportent (`profile`, session/project, PTY/Shell,
ownership/location) et entoure les acquisitions/inventaires non annulables par
`wait()`. Chaque frontière résolue revérifie l'abandon avant de poursuivre.
- Les guards précédant la fence de mutation, l'environnement et le forwarding
sont présents à `http-server.ts:975`, `989-1015` et `1022-1024`. Le `finally`
de l'entrée dispose les listeners sur toutes les sorties.
- `packages/server/src/opencode/compatibility/proxy.ts:27-40` installe le listener
de forwarding avant le fetch puis refuse immédiatement un downstream déjà
fermé. Après admission native, la sémantique antérieure demeure : abandon de
la réponse HTTP sans replay ni invalidation de la connexion partagée.
- `packages/server/src/workspaces/manager.ts:270-286` propage le signal à la
validation native de location. Une annulation éventuellement convertie en
`false` par son `catch` est réémise par le `wait()` appelant, dont le signal est
alors aborté ; elle ne devient donc pas une autorisation ou un 403 tardif.
## Validation fraîche
Exécutée depuis le worktree intégré, Windows, Node 25.2.1, sans daemon/profil/base
utilisateur, restart ou déploiement :
```powershell
node --unhandled-rejections=strict --import tsx --test packages/server/src/auth/password-hash.test.ts packages/server/src/server/routes/auth-responsiveness.test.ts packages/server/src/server/routes/auth.test.ts
node --unhandled-rejections=strict --import tsx --test packages/server/src/server/__tests__/instance-proxy-disconnect-diagnostic.test.ts packages/server/src/server/request-admission.test.ts packages/server/src/server/__tests__/instance-proxy.test.ts packages/server/src/server/__tests__/instance-proxy-legacy.test.ts packages/server/src/opencode/compatibility/proxy.test.ts
npm run typecheck --workspace @neuralnomads/codenomad
$env:TEMP='C:\Users\Admin\AppData\Local\Temp\opencode'
$env:TMP=$env:TEMP
node --unhandled-rejections=strict --import tsx --test "packages/server/src/**/*.test.ts"
```
Résultats :
- auth ciblé : **9/9 pass**, sortie naturelle ;
- transport ciblé : **65/65 pass**, sortie naturelle ;
- typecheck serveur : **pass** ;
- gate serveur complet intégré : **799 tests, 793 pass, 0 fail, 6 skips
déclarés**, sortie naturelle en environ 35,7 s ;
- `git diff --check` ciblé : aucune erreur whitespace (les commandes
`--no-index` retournent normalement 1 parce que les nouveaux fichiers
diffèrent de `NUL`; seulement les avertissements LF/CRLF habituels).
Le gate complet contient aussi les autres changements concurrents déjà intégrés ;
il confirme leur composition actuelle avec ces patches, sans attribuer leurs
tests aux deux corrections revues.
## Taille et suite minimale
Les fichiers touchés déjà surdimensionnés restent
`packages/server/src/server/http-server.ts` (~2346 lignes) et
`packages/server/src/workspaces/manager.ts` (~1240 lignes). Aucun refactor de
taille n'est justifié dans cette correction bornée.
Suite minimale recommandée : conserver les patches tels quels et poursuivre
séparément la qualification avec runtime privé d'une vraie compaction et le
scénario Electron Linux → Windows. Ne pas présenter ces deux corrections comme
preuve que la cause native ou le freeze global #824 est résolu.

View file

@ -1,119 +0,0 @@
# #824 — correction M1 de membership des étapes imbriquées
## Résultat
Le finding M1 de `issue-824-ui-review.md` est corrigé dans le worktree mission.
Les cinq nouvelles régressions échouent sur le renderer livré avant cette tâche,
puis **68/68 tests navigateur** passent sur le renderer corrigé, avec rejets stricts,
sortie naturelle, aucun skip/force-exit/force-click. Les shells conservés gardent
leur disclosure, leur scroller et leur scrollTop après suppression ou déplacement.
Le contrôle instrumenté de 80 étapes conserve **zéro retrait/insertion par delta**.
Cette tâche modifie uniquement :
- `packages/ui/src/components/tool-call/renderers/task.tsx` ;
- `packages/ui/tests/browser/render-cost.test.ts` ;
- `packages/ui/tests/browser/fixtures/render-cost.tsx` ;
- ce rapport `dev-docs/issue-824-task-membership-fix.md`.
Aucun store/SDK/serveur/style, daemon, base utilisateur, profil, déploiement ou
restart modifié. Aucun commit/push, sous-agent ni installation de dépendances.
## Cause et correction
L'index incrémental inférait les changements de membership depuis les nombres de
messages/parts et l'identité du message de queue. Une relecture autoritaire peut
conserver toutes ces propriétés et supprimer, remplacer, réordonner ou changer le
type d'une part. Les shells supprimés se démontaient, mais `childToolKeys` et le
compteur/troncature gardaient des clés fantômes. Un ajout retrouvant l'ancienne
cardinalité restait invisible.
Le renderer réutilise maintenant son **scanner structurel borné existant** au point
de révision de session. Il ne compare ni copie les payloads/output : seuls les IDs
et le type de part déterminent les clés. On supprime les heuristiques de counts/tail
plutôt que d'ajouter un autre cache ou une politique de refresh. La liste précédente
est rendue telle quelle quand ses clés ordonnées sont identiques.
Le scan reste limité à **10 000 unités message/part** et s'arrête une fois trouvées
201 étapes ; seules les **200 dernières** s'affichent. Les constantes et le budget
du scanner restent inchangés. Le coût des IDs/type est payé à chaque révision,
même si le texte seul change : c'est le compromis explicite pour reconnaître tout
remplacement autoritaire, sans observer/comparer les gros payloads. Aucun gain CPU
nouveau ni coût maximal de 10 000 IDs en production n'est revendiqué ici.
`For` remplace `Index` sur les clés tool : une part retenue suit son identité quand
une suppression ou un réordonnancement change sa position. Une identité nouvelle
reçoit une shell/disclosure neuve. Les getters réactifs et le hash Markdown de la
correction précédente restent inchangés. Une lecture pendante conserve la liste
résidente ; le retour autoritaire, une page vide et le changement de child-session
continuent à la réconcilier. Le recalcul remet aussi la troncature à jour après shrink.
Delta produit relatif au renderer **déjà intégré avant cette tâche**, préimage
conservée dans le temp : **+12/-90 lignes**. Ce n'est pas un delta contre HEAD, qui
ne contient pas encore les correctifs mission. `task.tsx` reste **569 lignes**, au-
dessus du seuil d'avertissement ~500 ; aucun refactor de taille indépendant.
## Preuves exécutées
Node `v25.2.1`, Windows, Chromium Playwright `153.0.8010.12` ; vrais composants
Solid, stores et `loadMessages(..., { force: true })`, native/API réponses privées,
réseau hors loopback bloqué. Le message natif `child-message` reste le même ; le
fixture remplace sa liste de parts, pas le composant ni l'index.
Les cinq nouvelles assertions rouges causales, sans seuil timing :
1. `2 -> 1 -> 2` en gardant step-0 : counter reste `2 steps` au lieu de `1 steps` ;
2. même scénario en gardant step-1 (suppression en tête) : même counter faux ;
3. remplacement/réordre `[step-0, step-1] -> [step-2, step-0]` à cardinalité égale :
nouvelle step-2 absente ;
4. changement tool -> text avec ID inchangé : counter reste à 2 au lieu de 1 ;
5. shrink de 201 à 199 parts : counter reste `200+ steps` au lieu de `199 steps`.
Sur correction, tous passent. Les deux premiers vérifient également l'arrivée
exactement une fois de la nouvelle step-2, puis l'identité shell/scroller, disclosure
ouverte et scrollTop 240 de l'étape retenue. Le troisième vérifie ordre DOM exact et
shell retained. Le cinquième vérifie 200 shells initiales, 199 ensuite et disparition
du diagnostic de troncature.
Gate final : 13 render-cost + 46 session-rendering + 6 task-copy + 3 tool-images =
**68 scénarios distincts**, ~176 s, exit 0 naturel, aucun failure/skip. Le contrôle
perf réel compte, pour chacun de dix deltas, **80 clones, 0 added, 0 removed** ; une
assertion explicite interdit maintenant les remounts dans ce sample instrumenté.
Ces compteurs sont distincts des A/B CPU antérieurs, qui restent dans leurs rapports
et ne sont pas recyclés comme mesures de ce patch.
`npm run typecheck --workspace @codenomad/ui` passe sur dernier patch.
`git diff --check` ciblé passe (avertissement LF/CRLF seulement).
Les SHA des trois exécutables de livraison sont identiques avant/après la séquence
rouge/verte. Cela qualifie la stabilité de ce lot, pas une composition entière
immutable de tous les travaux de mission.
## Artefacts et replay
Sous `C:/Users/Admin/AppData/Local/Temp/opencode/` :
`issue-824-membership-c5bca1d90b4a4564bd4c7f695b6381bd/` contient :
- `task-before.tsx` : préimage exacte avant correction M1 ;
- `red.log` : 5 échecs d'assertion attendus, exit 1, naturel ;
SHA256 `db9316eb888984364cbdd59d2fac8fac74600ed9e5e779f9c86b33869b1fc51a` ;
- `green.log` : 68 passes, exit 0, naturel ;
SHA256 `e4f5a996b9b576703888b353e14c02940b4dd72d0fe3e056cc54b96fcffe1849` ;
- `result.json` : statuts enfants + empreintes avant/après, `changed: []`.
SHA256 produit livré :
`f07ce171a6b1d3394ce94be496b0abd67f8bc0bb48e946eb9173a681c844f401`.
SHA256 test : `93d9e95988b53eb1e3934ecd13f4e8d32e0eb797682a866db113797174be5075`.
SHA256 fixture : `0df543ea983c23e33a4957ed6732dc81a2e04cf5ede7d4f1281d6ee058360ebb`.
Depuis `packages/ui`, avec TEMP/TMP pointant vers le temp approuvé :
```powershell
$env:CODENOMAD_RENDER_TASK_BASELINE='<artefact>/task-before.tsx'
node --unhandled-rejections=strict --import tsx --test --test-concurrency=1 --test-name-pattern='^authoritative task membership' tests/browser/render-cost.test.ts
Remove-Item Env:CODENOMAD_RENDER_TASK_BASELINE
node --unhandled-rejections=strict --import tsx --test --test-concurrency=1 tests/browser/render-cost.test.ts tests/browser/session-rendering.test.ts tests/browser/task-copy.test.ts tests/browser/tool-images.test.ts
```
Prochaine étape : re-review indépendante du finding M1, puis validation combinée
sur la composition figée. Pas de qualification desktop/Linux/macOS, daemon réel ou
disparition universelle des freezes revendiquée par ce lot.

View file

@ -1,218 +0,0 @@
# Issue 824 — revue indépendante de l'intégration UI
## Verdict
**Corrections demandées avant validation finale**, pour un défaut de membership
du renderer `task` confirmé ci-dessous. Le coalescing du compactage et la
séparation des diagnostics SSE sont acceptables dans leur périmètre; aucun défaut
de perte de texte, d'ordre ou de fence n'a été reproduit sur ces surfaces.
Revue bornée au diff depuis `366830f6` de `opencode-data.ts`, `instances.ts`,
`compaction-delta-buffer.ts`, `task.tsx`, `server-events.ts`,
`event-source-handlers.ts` et de leurs fixtures. Aucun fichier produit n'a été
modifié par cette revue.
## Findings
### M1 — une suppression autoritaire d'une part tool laisse une clé fantôme dans le renderer `task`
Dans `task.tsx`, la mise à jour incrémentale ne traite que
`nextPartCount > previousPartCount`. Si une page autoritaire conserve le message
mais retire une part tool, `nextPartCount < previousPartCount` ne déclenche ni
rescan ni retrait de `childToolKeys`.
La ligne supprimée disparaît malgré tout parce que `TaskToolCallRow` ne trouve
plus sa part et son `Show` se démonte. En revanche, la clé reste dans
`childToolKeys` : le compteur et la troncature sont faux, et un ajout ultérieur
qui ramène le nombre de parts à sa valeur précédente peut ne pas être indexé.
Reproduction indépendante avec le vrai composant, le vrai store et
`loadMessages(..., { force: true })`, via une transformation Vite en mémoire qui
retire seulement `step-0` de la réponse native :
```json
{"count":"2 steps","visible":1,"partIds":["step-1"]}
```
La reproduction directe déjà exposée par le fixture donne le même état. Le test
vert actuel `removeTool(0)` affirme uniquement que le DOM de `step-0` est détaché;
il n'affirme ni `1 step`, ni la membership, ni l'ajout suivant. Le test de page
vide couvre une baisse du nombre de messages et prend une autre branche de full
rescan.
Correction minimale recommandée : full rescan borné dès qu'un nombre de parts
d'un message indexé diminue (et, si les pages peuvent remplacer des identités à
cardinalité égale, comparer aussi l'identité bornée des parts). Ajouter un cas
page autoritaire 2→1 dans le même message, puis ajout d'une nouvelle part 1→2;
le shell de la part non supprimée doit rester monté, le compteur doit faire
2→1→2 et la nouvelle clé doit apparaître exactement une fois.
### R1 — l'accumulateur de compactage est borné dans le temps, pas en taille
Le timer part du premier fragment et n'est jamais repoussé : avec de vrais
événements EventSource échelonnés, la résidence est donc bornée à environ 250 ms.
En revanche, tant que la boucle ne rend pas la main, le nombre de fragments et la
taille de la chaîne concaténée n'ont pas de plafond explicite. Le gros backlog
synchrone du diagnostic peut toujours construire un unique payload volumineux.
Ce n'est pas bloquant pour le correctif revendiqué : le transport produit livre
ici un événement par callback, les nouveaux tests de cadence rendent réellement
la main, et le résumé final doit de toute façon exister en mémoire. Ne pas appeler
le buffer « borné en mémoire ». Si une mesure réelle montre une rafale sans
yield, ajouter une limite qui force une réduction intermédiaire sans supprimer ni
réordonner de texte; ne pas introduire un drop silencieux.
### T1 — le test store compactage ne fournit pas une sortie naturelle
Les 11 assertions passent avec rejets stricts, mais le processus reste vivant
au-delà de 120 s sans `--test-force-exit`. Avec l'option de force, 11/11 passent
en 0,77 s. Cela correspond à la convention déjà déclarée pour les stores importés
et ne démontre pas une fuite du nouveau buffer, mais ce fichier seul ne constitue
pas un gate de lifecycle naturel. Les 10 tests navigateur intégrés ci-dessous
sortent naturellement.
## Contrats compactage vérifiés
- Chaque delta avance immédiatement `messageRevisions`, `fullDataRevisions` et
la fence de lecture du reducer observé; l'agrégat réentrant n'avance pas ces
révisions une seconde fois.
- Un buffer est séparé par couple instance/session. Un événement non-delta de la
même session force l'ordre; les autres sessions et instances ne forcent pas le
flush.
- `ended` et `failed` annulent les fragments car le SDK 2.0.21 remplace le résumé
par `ended.data.text` ou remplace la ligne par l'erreur. Delete, reconnect,
prune, revert committed et dispose annulent aussi avant qu'un timer puisse
ressusciter le contenu.
- Une lecture SDK déclenchée avant admission est refusée par révision. Une lecture
après admission force d'abord l'agrégat; la page native qui contient déjà ce
texte n'est pas réappendue.
- Rotation à 200 messages, idle, nouvelle exécution et second start conservent
l'ordre. La queue de rotation et la resynchronisation autoritaire restent les
fences existantes.
- Une session inactive jamais chargée ne crée pas de transcript ni de payload de
reducer; seules les fences scalaires et le marqueur de compaction inachevée
persistent. End/failed/dispose les retirent. Une activation intermédiaire passe
par la page autoritaire, conserve la fenêtre UI de 200 messages et reprend
ensuite le streaming sans duplication.
- Une session chargée puis inactive réduit l'agrégat mais n'hydrate pas sa vue
cachée; son retour recharge le texte exact.
Les cinq scénarios navigateur utilisent le vrai `SessionView`, le dispatcher
natif et les stores. Le cas cadencé livre huit événements dans huit tâches
séparées par 10 ms; ce n'est pas uniquement le backlog synchrone historique.
## Renderer imbriqué : propriétés validées hors M1
- Les getters réactifs mettent à jour output, copy et erreurs sans remonter le
shell d'une clé stable; disclosure, scroller et scrollTop restent conservés.
- Le `Show keyed` externe remonte le shell quand `Index` réutilise une position
pour une autre clé, donc l'état d'une étape évincée n'est pas transféré.
- Une invalidation de lecture conserve le snapshot résident; le full rescan après
retour autoritaire et la page vide suppriment les messages obsolètes.
- La limite de scan reste 10 000 unités et l'affichage 200 étapes. Le patch ne
rend pas le scan ou le nombre de shells non borné.
- L'output d'une page native à révision de part zéro utilise de nouveau son hash
Markdown et n'affiche pas le cache de l'ancienne page.
## Diagnostic SSE / flush Solid
La correction ne prétend plus capturer une dérivation Solid dans le `try/catch`
de chaque abonné. Elle sépare correctement les frontières : JSON invalide produit
`Failed to parse event`; une exception qui sort du flush de `solidBatch` produit
`Failed to dispatch event`. Elle ne déclenche ni reconnexion ni rejeu, et les
événements suivants continuent en FIFO. Le test navigateur dérivé traverse le
vrai EventSource et confirme les deux événements chez les abonnés natif et typé.
Les risques déjà acceptés restent inchangés : ce logger `debug` est désactivé par
défaut, et la course serveur abort local / erreur amont n'est pas couverte ici.
Aucune nouvelle promesse de diagnostic persistant ou d'isolation du flush n'est
faite.
## Gates exécutés
- `node --unhandled-rejections=strict --import tsx --test --test-concurrency=1 packages/ui/tests/browser/compaction-responsiveness.test.ts packages/ui/tests/browser/event-subscriber-isolation.test.ts` — **10/10**, sortie naturelle.
- `node --unhandled-rejections=strict --import tsx --test --test-concurrency=1 --test-name-pattern="^(unrelated child text|a failed child refresh|native page output|changing an indexed task key)" packages/ui/tests/browser/render-cost.test.ts` — **4/4**, sortie naturelle.
- `node --conditions=browser --unhandled-rejections=strict --import tsx --test packages/ui/src/lib/event-source-handlers.test.ts` — **4/4**, sortie naturelle.
- `node --conditions=browser --unhandled-rejections=strict --import tsx --test packages/ui/src/stores/opencode-compaction.test.ts` — **11 assertions vertes**, mais timeout de lifecycle à 120 s.
- Même test avec `--test-force-exit` — **11/11**, 0 échec.
- `npm run typecheck --workspace @codenomad/ui` — vert.
- `git diff --check` ciblé — vert, hors avertissements LF/CRLF du worktree.
Les deux reproductions ad hoc du finding M1 ont utilisé un serveur Vite et un
Chromium privés loopback, sans fichier temporaire dans le dépôt, daemon, profil,
base utilisateur, restart ou déploiement.
## Taille des fichiers
- `packages/ui/src/stores/opencode-data.ts` : ~828 lignes, au-dessus de la cible
de 800.
- `packages/ui/src/stores/instances.ts` : ~2 093 lignes, très au-dessus de la
cible.
- `packages/ui/src/components/tool-call/renderers/task.tsx` : ~647 lignes,
au-dessus du seuil d'avertissement.
- `packages/ui/src/stores/compaction-delta-buffer.ts` : 56 lignes.
Aucun refactor de taille seul n'est recommandé dans cette mission.
## Addendum — disposition du finding M1
### Verdict de suivi
**M1 est clôturé.** La correction de membership intégrée après la revue initiale
traite les suppressions, remplacements et réordonnancements autoritaires sans
réintroduire les remounts qui motivaient le correctif de performance. Le verdict
« corrections demandées » ci-dessus est donc levé pour cette surface. Les
observations R1 et T1 restent des limites non bloquantes, inchangées.
Cette disposition repose sur une revue indépendante du nouveau diff de
`task.tsx`, `render-cost.test.ts` et `fixtures/render-cost.tsx`; elle ne fusionne
pas les chiffres A/B historiques avec les mesures de ce suivi.
### Pourquoi la clé fantôme ne subsiste plus
- À chaque révision de session, le renderer recalcule les identités structurelles
dans le budget existant, au lieu de ne détecter que l'augmentation du nombre de
parts. Une baisse 2→1, un retour 1→2, un remplacement/reorder 2→2 et une part
passée de `tool` à `text` sont donc tous visibles.
- Le scan reste borné par `TASK_MESSAGE_SCAN_LIMIT = 10_000` et collecte au plus
`TASK_STEP_RENDER_LIMIT + 1`, puis affiche les 200 dernières clés.
- Si la séquence de clés est strictement identique, le setter conserve le tableau
précédent. Si elle change, `<For>` associe les lignes aux valeurs de clé plutôt
qu'à leur position : les clés retenues conservent leur composant et leur état,
tandis que les nouvelles identités obtiennent un shell neuf.
- Le snapshot résident reste affiché pendant une lecture invalide; le rescan se
fait sur le retour autoritaire. Une erreur conserve toujours le snapshot et le
retry existants.
### Régressions discriminantes
Les nouveaux cas vrais composants/stores/pages natives vérifient :
1. suppression puis ajout dans le même message, pour chacune des deux positions
initiales : compteur `2→1→2`, nouvelle clé unique, shell/disclosure/scroller et
`scrollTop` de la clé retenue inchangés;
2. remplacement et reorder à cardinalité égale : ordre exact `[step-2, step-0]`,
`step-1` absent et shell de `step-0` conservé;
3. identité de part identique dont le type passe de tool à texte : compteur réduit
et ligne supprimée;
4. fenêtre `201→199` : toujours 200 éléments maximum, puis 199, sans diagnostic de
troncature obsolète;
5. 80 étapes soumises aux deltas de texte non structurels : chaque échantillon
conserve `added=0` et `removed=0`. Les 80 snapshots de données déjà documentés
restent présents; aucune nouvelle revendication ne les élimine.
### Validation fraîche du suivi
- `node --unhandled-rejections=strict --import tsx --test --test-concurrency=1 packages/ui/tests/browser/render-cost.test.ts` — **13/13**, sortie naturelle (~45 s).
- `node --unhandled-rejections=strict --import tsx --test --test-concurrency=1 packages/ui/tests/browser/task-copy.test.ts packages/ui/tests/browser/tool-images.test.ts` — **9/9**, sortie naturelle (~37 s).
- `npm run typecheck --workspace @codenomad/ui` — vert.
- `git diff --check` sur les trois fichiers revus et ce rapport — vert, hors
avertissement LF/CRLF du worktree.
Échantillon descriptif frais des dix deltas à 80 étapes : temps de tâche entre
environ **9,2 et 16,1 ms**, temps synchrone rapporté entre **0,1 et 0,3 ms**, avec
`added=0` et `removed=0` pour chaque échantillon. Il s'agit d'un fixture Chromium
de développement et non d'un nouveau benchmark desktop ou d'une garantie FPS.
Aucun daemon, profil, base utilisateur, restart, déploiement, dépendance, commit
ou push n'a été utilisé pour ce suivi.