mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-07 16:09:43 +02:00
feat(terminal): renderer watchdog, atomic replay clear, fetch deadlines, reconnect recovery
Four ways the terminal can silently stop being correct — in each case the
buffer keeps updating, nothing throws, and the only recourse is a reload.
1. Renderer freeze after backgrounding. iOS DISCARDS scheduled rAF callbacks
when a PWA backgrounds, and xterm's RenderDebouncer only clears its
`_animationFrame` handle from inside that callback — so one drop leaves it
permanently set and every later refresh() early-returns. Parsing is
decoupled from rendering, so bytes keep filling the buffer correctly while
nothing paints. Codeman has exactly ONE xterm for the whole page load, so a
single backgrounding wedges it until a reload. Adds a 2s liveness poll and
`_kickRenderer()`, which does what the dropped `_innerRefresh` would have.
2. Replay clears raced live output. xterm's write() is async-queued while
reset() is synchronous and, per upstream, "does not clear input buffers and
does not reset the parser" — so bytes queued before a reset are parsed after
it and fuse into the snapshot. Verified against the real xterm 6 here:
write('p8'); reset(); write('rmissions') renders "p8rmissions". The main
path was already safe via a queued erase; the needsRefresh and clearTerminal
paths were not. All three now share one queued `\x1bc` (RIS), which unlike
3J/H/2J also resets modes, charsets, scroll regions and SGR state.
3. Output lost on WebSocket reconnect. Input frames carry seq+cid and are
delivered exactly once; output frames carry nothing. ws.onopen re-sends dims
and flushes queued input, and needsRefresh only fires on external-CLI
startup and SSE backpressure drain — never on reconnect. Output produced
while offline was simply absent afterwards. Interim fix: reaching onclose
means the drop was unintentional, so the session is marked and the next open
reconciles from the server buffer. Sequencing output is the follow-up.
4. Terminal captures had no deadline. No AbortController anywhere in the
frontend, including `?full=1`, which the code itself calls "unbounded-ish
work: at the default history limit it can be megabytes". Adds a budget that
scales with full-vs-tail and with captures in flight, degrading to a plain
fetch where AbortController is missing.
Also: the service-worker precache was dead — the build content-hashes assets
but sw.js listed pre-hash names, so 15 of 23 entries 404'd (verified against a
running instance) and cache.add().catch() hid it. Offline still worked via
runtime caching, but CACHE_NAME was a constant so activate's cleanup never
deleted anything and every past release's assets accumulated. Both are now
derived from the build manifest. Crash-trail entries are flattened and capped,
since they are joined with \n into one value and one call site interpolates a
server-controlled WS close reason.
The watchdog reads xterm privates — there is no public API. Every access is
optional-chained so a shape change degrades to a no-op. `_renderService` only
exists after open(), which needs a real DOM, so the gate cannot assert the
field path; test/xterm-private-api.test.ts pins the dependency range instead.
Tests: 23 new (terminal-resilience, sw-precache-manifest, xterm-private-api),
all pure/static so they run in the gate, which excludes the mobile suite. One
static source guard in history-truncation-notice updated for the renamed call;
the behaviour it pins is unchanged.
Not verified: no browser available, so no runtime reproduction of the freeze
and no real-device test of the reconnect path. Both warrant a device pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
9466acfc1a
commit
c0422c4e21
+25
-16
@@ -18,7 +18,16 @@
|
||||
* @see src/push-store.ts -- server-side VAPID key management and subscription CRUD
|
||||
*/
|
||||
|
||||
const CACHE_NAME = 'codeman-v1';
|
||||
// Build identity. scripts/build.mjs rewrites this declaration after it content-
|
||||
// hashes the assets; the literal below is what dev serves, and dev wants a
|
||||
// stable key.
|
||||
//
|
||||
// Why the cache key MUST carry it: `activate` deletes every cache whose key is
|
||||
// not the current one, so the old constant key meant that cleanup never deleted
|
||||
// anything — hashed assets from every release ever deployed accumulated in one
|
||||
// bucket until the origin hit its storage quota.
|
||||
const BUILD_ID = 'dev';
|
||||
const CACHE_NAME = `codeman-${BUILD_ID}`;
|
||||
|
||||
// Reverse-proxy base path: the worker is served at `<base>/sw.js`, so its own
|
||||
// location tells us the mount prefix ('' at root, or '/codeman'). Every URL below
|
||||
@@ -27,27 +36,27 @@ const CACHE_NAME = 'codeman-v1';
|
||||
const SW_BASE = self.location.pathname.replace(/\/sw\.js$/, '');
|
||||
const B = (p) => (p && p[0] === '/' ? SW_BASE + p : p);
|
||||
|
||||
// Content-hashed assets. scripts/build.mjs rewrites this declaration with the
|
||||
// filenames it actually emitted; dev has no hashing, so the empty literal below
|
||||
// is correct there and the unhashed modules are simply cached on first use by
|
||||
// the runtime handler further down.
|
||||
//
|
||||
// This list used to be maintained by hand with the PRE-hash names, which the
|
||||
// build then renamed — so in production every entry 404'd and the silent
|
||||
// `.catch()` in install swallowed all of it. Measured against a running
|
||||
// instance: 15 of 23 entries failed. Offline still worked, because the fetch
|
||||
// handler caches every successful GET at runtime, but the precache warmed
|
||||
// nothing while looking like it did. Deriving it from the same manifest that
|
||||
// renames the files is the only thing that keeps the two from drifting again.
|
||||
const HASHED_ASSETS = [];
|
||||
|
||||
// Core app shell -- cached on install for instant startup
|
||||
const APP_SHELL = [
|
||||
'/',
|
||||
'/styles.css',
|
||||
'/mobile.css',
|
||||
'/constants.js',
|
||||
'/app.js',
|
||||
'/api-client.js',
|
||||
'/terminal-ui.js',
|
||||
'/session-ui.js',
|
||||
'/settings-ui.js',
|
||||
'/panels-ui.js',
|
||||
'/notification-manager.js',
|
||||
'/mobile-handlers.js',
|
||||
'/keyboard-accessory.js',
|
||||
'/voice-input.js',
|
||||
...HASHED_ASSETS.map((p) => '/' + p),
|
||||
'/vendor/xterm.min.js',
|
||||
'/vendor/xterm-addon-fit.min.js',
|
||||
'/vendor/xterm-addon-unicode11.min.js',
|
||||
'/vendor/xterm-zerolag-input.js',
|
||||
'/vendor/xterm-predictive-echo.js',
|
||||
'/vendor/xterm.css',
|
||||
'/icon-192.png',
|
||||
'/icon-512.png',
|
||||
|
||||
Reference in New Issue
Block a user