mirror of
https://github.com/stablyai/orca.git
synced 2026-09-26 08:02:38 +00:00
75ed7f06ddb35cbccb6fac50cdba2249072955fc
11540
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
75ed7f06dd |
style(mobile): put every document import back in its import block
pullfrog caught `fit-scale`, where `scheduleDocumentFrame` sat below `MIN_FIT_COLS`. A statement walk over all 41 non-test modules under `document/` — the tree, not a grep, so a multi-line import or one inside a comment cannot hide — found one more: `host-notify` split its two `document-host-seams` lines around the re-export between them. Both imports moved up; the re-export stays where it was, below the block, with its comment in the present tense. The sweep reports no misplaced import across the 41 modules now. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
8e55c8c65c |
style(mobile): reverse the stop order without mutating the list
`Array#reverse` mutates, which the lint rule refuses and which would have left the census comparing a list it had just reordered. `toReversed` on the filtered copy says the same thing and cannot. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
438bb24777 |
docs(mobile): put the engine doc block back on the engine seam, in the present tense
Round 1's last two. The block describing how the WebView knows the engine is there had ended up above `windowCapturedEngineErrors`, one function too high; it is on `windowHasEngine` again, with that function's own note about the optional global folded in. Two references that had outlived what they named: the host-seams case cited `runtime-constants`, which this branch deleted when its one element read moved into `startSurfaceSwap`, and now cites modules the sequence still has; the query-reply harness narrated what its oracle used to be instead of what it is. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
5e5f16767d |
test(mobile): show that the page's capture buffer is written, and say what writes it
Round 1 read the page's `capturedEngineErrors` array as never written and asked for an empty readonly list instead. It is written, and the empty list would drop a line from every report the page makes: `startHostNotify` installs the reporter through `installErrorReporter`, which on the page is a `window` error listener, and the reporter appends each error it forwards before `reportEngineError` quotes the buffer back. `host-notify.ts` is the file that proves it; the comment on the mount said none of this and now says it. The pre-start window round 1 asked about is the half the page genuinely cannot have. The WebView's `<head>` opens its buffer before the engine script tag, so an engine that throws while loading is captured by something no document has started; on the page the engine is a static import of this module, so there is nothing to capture before the document exists. Red-first: two errors dispatched at a real mount, and the reports quote `captured: first failure` then `captured: first failure | second failure`; a second mount quotes its own line and not the first document's. Answering the seam with `() => []`, which is what round 1 asked for, fails the first assertion. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
0a8d071db4 |
test(mobile): read the bundle census and the artifact off one build
Round 1's finding: the input assertion built the bundle while the wrapper assertions read the committed string, so under the presets-to-preferences control the rebuild red and `__commonJS` passed against a stale artifact. And `inputs.length > 40` was a bound, not a census. `terminalDocumentBundle()` returns the text and the module list from one build, and `buildTerminalDocumentScript` is that function's text — so the thing measured is the thing written. The case asserts the exact input count, no node_modules input, neither wrapper, and that the committed artifact equals what the sources build. A stale artifact now reds. Controls: the presets pointed back at `storage/preferences` fails on the inputs and on `__commonJS` in the same run; `MIN_FIT_COLS` changed to 21 without rebuilding fails the equality. Worth knowing for the next reader: an unused export or a dropped comment does not fail it, because esbuild does not emit either — the assertion is about what ships. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
69b71c9ec8 |
test(mobile): compare the start sequence with what the modules export, by name
Round 1's finding on the census: completeness was a count, so a module exporting a start nobody calls red only as `expected 11 to be 10` and greened again the moment the literal moved with it. The two sets are compared by name now, with the order asserted as well: the starts the factory calls are every exported `start*(scope)` there is, the stops are every exported `stop*(scope)`, and the stops of modules that have both run in the reverse of the order their starts did. `cancelDocumentFrames` is held out of the set comparison and asserted by position instead — last, after every stop that might still hold a frame. `stopEdgeScroll` is the one exported stop the sequence does not call, and it is not a lifecycle undo but the overlay's own for a drag that is over; the test asserts `stopSelectionOverlay` reaches it rather than waving it through. The names come from the tree rather than the text, because a regex over the file would also match the sequence's own name in the unwind inside `startTerminalDocument`'s catch. Red-first control, `export function startReflow(scope)` added to `reflow.ts` and not called: the set comparison fails naming `startReflow`, where the count failed with a number. A second case plants the same shape against the reader itself, so the comparison is a measurement rather than an agreement between two empty lists. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
1c7d98d309 |
docs(mobile): separate the lane's closure reading from main's
The byte figure in the note was measured before `origin/ota-c7-5b-document-factory` and the main it carries were merged in. This head reads 3,765,180; the 243 between the two are the touch-root predicates and main's #21687 momentum change, which are not this lane's to claim in either direction. Both numbers are named rather than one of them silently replaced. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
c11fab5f94 |
Merge origin/ota-c7-5b-document-factory into ota-c7-5c-scope-threading (OTA phase C, C7.5c)
Two review rounds of C7.5b, threaded. Both folds are page-wide reads a second document on the page turns into another terminal's input: - `eventTargetInRoot(root, target)` asked once at the top of each of the four document-level touch handlers. The handlers already took the scope, so the predicate reads `scope.root` and `touchIsThisDocuments` takes it too. - `touchesInRoot(root, e.touches)` at the top of each of the six touch handlers, with every count and index reading the filtered list. Threading resolved the rest: `clearLongPress`, `touchSlopExceeded`, `getTotalScale`, `clampPan`, `updateTransform`, `shouldRouteScrollToTerminalInput`, `resetSmoothScrollOffset` and `getCellHeight` all take the scope here. `root === null` is the WebView, where the document is the page: both predicates pass everything through and the native document is unchanged. Resolved toward this branch everywhere the two disagreed. The two-host tests and the page mount's unit tests import the modules and build scopes with seams, as the other rewrites do, rather than importing the generated factory this branch deleted. The scroll-routing oracles read the module's TypeScript rather than the emitted script, and carry main's #21687 momentum change: the decay is per elapsed millisecond now, so the assertions name `lastMomentumTime`, the clamped `elapsed` and `vel * elapsed`. Deleted again, as the branch deleted them: `terminal-document-golden.txt`, `terminal-webview-payload-hash.test.ts` and `document-factory-artifacts.test.ts`. The last is round 1's L2, which pinned the two generated wrappers line by line — the page's wrapper no longer exists, because the page imports the modules, and the native script's wrapper is esbuild's IIFE rather than text a generator writes. What replaces both is `native-document-bundle.test.ts`, which runs the bundle. Terminal suite 64 files / 621 tests. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
f3e3e5f8b2 |
docs(mobile): correct the closure reading for the text-scale leaf
The presets moving to their own module adds one more to the page's closure than the reading recorded, and five bytes with it. Measured the same way on both sides. modules 4320 -> 4322 (+2) local modules 970 -> 972 (+2) minified bytes 3,768,122 -> 3,764,937 (-3,185) Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
5a5caafd07 |
fix(mobile): keep AsyncStorage out of the WebView document, and prove it
The 13 KB the bundle grew is not esbuild's lazy `__esm` wrappers and not a cycle: there are none of either. It is a dependency that rode in. `text-scaling` reached `storage/preferences` for the text-scale presets, and that module imports AsyncStorage, so the phone's document carried AsyncStorage, `merge-options` and `is-plain-obj` — 11.6 KB of storage library inside a string with nothing to store, wrapped in esbuild's CommonJS interop. The old generator hid this: it substituted the presets as a JSON literal, so the import never reached the emitted text. The presets move to `terminal/terminal-text-scales.ts`, a leaf with no imports of its own, which `storage/preferences` imports and re-exports for the settings screen. The bundle: 113,442 characters, 3,465 lines, 47 inputs, none from node_modules was 120,217 characters with 6 node_modules inputs and three `__commonJS` wrappers the golden it replaces was 110,085 bytes `minify: false` stays, for the reason given: the overlay reports the line and column `window.onerror` hands it. Three cases join the bundle evaluation, and each was made to fail before it was kept: - A `set-theme` and a `write` before `init`, which is what a byte golden covered by accident. Read from the router rather than assumed: the theme applies to the scope and paints through the seam, the chunk normalises and queues, the pump returns because there is no terminal, and `init` then resets the queue and the mode scan so the early chunk is dropped and the init frame's own theme wins. Deleting `resetWriteQueue` from `init` makes the terminal write `early chunk\x1b[0mreplayed` — the early bytes ahead of the snapshot, which is the corruption the reset prevents. - The transport, both ways: the native document installs `message` on `window` and on `document` and keeps them, because the WebView never stops its document; the page's factory with a no-op transport installs none, so none of the shell's own frames are taken. Installing on one target fails the first half, installing a real listener the second. - What the bundle carries: no input from node_modules, no `__commonJS`, no `__esm(`. Pointing the presets back at `storage/preferences` fails it with the six inputs named. The build options become one object the census and the build share, so what is measured is what ships. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
44e7aa9521 |
docs(mobile): re-measure the session route's closure for the threaded document
The reading in the closure test's note is against `origin/main` at
|
||
|
|
144483f50c |
refactor(mobile): fold the document's never-written constants out of the scope
Seventeen scope fields were never assigned after the factory built them. They were fields because the generator substituted them into one function scope and a module cannot import into a string; each is now a `const` in the module that owns it. - `escape-introducers.ts` holds `ESC` and `C1_CSI`, as the bytes rather than as `String.fromCharCode` calls a JSON substitution needed. - `write-queue.ts` owns the status dot, its two presentation selectors, the pattern and the DECSET tail limit. The pattern is a regex literal: a `new RegExp` at a module's top level is parse-time work, which ruling 20 refuses and the census measures. - `fit-scale.ts` owns `MIN_FIT_COLS`, `text-scaling.ts` the ends of the preset range, `tap-dispatch.ts` the press and tap thresholds, `selection-range.ts` the word pattern and `selection-overlay.ts` the edge-scroll distance and tick. Four functions stop taking a scope they no longer read: `isStatusDotPresentationSelector`, `endsWithStatusDotPresentationSequence`, `extractMouseModeScanTail` and, with `normalizeInitialData`, `isAltScreenActive`. `runtime-constants.ts` held no constants once they moved out, only the surface element read. That read is the first line of `startSurfaceSwap` now, which is the module the field is documented as belonging to, and the start sequence is ten calls rather than eleven. The engine error buffer becomes a seam, `capturedEngineErrors`. The WebView's `<head>` keeps its own: it opens before the engine script tag, so an engine that throws while loading is captured by something no document has started yet, and the first report quotes it. That is why the head declaration stays where the design said it would go — the page's mount answers the seam with a buffer per mount instead of assigning a window global, which is what the document no longer touches. Two of the page's three casts are gone, and tsc is what says so: xterm's cell attribute getters answer numbers, and `getLine(...).getCell` answers `undefined` rather than null, so the shape now describes the engine it was written against. The third stays with a narrower reason: `getCell` takes back the cell xterm allocated, and describing that parameter means naming xterm's whole cell type where the document declares the six members it reads. Comments that narrated the extraction — the scope table's `var` census, "the flip", "the main slice", "C7.1 extracts" — say what the code does instead. Oracles that moved with the constants: the reflow floor and the status dot now read the owning module's `const` rather than a scope-factory line; the write-queue harness resolves the module's one value import; the parse-time census asserts the stronger fact that no module does parse-time work, with the element reader aimed at every module and its presence proved by the planted case. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
a82a8b1459 |
Merge origin/main into ota-c7-5b-document-factory (OTA phase C, C7.5b)
`bd860f34c1` (#21687, OMP terminal momentum normalised across refresh rates) landed on main and moved the same three files this branch does. Document modules #21687 changed: `surface-touch-gestures.ts` only, and only inside the momentum loop of its touchend handler — `momentumStep` takes the animation frame's timestamp, holds `lastMomentumTime`, decays by `FRICTION ** (elapsed / 16)` and scales the delta by `elapsed` instead of a fixed 16 ms. It auto-merged with this branch's change to the line above it (`touches.length === 0`, fold 5), so the module carries both: the loop is main's and the count is this branch's. No other document module was touched by either side in the same file, and there was no module-level conflict to resolve. `terminal-document-golden.txt` and `terminal-webview-payload-hash.test.ts` conflicted because both sides re-pinned them. Resolved by regenerating from the merged modules (`node scripts/build-terminal-document-script.mjs`, then `build-terminal-document-fixture.mjs`), never by taking a side: the golden now carries main's six momentum lines and this branch's seven `touchesInRoot` lines, which is the only reading of the merge that is true of the modules. Golden byte length: 729,152 on this branch and 726,574 on main, 729,363 merged. Fixture 111,329 bytes. sha256 0263338994... (ours) and 775a129414... (main's) -> 0be398e4d7... Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
8dab23e4fe |
fix(mobile): count only the fingers inside this document's own host (OTA phase C, C7.5b round 2)
Round 2's residual of `9824145e1f`'s class, one level in. `eventTargetInRoot` settles whose event it is; every branch then counts `e.touches`, which is every finger on the screen. A finger resting in host A is therefore B's second finger: a one-finger touch in B's own surface reads `length === 2`, latches a pinch and drops B's selection, and on touchend `length === 0` is never true so B's surface tap never fires. `touchesInRoot(root, touches)` beside `eventTargetInRoot` returns this document's own fingers, and every count and index reads through it. A list rather than a count, because `touches[0]` and `touches[1]` are page-wide in exactly the same way as `touches.length` — the first finger on the screen may be the other terminal's. `root === null` is the WebView, whose fingers are all its own: the list is returned untouched, so nothing is allocated on a path that runs at frame rate. Census of every `touches` / `changedTouches` / `targetTouches` read under `src/terminal/document/` (non-test). There are no `changedTouches` or `targetTouches` reads at all; every read is `e.touches`: - `tap-dispatch.ts`, 15 reads across the three handlers that take an event (`[0]`, `[1]`, `.length`, and the list handed to `touchById`): MUST be filtered. The document listens on `document`, so the event and its list are both page-wide. - `surface-touch-gestures.ts`, 18 reads across its touchstart, touchmove and touchend handlers: MUST be filtered. These listeners are on the document's own surface, so the event is always this document's — but the list inside it is still every finger on the screen, which is the whole defect. - `tap-dispatch.ts:21-24`, `touchById(touches, id)`: no filtering of its own. It reads whatever list it is given, and all three callers now hand it a filtered one; its parameter widens from `TouchList` to `ArrayLike<Touch>`. Red-first in `document-host-root.test.ts`, the reviewer's two repros, with the three product files at `aba99c3e4f` and the artifacts rebuilt: 2 failed / 3 passed (pinch cancel posted with one finger on B's overlay; `terminal-tap` never posted). With the fix: 5 passed. The pinch-inside-own-host control stays, and the first repro lands on B's menu pill rather than its surface, because a single finger on the surface dismisses a selection by design — on the pill, keeping the selection is the whole assertion. `document-host-seams.ts` also rewritten in the present tense where it read as history. Golden re-pinned: 19 hunks, +51/-33. `touchesInRoot` emitted after `eventTargetInRoot`; one `const touches = touchesInRoot(scope.root, e.touches)` at the top of each of the six touch handlers, and every `e.touches` read inside them now reads `touches`. Document 728,589 -> 729,152 bytes, sha256 1556f532... -> 02633389... Correction to `9824145e1f`'s message: it cites `tap-dispatch.ts:241-244` for the four installs, which at that commit are `261-264` (the line numbers are the pre-fold ones from the review). Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
14499f4d9a |
refactor(mobile): dissolve the document's constants module into its owners
`document-constants.ts` existed because the document was a string: a string cannot import, so the generator substituted JSON literals into the text and the web page imported the same bindings to keep one source. The document imports now, so the indirection is a re-export shim over four real owners and each site reaches the owner instead. - `terminal-theme.ts` takes the background fallback from the theme's own `colors`. - `url-tap.ts` imports the two patterns and the length cap from `terminal-webview-url-tap.ts`, which is where the page's copy reads them, and the three local aliases go with the substitution they were shaped for. - `text-scaling.ts` and `document-scope.ts` take the presets from `storage/preferences`. - `terminal-init.ts` and `document-scope.ts` take the caret options and the built-in theme from `terminal-webview-html/theme.ts`. One test oracle moves with it: the URL tap's "both copies spell the pattern identically" case pinned the substituted assignment line, and now reads the document module's import of the page's own constant. The resolver cases that compare the two behaviours are unchanged. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
e476193bf5 |
chore(relay): bound the shadow health gate and apply a pending backend update on resume (#21865)
* fix(relay): bound the same-cap shadow gate and apply a resumed backend update Two findings both adversarial reviews of tonight's merged set agree on. The report-only shadow health gate (#21849) had `continue-on-error: true` but no step timeout. That bounds the step's contribution to the job outcome, not its clock. Its reads are serialised, and a failure that answers nothing slowly — an expired credential, a project-wide Logging 429 storm — makes every read cost its full 3 x 60 s retry budget, so the cost scales with the roll window: roughly 8S + 2 reads for S ten-minute sub-windows. A 40-minute window is about 34 reads, or 108 minutes, against the job's `timeout-minutes: 75`. A cancelled job cannot be absorbed by continue-on-error, fires the failure-gated cleanup isolation on an already-restored cell, and stops the strict next-cell chain. Give the step `timeout-minutes: 5` and the artifact upload `timeout-minutes: 2`. A timed-out step is a failed step, which continue-on-error covers, so the job stays green. Inside the script, stop reading after an overall four-minute deadline and report the remaining checks unverified, so the normal outcome is a written verdict rather than a killed process; the step timeout is then only for a hung process. The census test pins both timeouts and that the deadline leaves the step time to write its verdict. The resume branch (#21860) accepted `changes == 0` with a non-empty `backendUpdate` as complete and applied nothing, so a resumed cell silently kept the 300-second drain and no request logging behind a green resume. That shape means the template and MIG are converged and only this cell's reviewed backend update is left, so apply the saved resume plan — the validator has already bounded it to this cell's backend and neither attribute restarts an instance — then continue as converged. Template-and-MIG drift still applies nothing, which is what a resume means, and a stranded cell's explicit MIG replace is unchanged. Claude-Session: relay-same-cap-gate-timeout-and-resume * fix(relay): raise the shadow gate bounds clear of a healthy gate's read time A healthy gate is already minutes of serial reads on the 2-vcpu runner, so a four-minute deadline would report unverified tails on ordinary days and stop the shadow roll measuring the comparison it exists for. Raise both together: the step to eight minutes and the script's own deadline to seven, keeping the census pin that the deadline leaves the step room to write its verdict. The job budget is unaffected: a ~14-minute cell plus eight is well inside 75. Claude-Session: relay-same-cap-gate-timeout-and-resume |
||
|
|
3db9459d14 |
refactor(mobile): thread the document scope and bundle the page's script
Ruling 25. The terminal document stops being a string the build machinery assembles
and becomes ordinary TypeScript: every module function that reads document state takes
`scope: TerminalDocumentScope` as its first parameter, `document-scope.ts` exports the
types and `createTerminalDocumentScope(host)` and nothing else, and `tsc` is the oracle
that the threading is whole (a missed parameter is a type error).
`create-terminal-document.ts` is hand-written, not emitted: it builds the scope, runs the
eleven starts inside a try that unwinds on a throw, stops the ten in reverse with
`cancelDocumentFrames` last, and returns `{ send, stop }`. `native-document-entry.ts` is
one statement. The concatenator becomes an esbuild IIFE bundle of that entry at the
Chrome 74 floor, written as a string the same way the engine artifact is.
The byte pin cannot survive that and does not try to: esbuild merges module scopes and
renames the threaded parameter (`scope` -> `scope2` where two modules collide), so the
document's text is no longer a stable artifact and the behavioural suites are the proof.
`native-document-bundle.test.ts` evaluates the real bundle and reads three behaviours the
phone depends on: it announces `web-ready`, an init message opens the engine inside
`#terminal-surface`, a ping is answered, and a missing engine global reports fatally
instead of starting.
Rewritten tests, old oracle -> new oracle:
- host-seams, write-queue, document-frame-registry, document-parse-time-effects:
evaluated a region of the generated text -> import the module and pass a scope the case
builds.
- terminal-webview-engine (WebGL recovery), terminal-webview-theme,
terminal-webview-text-zoom, terminal-webview-query-reply,
terminal-keyboard-avoidance-webview: a `vm` evaluation with injected globals -> the
imported functions over a scope whose seams are the case's own doubles.
- terminal-webview-url-tap: a function extracted out of the document's text and evaluated
-> the document's `osc-link-tap` exports imported directly.
- terminal-webview-reflow, terminal-webview-scroll-routing, terminal-path-tap,
terminal-webview-tap-routing, terminal-webview-wheel-scroll: assertions over the
assembled document text -> the same assertions over the module's own source, read
through `document-module-source.test-support.ts` (TypeScript, so no semicolons).
- document-host-root, document-start-unwind: imported the generated factory -> import
`create-terminal-document`.
- terminal-webview-consumer-census: the generated factory was a census exception -> it no
longer exists.
- config/scripts closure tests: re-measured for the bundled document.
Deleted with the machinery they served: the concatenator's emit/substitute/order code,
`terminal-document-module-order.mjs`, the document fixture builder,
`generated-document-region.test-support.ts`, the byte golden and its identity test, the
payload-hash pin, `document-factory-emit`, `document-factory-artifacts`,
`document-module-order`, and the generated factory artifact with its gitignore and
lint-ignore lines.
Two assertions the tests no longer need: the published contrast floor arrives unvalidated
from a host of unknown version, so `TerminalDocumentThemeMessage` types it the way the
router types its other wire fields and `normalizeTerminalContrastOverride` remains what
decides it is a number; the recovery harness holds its timer callback in a wrapper rather
than asserting a narrowing.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
|
||
|
|
30f2bc60f9 |
fix(antigravity): recognize non-Gemini tui-idle prompts (#21231)
* fix(antigravity): recognize non-Gemini tui-idle prompts * fix(antigravity): reject stale composer caret in model picker * fix(antigravity): do not treat a wrap continuation caret as ready An unsent composer can show `> draft` then an indented `>`. That continuation is not an empty input box, so tui-idle must stay false. * test(antigravity): align later bare-caret status expectation --------- Co-authored-by: Neil <neil@stably.ai> |
||
|
|
ea5152f1c2 |
fix(orchestration): line-settle delay for antigravity multiline paste (#21665)
* fix(orchestration): retry Enter after cursor-agent worker-start paste Worker-start dispatches through bracketed paste in the main process; cursor-agent can leave long prompts as "Pasted text +N lines" and swallow the first Enter. Apply the same submitRetryDelayMs path Codex uses in the renderer, but only for agents without the Claude/Codex render gate so hook turn-start reservation stays intact. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(orchestration): line-settle delay for antigravity multiline paste Antigravity 1.2.x expands long bracketed paste slowly ("↑ N more lines") while Orca only waited for byte ingest (~500 ms on macOS). Add submitLineSettleMsPerLine and retry Enter for antigravity; wire agent-aware submit scheduling through the main-process prompt writer and plain terminal.send suffix path. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(orchestration): antigravity line-settle only; drop unverified retry Address PR review: revert accidental pnpm-lock.yaml churn, remove cursor and antigravity submitRetryDelayMs until live-verified, keep submitLineSettleMsPerLine for agy multiline paste, and move the regression test out of the 900+ line runtime submission suite. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
bd860f34c1 |
fix(mobile): normalize OMP terminal momentum across refresh rates (#21687)
Use elapsed animation-frame time for OMP terminal momentum and update the generated payload contract. |
||
|
|
438744ca77 | fix(opencode): preserve global config discovery (#21854) | ||
|
|
aba99c3e4f |
test(mobile): pin both generated wrappers, and say what a shadow root would break now (OTA phase C, C7.5b round 1)
Round 1 M1 and L2.
M1, the mount's stylesheet comment was one version behind: it said the document
reads its elements with `document.getElementById`, which `712daa80e6` replaced
with `elementInRoot`, and drew its shadow-root conclusion from that read. Both
halves re-derived rather than reworded. A shadow root no longer breaks the reads
(`elementInRoot` is a `querySelector` under the host, which a shadow root
answers); it breaks this sheet, because a rule in the document's head does not
cross a shadow boundary, so it would have to move inside each root and be parsed
once per host instead of once per page.
L2, `document-factory-artifacts.test.ts` anchored the page body at `):
TerminalDocument {` and nothing else, so the header, the `@ts-nocheck` line, the
`import type` and the parameter's own line could all drift with the test green —
and that signature is the one line a caller of the page's artifact reads. Both
wrappers are now literal lines: nine for the page (header, directive, import,
blank, the three-line signature) and one plus two for the native script
(declaration, closing brace, trailing call). Literal rather than the generator's
own constants, which would only agree with whatever it emits.
Red controls, each with the generator changed and then restored:
- the page's `import type` reordered to `{ TerminalDocumentHost, TerminalDocument }`:
2 failed ("the page module opens with its wrapper", and the on-disk reading).
- the native trailing call changed to `createTerminalDocument({});`: 1 failed
("the native script closes with its wrapper"). The old anchor caught neither.
No emitted line moved: golden and payload hash unchanged, terminal suite 68 files
/ 631 tests.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
|
||
|
|
a29d1cb107 |
test(mobile): unit-test the page mount's three paths no happy path reaches (OTA phase C, C7.5b round 1)
Round 1 M2. `terminal-web-document-mount.ts` had no unit test: the only reading of it was the render check, which drives the whole page bundle in a browser — right for behaviour, too coarse for three lines that only a failure reaches. The body's "five test files whose subjects no longer exist" is wrong for three of them. What ruling 22 deleted was the machinery (the claim token, `liveDocument`, the owner-checked dispose, the second-mount refusal); these three subjects survived it and lost their only cover: - both engines disposed when a swap never committed (`terminal-init.ts:203-213`); - the host given back when a start throws (`startDocumentOrGiveTheHostBack`); - the component naming that throw's cause (`TerminalWebView.web.tsx:83`). `terminal-web-document-mount.test.ts` (happy-dom) covers all three against the real generated factory. Only the factory's *arrival* is mocked, delegating to the real `createTerminalDocument` except for the one case that makes a start throw, so no stub stands in for the program under test. Six cases, each red against a deliberately broken line: - two distinct terminals both disposed. Broken `new Set([scope.term, scope.committedTerm])` -> `new Set([scope.term])`: expected [1,1], got [0,1]. - the same terminal disposed once. Broken the set -> a plain array: expected 1, got 2. The pair is the dedup's own oracle; either half alone passes for the wrong reason. - the host emptied and the class dropped on a throw. Broken by deleting the two lines in the mount's catch: host still carried `#terminal-container`. - control: a live document keeps the markup and the class, so the two assertions above cannot pass for a mount that planted nothing. - `onEngineError` gets `terminal document failed to start - engine missing`. Broken by deleting the component's `receive` in its catch: expected one message, got none. - control: nothing reported when the document starts. The engine double moves to `document-terminal-double.test-support.ts` and both readers of the seam share it; a second hand-written copy of thirty members would drift as the shape grows. It now counts disposals beside reporting `open`. Terminal suite 67 files / 625 tests -> 68 / 631. No product line changed, so the golden and the payload hash do not move. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
9824145e1f |
fix(mobile): filter a document's page-wide touch listeners to its own host (OTA phase C, C7.5b round 1)
Round 1 H1. The dispatcher's four listeners are on `document`, so with two documents on one page (legitimate since `712daa80e6`) each is handed the other's touches, and the two-finger branch acts before any target filtering: a pinch in host B posted `mobile-clip-cancel-by-pinch` from document A and dropped A's selection. Fixed at the source, one predicate beside `elementInRoot`, asked once at the top of each handler rather than inside a branch. `root === null` is the WebView, where the document is the page, so it answers yes to everything and the native document is unchanged. `e.target` is the element the finger went down on for the life of the touch, so a select-drag travelling outside the host still answers yes on move, end and cancel. Census of every global listener install under `src/terminal/document/` (non-test): - `tap-dispatch.ts:241-244`, four capture-phase `document` touch listeners (touchstart, touchmove, touchend, touchcancel): MUST be root-filtered; this fix. - `document-host-seams.ts:165-166`, `window`+`document` `message` in `installWindowHostTransport`: WebView-only. It is that host's transport seam default and the page installs nothing (ruling 24), so no page carries two. - `fit-scale.ts:163`, `window` `resize`: page-wide by nature. A viewport change concerns every document on the page and the event has no target in either host; both must refit. - `webgl-recovery.ts:104`, `document` `visibilitychange`: page-wide by nature. Backgrounding concerns every document on the page; its target is the document. - No document-level mouse, wheel, keyboard or selection listener exists: those are all on `targetSurface` or the menu buttons, read through `elementInRoot`, so they are already inside their own host. Red-first, the reviewer's own repro in `document-host-root.test.ts`: A and B both in select mode, a two-finger touchstart in B's surface. Before: 2 failed (A posted the pinch cancel too, and the control in A's own host cancelled B). After: 3 passed. The control keeps the assertion honest — the same touch inside the document's own host still cancels its selection. Golden and payload hash move (regen is a review event): six hunks, +22/-1. `eventTargetInRoot` emitted after `elementInRoot`; `touchIsThisDocuments` after the CAPTURE constants; the three-line guard at the top of each of the four handlers; `onDocumentTouchCancel()` becomes `onDocumentTouchCancel(e)`. Document 728,119 -> 728,589 bytes, sha256 5b65315b... -> 1556f532... Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
2524737ef0 |
chore(relay): apply the cell backend drain and request-logging settings inside each same-cap wave (#21860)
* chore(relay): target each cell's backend service from the same-cap job
The 60 s connection drain timeout merged in #21848 has no safe apply path.
A root plan scoped to the backend services alone still pulls every
`google_compute_instance_template.relay_gce_cell` in as a dependency, and
standing image drift turns all 29 into replacements, so applying it would roll
the fleet at once.
Add `google_compute_backend_service.relay_gce_cell["${TARGET_CELL_ID}"]` to
both plan invocations in the per-cell same-cap job, next to the template and
MIG it already targets, and teach the reviewed plan validator to allow exactly
one extra change: an in-place update of that one cell's backend whose only
changed attribute is `connection_draining_timeout_sec`, landing on the
constant `validate-relay-asia-topology-plan.mjs` exports. Any other attribute,
any other resource, or a backend for another cell still fails the validator.
The accepted update is reported as `connectionDrainUpdate` and kept out of
`changes`, so the apply step's stranded branch and the resume step's drift
branch keep reading the template-and-MIG count they were written against; the
resume branch additionally accepts a plan whose only pending change is that
drain update, which restarts nothing.
Claude-Session: relay-same-cap-targets-cell-backend
* fix(relay): also let the same-cap wave apply this cell's LB request logging
A read-only production plan for production-gce-c7 showed the live US cell
backends carry no `log_config` at all, while relay-gce-cells.tf has declared
`log_config { enable = true, sample_rate = var.relay_gce_cell_log_sample_rate }`
on every cell backend since the Terraform root landed in
|
||
|
|
41f34f6ff2 |
fix(terminal): let Shift+middle-click paste in mouse-tracking panes (#21858)
* fix(terminal): let Shift+middle-click paste in mouse-tracking panes Follow-up to #21834 (issue #21762). That fix arms the native-paste suppression window for every terminal middle-click, then returns early when the pane is in mouse-tracking mode so the TUI performs the paste from the forwarded mouse report. xterm's SelectionService.shouldForceSelection deliberately withholds that report for a shifted click (Option-click on Mac), so the TUI never pastes. With the native follow-up paste now suppressed as well, Shift+middle-click in Claude Code, Codex, and other tracking TUIs pasted nothing at all. Previously Chromium's native paste was the one paste. Mirror xterm's platform rule: when the click's modifier forces selection, fall through to Orca's own paste-to-PTY path (stop propagation, focus, paste) exactly as in a non-tracking pane. The auxclick handler gates stopPropagation the same way. * test(terminal): pin Alt+middle-click to the TUI-owned path off Mac --------- Co-authored-by: bench <bench@example.invalid> |
||
|
|
e0b717dd60 | test(antigravity): cover Kitty mode reattach metadata (#21810) | ||
|
|
8b6d8c2718 |
test(mobile): type the two-host engine double as the shape the seam returns
The double was reaching `createTerminal`'s return type through `as unknown as Parameters<typeof queueMicrotask>[0] & never`, which the type-aware gate reads correctly as an intersection with `never` and which was a cast standing in for naming the type. `TerminalDocumentTerminal` names it. Every member the type declares is present — the ones `init` and the start sequence reach do something, the rest answer in the shape their caller reads — and the shape needed no narrowing to accept a double. Two things the type does not declare moved off it: where `open` was called is handed back beside the terminal rather than exposed as a second getter, so the literal carries nothing excess, and the buffer gained the `getLine` the type requires. No cast, so nothing to write a SAFETY line about. Still red without the fix, checked again after the retype by pointing the emitted reader back at `document`: one case. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
76d87604ff |
fix(terminal): keep drag selection stable during redraws
Pause output-driven drift fitting while the user drags a terminal selection, then converge immediately on mouseup. Includes regression coverage and visual proof. |
||
|
|
f2d05ad9c8 |
Merge remote-tracking branch 'origin/main' into ota-c7-5b-document-factory
Main at
|
||
|
|
712daa80e6 |
fix(mobile): read a document's elements from the host it was planted in
The last thing two documents on one page shared. Ruling 22 gave each call its own scope, but the element reads were `document.getElementById` and the ids are in the markup every host plants, so the second document's start sequence took the first host's surface, overlay, handles and menu — two documents driving one terminal, with the second host left empty. Reachable, not theoretical: expo-router keeps the outgoing screen mounted for the length of a stack transition, so two routes that both hold a terminal have two live documents on the page while the animation runs. `root` joins the host argument and `elementInRoot` is the one reader; the ten reads in runtime-constants, surface-swap, selection-state-and-eviction and text-scaling go through it. No id is renamed and nothing is refused: two documents on one page are two terminals. Two deviations from the ruling, both about *when* the default is read. `root` is `ParentNode | null` with null meaning "the page I am in", rather than defaulting to `document`: a data default is evaluated whenever a scope is built, which put a DOM read into every slice evaluation and took eight keyboard-avoidance cases down with a `ReferenceError` in their `vm` context. Null defers it to the read, which is the rule the eight seams above it already follow. And the reader lives in `document-host-seams.ts`, which declares the type, taking the root as an argument: in `document-scope.ts` it was four lines over the file's 300 (no bump, no disable). Red first, and the red was the second document: with a page-wide read the second engine opens on an element outside its own host. `document-host-root.test.ts` plants two hosts, starts a document in each, and reads which surface each engine was opened on through the `createTerminal` seam, because the scope is not reachable from outside. Falsified again after the fix by pointing the emitted reader back at `document`: red, one case. Two neighbours checked while here. The document-level touch listeners are already host-scoped, because every handler tests its target against the scope's own surface, overlay and handles, which are now this document's. `window.__engineErrors` is the one page global left, and it is now kept rather than replaced per mount: a capped diagnostic buffer, where a second mount was costing the first its captured lines. Golden 27 diff lines, hash and length repinned: the reader, the `root: null` default, and the ten reads. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
8a06a62369 |
test(config): separate the two commits inside the closure reading
The factory arriving is not the whole -1,890. Making the document a factory put the `host` argument on `createTerminalDocumentScope`, which is this lane's only edit to a module the closure already carried, and that alone is +80. Both numbers are in the note now, so neither commit's cost is read as the other's. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
6855679687 |
test(mobile): stop the derived start sequence warning on every suite run
The helper reaches its neighbours through a variable specifier, which the bundler answers by rewriting as a glob — and it refuses to glob the directory the import is written in, so every suite that loads this file printed the refusal twice. `@vite-ignore` leaves the specifier alone and the module runner resolves it, which is what was already happening. An extension does not help: with one the refusal becomes the own-directory rule, and the path alias is not resolved for a runtime specifier at all. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
23b199b268 |
test(mobile): stop censusing state a call of the document already isolates
Ruling 22 answers what ruling 21's state half was for. A module's top level is emitted inside the factory, so a `let` there is one binding per call — which is exactly what moving it onto the scope was achieving. The census that refused it, and the planted-module precondition beside it, go. The effect half stays, and the distinction is what a stop can reach. An effect in a module body runs at the position its module is emitted rather than in the start sequence, so no stop function undoes it and each call leaks another one. A binding leaks nothing. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
89b6d5802d |
refactor(mobile): call the document instead of starting its modules
The page's half of ruling 23, and ruling 24. The mount plants the markup and calls the factory; the handle is `send` and a `dispose` that stops it. The document is a function, so the page holds an object per call and nothing else. Deleted with the singleton it was written for: `page-document-modules.ts`, the claim token and `liveDocument`, `release`, the owner-checked `dispose`, the second-mount refusal, `resetTerminalDocumentScope`, the `adopt` callback, `ready` and every pending-import path. All of it existed because two mounts shared one module-level scope and because the handle had to come back before its import did. A call is a document now, so a second mount cannot reach the first one's state and a caller's cleanup cannot arrive before there is something to clean up. The second-mount refusal is not replaced by a one-line guard: with a scope per call there is no shared state left to refuse for, and a host element with two documents planted in it is the caller's own doing, visible on the screen. Ruling 24 splits `message-bridge` by what it is, which is what made the page able to run this text at all. Two more seams, eight now: `installHostTransport`, whose window default installs the `message` listeners on window and document and hands back their removal, and `hasEngine`, whose default is the `window.Terminal` the engine bundle installs. The page answers a transport that installs nothing, because its transport is the handle, and an engine that is always there, because the engine is the import above. So the page no longer takes the shell's frames or reports a missing engine on every mount, and `stopMessageBridge` takes the listeners off — the WebView never removed them, which ruling 21 asks for. The refit the bridge happened to own moves to `fit-scale`, which is whose work it is; both hosts start it, and the mount's hand-copied five calls are gone. The engine's disposal moves into `stopTerminalInit` for the same reason: the mount cannot reach the scope any more, and a stopped document's terminal is a WebGL context nothing will read again. The start sequence the generator emits is now inside the document's own undo: a start that throws runs `stop` and rethrows, so neither host can be left holding a listener from a build that failed. That replaces the deleted entry module's unwind, and it covers every start rather than the four that had one. Readiness arrives the same way on both hosts. The document posts `web-ready` through `postToHost`, which the controller already handles, so the mount-side `confirmWebReady` is gone. That flush is also the one caller that reaches `post` before the effect has a handle, which is why the component's queue stays and now says so. The golden and the payload hash move, 102 diff lines: the two seam defaults and their state fields, the reset gone, `startFitScale`, the disposal, the bridge over its seams, and the start sequence inside its try. Tests: the seam tests and the unwind test move to the factory and the derived start sequence; the frame registry builds a fresh scope instead of resetting one; the two mount test files and the page entry's order test go with their subjects. The render check keeps every behavioural case and loses two whose subject the static import removed — a Reload while the chunk is in flight, and a chunk that will not load, which is now the route's chunk rather than the document's. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
3b055c869f |
fix(wsl): await Pi and OMP guest relay materialization (#21721)
* fix(wsl): await Pi and OMP guest relay materialization * fix(wsl): materialize Pi extension before guest launch * fix(wsl): keep relay state under lint limit * fix(wsl): preserve guest agent readiness across launches * test(wsl): expect guest status path translation * ci: rerun PR checks after rebase * ci: retrigger PR checks * ci: run final PR verification |
||
|
|
5b8ac36f41 |
chore(relay): add a report-only post-wave health gate to the same-cap cell job (#21849)
* feat(relay): report a post-wave health verdict on each same-cap cell, without gating on it After a same-cap cell finishes rolling, an operator reads five things by hand before dispatching the next cell: director 503s against the same clock hour a day and two days earlier, whether the cell's new container announced its listener and has stayed up, the cell's own pool pressure, the asia-east2 pool trio, and Cloud SQL FATALs. This runs those same reads automatically and records PASS / WARN / WOULD_BLOCK with its numbers, so its calls can be compared with the operator's over a full roll before it is ever allowed to stop one. It cannot fail a cell in this change. The script exits 0 on every verdict, and the step is continue-on-error, so even a crash stays off the job's outcome and the failure failsafe cannot fire on anything it observes. It also runs after the restore, so no cell waits on it to go back into admission. Cloud Logging returns only --limit entries and says nothing when it truncates, so every count is split into sub-windows of ten minutes and a sub-window that comes back at the limit is reported unverified rather than as a count. Windows are always explicitly bounded: --freshness does not bind on these logs. Claude-Session: https://claude.ai/session/ced32ebb-7155-4413-adad-1eccd14c2010 * fix(relay): bound the shadow gate's cell reads at the apply start and cap every read Four fixes from review, all in the report-only shadow health gate. The boot search opened at apply-completed-at, which is stamped after `terraform apply` and `wait-until --stable`. The new container announces its listener while the MIG is still converging, so that bound is already past the announcement it looks for and a healthy roll read as would-block. The job now stamps apply-started-at immediately before the apply, and the boot search opens there; apply-completed-at is kept, recorded rather than judged, so an operator comparing verdicts can see apply time next to boot time. The crash query started at the newest listener timestamp, which erased any crash before it. A crash-restart loop ends with an announcement that looks like a clean boot, so that is exactly the case it hid: against production, the 2026-09-20 c28 crash at 20:18:10 was dropped because the listener landed at 20:18:27. It now runs from the apply start, still scoped to the instance id the listener identified, and that crash is counted. A runtime-metrics read that came back at its 500-entry limit fed judgePool as though it were a complete sample run. A truncated run has holes and the consecutive-sample rule reads a hole as a recovery, so it now reports unverified. gcloud reads had no timeout. continue-on-error bounds the job's outcome but not its clock, so a stalled read could have spent the rollout's remaining minutes. Each read now gets 60 s and a timed-out read is just a failed read. Claude-Session: https://claude.ai/session/ced32ebb-7155-4413-adad-1eccd14c2010 * test(relay): require each shadow-gate stamp's presence before asserting its order The ordering assertion used indexOf, which answers -1 for an absent stamp, and -1 precedes every real offset. Deleting the apply-started-at line left the test green, so the census could not see the fix it was written to pin. Each stamp's presence is now asserted first, with a message naming the stamp and the step, and presence is judged inside the step that owns the stamp rather than anywhere in the file: a stamp written into a neighbouring step records the wrong instant but would satisfy a whole-file match. Control-run against a scratch copy of the job. Deleting drain-started-at, apply-started-at, or apply-completed-at each reds with its own message, and moving apply-started-at after terraform apply reds on the ordering assertion, so presence and order both fail independently. Claude-Session: https://claude.ai/session/ced32ebb-7155-4413-adad-1eccd14c2010 |
||
|
|
587000852f |
build(mobile): emit the page's terminal document factory beside the WebView's
Ruling 23, and the first half of C7.5b commit 2: the artifact the page will import. The page cannot run the native script, because building a function from a string needs `eval` and the page's policy refuses it, and it cannot run the modules either, because they are one singleton while the whole point of the factory is a scope per call. So one emitted body gets two wrappers. `buildTerminalDocumentFactoryBody` is now the shared half: the modules in order, the start sequence, the stop handle and the return. The native script wraps it in the declaration and the trailing call, exactly as before. The new `terminal-webview-document-factory.generated.ts` wraps the same lines in a `@ts-nocheck` module whose only other content is the type import and the annotated signature. One generator run writes both, so the page's factory cannot be a build behind the WebView's. `@ts-nocheck` covers this one generated file. Every line of its body is esbuild output from a module that was type-checked at its source, with `declare global` blocks and type re-exports already erased and constants already substituted; the one line a caller reads is the signature, and the generator writes it with its types. `TerminalDocument` joins `TerminalDocumentHost` in `document-host-seams.ts` as the shape the factory returns. The pin is byte equality. `document-factory-artifacts.test.ts` strips each wrapper and holds the remaining text equal, so the byte golden pins the page's artifact by construction rather than by a second golden; it also reads the file on disk against what the generator would write now, since that file is gitignored and built by postinstall, and it refuses a trailing call in the page's copy, which would start a document as the module was imported. The path joins `.gitignore` and the oxlint ignore list beside the engine artifact. The consumer census gains the generated file by name: it is the document, and its one import is the host contract its signature is written against. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
4f839cc8c9 |
chore(relay): cut the cell LB connection drain to 60 s and allow ten-cell same-cap batches (#21848)
* perf(relay): cut the cell LB drain to 60s and widen the same-cap batch to ten cells Two independent sources of relay roll wall clock, neither of which protects a host: 1. `connection_draining_timeout_sec` on the per-cell backend services was 300s. The same-cap job drains every host off the cell to a restart-safe condition before Terraform runs, so the LB drain only ever covers a host still mid-handshake. Measured 2026-09-16 over ten same-cap cell jobs, it sat as ~5m55s of dead time between `Apply complete` and the old VM powering off, inside an 8.5-minute `wait-until --stable` step. Now 60s, and pinned in the topology `check` block beside the other fixed-one invariants. 2. The same-cap wave capped a batch at four cells, so a 22-cell roll needed six batches, six single-use monitor gates, and a human handoff per batch. The wave workflow now declares cell_1..cell_10 with the identical serial shape and chaining, and the validator accepts two to ten. The shared wave-index rule (`relay-monitor-evidence.mjs` and the relay-ops preflight CLI) widens from 0-3 to 0-9 so the later cells can present the same evidence; each job workflow keeps its own narrower range, so the capacity wave stays at four. Cells remain strictly serial, one at a time behind the rollout lease, each with its own live preflight. Claude-Session: https://claude.ai/session/relay-roll-drain-timeout-and-batch-cap * fix(relay): align the Asia topology plan validator with the 60s cell drain `validate-relay-asia-topology-plan.mjs` rejected any Asia backend whose `connection_draining_timeout_sec` was not 300, and `cloud-deploy-relay-asia-topology.yml` targets `google_compute_backend_service.relay_gce_cell["<cell>"]` per cell. With the Terraform local at 60 that workflow would have failed its own plan review. The validator's two restated topology values are now named exports, and a new census test reads `relay-gce-cells.tf` and equates three statements of each: the `relay_gce_topology` local, the topology `check` assert that pins it, and the validator constant. Terraform cannot export a local to JS, so reading the source is the only way to stop them drifting; the test was confirmed to fail when the local alone is moved back to 300. Repo-wide grep finds no other pin of the drain value. Claude-Session: https://claude.ai/session/relay-roll-drain-timeout-and-batch-cap |
||
|
|
cb715898cd |
fix(release): pass the draft-verify tag on Windows pwsh (#21851)
The Windows matrix defaults to pwsh, so assert-github-release-is-draft.mjs received an empty argv and failed with "tag is required" after the signed installer was already uploaded. Force bash, interpolate the tag in YAML, and fall back to env TAG. |
||
|
|
eb902c5b9f |
Merge remote-tracking branch 'origin/main' into ota-c7-5b-document-factory
# Conflicts: # mobile/scripts/build-terminal-document-script.mjs # mobile/src/terminal/document/document-host-seams.ts # mobile/src/terminal/document/document-module-order.test.ts # mobile/src/terminal/document/document-scope.ts # mobile/src/terminal/document/generated-document-region.test-support.ts # mobile/src/terminal/terminal-document-golden.txt # mobile/src/terminal/terminal-webview-payload-hash.test.ts |
||
|
|
935fb0cd54 |
refactor(mobile): emit the terminal document as a factory
Ruling 22, commit 1 of C7.5b. The generator's concatenation already gave the 38
modules one function scope with one local `scope`; naming that scope a function is
what makes it the shape both hosts run, and what will let the page have its own
state per mount instead of a module singleton with a reset between them.
`createTerminalDocument(host)` is emitted around the same module bodies, in the
same order, followed by the same start sequence. It then declares `stop`, which
calls every module's stop in reverse order and takes back the frames the document
is still owed, and returns `{ send: handleMsg, stop }`. The native document is
that function plus one call with no argument, which is what the WebView has always
run: no argument means every seam is the window read it already did.
`createTerminalDocumentScope` takes the host and spreads the hooks it names over
the window defaults, filtering undefined so absent and present-but-undefined mean
the same thing. The emitted scope declaration is the one line the host reaches, so
the generator rewrites it and refuses if the line it expects is not there — a
rename would otherwise leave every call on the defaults with nothing to say so.
The golden moves by the wrapper and that one line, and by nothing else. 108,329 to
108,831 bytes, the whole diff:
-(function() {
+function createTerminalDocument(host) {
- function createTerminalDocumentScope() {
- return { ...createTerminalDocumentState(), ...createTerminalDocumentHostSeams() };
+ function createTerminalDocumentScope(host = {}) {
+ const named = Object.fromEntries(Object.entries(host).filter(([, hook]) => hook !== void 0));
+ return { ...createTerminalDocumentState(), ...createTerminalDocumentHostSeams(), ...named };
- const scope = createTerminalDocumentScope();
+ const scope = createTerminalDocumentScope(host);
-})();
+ function stop() {
+ stopSurfaceTouchGestures();
+ stopTapDispatch();
+ stopSelectionOverlay();
+ stopNormalBufferSmoothScroll();
+ stopHostNotify();
+ stopTerminalInit();
+ stopWebglRecovery();
+ stopFitScale();
+ stopViewportTransform();
+ cancelDocumentFrames();
+ }
+ return { send: handleMsg, stop: stop };
+}
+createTerminalDocument();
The byte golden and the payload hash are re-pinned once: 726,363 to 726,865 bytes,
sha256 9950f177 to c7bbcb0b.
Four test files sliced the document with their own copy of the IIFE bounds, which
ruling 17 allows moving. They now share one reader in the test-support module
beside the one that locates a single module, and that reader names the factory and
its call. Every assertion is unchanged. The module-order guard and the region
reader compare against the text the document carries rather than a raw emit, since
the scope module is the one the generator rewrites; both go through one exported
function so neither can describe the rewrite differently from the generator.
`TerminalDocumentHostSeams` and the new `TerminalDocumentHost` moved to
`document-host-seams.ts`, which owns the six functions they type. Types emit
nothing, so the golden is unchanged by the move; it keeps `document-scope.ts`
inside its 300-line cap with no disable and no bump.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
|
||
|
|
ec82173130 |
feat(mobile): mount the terminal document in the page over its own modules (OTA phase C, C7.5) (#21809)
* test(mobile): pin the terminal WebView document byte for byte The document is already pinned as a digest, which says whether the emitted bytes moved and nothing about where. C7.1 moves the hand-written script inside it into modules the web page can import and rebuilds the document from them, and the claim that has to hold through every one of those commits is that the native screen kept the document it had. A digest cannot be the instrument for that: it fails as two hexadecimal strings. So the document is also committed as itself. The fixture is generated by `scripts/build-terminal-document-fixture.mjs`, never pasted, and the test rebuilds the comparison through that script's own substitution rather than restating it, so a fixture written by one rule and read by another cannot agree with itself. The generated xterm engine is stored as two placeholders. It is already covered by the digest test, postinstall regenerates it from whatever xterm the lockfile holds, and inlining it would put 612 KiB of vendored bytes into the file whose job is to isolate hand-written changes. Two further cases keep that from becoming a hole: the placeholders must each appear exactly once and the engine must not appear at all, and the restored document must equal the real one. Regenerating the fixture is a review event. It is only correct when the emitted document was meant to change, and the diff in that commit is the evidence. Red-first: flipping one character inside a comment in `write-queue.ts` fails both identity cases with a one-line diff naming the comment, where the digest test reports a hash. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): compare two terminal documents as programs, not as bytes The C7.1 flip commit moves the document's 57 reassigned variables onto a scope object, because a variable assigned across ES modules is a syntax error, and every read and write of them gains a qualifier. The ruling asks that the review of that commit be a test rather than a 515-line read. This is that test's instrument. It cannot be a byte comparison. Once the script's source is modules, `oxfmt` owns its style, and the repository's style has no semicolons where the hand-written document has one on nearly every line. A byte diff would therefore be dominated by changes that are not the refactor, which is the opposite of what the reviewer needs. So the comparison is over tokens: semicolons are excluded for the same reason they moved, comments never reach the stream, and one difference is allowed — `name` becoming `<qualifier>.name`, three tokens for one — which it counts and reports. It is stricter than "it still runs": a reordered statement, a changed literal, a dropped operator, a renamed local and a qualifier under the wrong object name all diverge, each reported with the token index and both sides. Acorn carries `value` on its tokens but does not declare it, so the field is read through a narrowing check rather than asserted onto the declared type. Red-first, by mutation: dropping the qualifier-name check fails the case that names it; removing the leftover-token check fails the dropped- and added-statement cases; treating semicolons as significant fails the three cases that depend on ignoring them. The acceptance case runs on the real 2,758-line script rather than on a fixture, so the instrument is known to survive everything the document actually contains. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): count each normalisation the move makes, separately Measured while extracting the first group: the document's ES5 style is not a style this repository's own rules permit. `curly` braces 279 brace-less if/else/for/while bodies, `no-unused-vars` unbinds 38 catch clauses, and 446 `var` declarators become `const`, `let` or a scope field. Those rewrites land before the qualifier is considered at all, so "the qualifier and nothing else" was never reachable once the source is a linted module. The comparison now allows exactly four classes and counts each on its own: a reference that gained the qualifier, a declaration that moved onto the scope object, a `var` that only changed keyword, a body that gained braces, and a catch clause that lost its binding. Separate counters rather than a total, because the flip commit pins each number and a total would let one class absorb another — which is the drift the pin exists to catch. The two `var` classes partition the 446, and the qualifier's 641 sites partition into references that kept their declaration and declarations that moved. Two ordering facts the cases pin. The catch rule is tried before the brace rule, or the inserted-brace rule eats the `{` that follows `catch` and the streams never resynchronise. A body braced at the very end leaves its closing brace after the baseline has run out, so trailing closes are absorbed after the walk rather than reported as a length difference. Everything outside the four classes still refuses with the token index and both sides: a changed literal, a dropped operator, a reordered pair, a renamed local, a qualifier under another object's name, a brace opened and never closed, and a brace closed where none was opened. Red-first, by mutation: disabling the catch rule, disabling the trailing-brace absorption, folding scope-field declarations into plain references, and not counting brace insertions each fail exactly the case that covers them. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): make the mouse-report cell a module the page can import The first of the twelve groups the document already names. `*-injected.ts` has been splicing JS strings into the document for a while, and tests evaluate those strings, so the one-source-two-consumers shape is already there; what is missing is that a string cannot be imported by the web page, typechecked, or linted. This turns one of them into a module and adds the generator that puts it back into the document. The generator is a transform, not a bundle: a bundler orders its output by the dependency graph, and the document's order is part of what the equivalence test holds fixed. Imports are dropped rather than resolved, because inside the document every name is already in scope — that is what the single IIFE means — and `document-externals.ts` declares the names whose groups have not moved yet and emits nothing at all. esbuild prints an ESM module's exports as a trailing block, so that block is dropped whole rather than by its keyword; leaving the keyword behind would put a bare block statement in the document. Both sides of the comparison now go through that same printer before being read. Otherwise every choice the printer makes — semicolons, property shorthand, quote style — reads as a difference in the program when it is a difference in who typed it, and each would need its own rule. A script that does not parse is reported as a refusal naming its side, not thrown. `let` is contextual outside strict mode, so acorn reports it as a name and not as a keyword; without that the var-to-let rewrite the linter performs would be refused on every reassigned local. The group's counts are pinned exactly: nine references gained the qualifier (`term` seven times, `panX` and `panY` once each), nine locals became `const` or `let`, thirteen one-statement `if` bodies gained braces, no declaration moved onto the scope object and no catch clause lost a binding. The document is untouched, so the byte pin from |
||
|
|
21a8f7fc4e |
fix(mobile): drop what a disposed page mount adopted, and close the fixture's three resources apart
Round 7's five items. 1. The queued-dispose case's precondition was vacuous. It read the host for a missing container, which dispose empties on every path, so a build that returned straight after its ownership check satisfied it. The wrapper now counts resize adds and the case asserts exactly one, which is the document having started. Red under that mutation, on the count. 2. The render fixture's rollback awaited its cleanup unguarded, so a cleanup that also refused replaced the error the caller needs — the reason the setup failed. The rollback is best-effort now and the original error is what comes back. 3. That cleanup stopped at the first throw, so a browser refusing to close took the socket and the scratch tree with it, which is the leak the rollback exists to prevent. Each of the three is asked independently and the first failure is rethrown after all three have been tried. 4. The rejection case restores its `window` patch in a `finally`, as its sibling does, so a failure part way through no longer leaves the patched functions behind for everything that runs after it. 5. `dispose` left `started` set. `send` reads it, and what it holds names the page's one set of document modules, so a stale handle could route a host command into whichever document is live next. Nulled, and pinned: the stale handle pings, and with the old code the *live* mount's `receive` answers `pong`, because the scope's seam belongs to it by then. The precondition is the live handle's own ping being answered, so the silence is the stale handle declining rather than the command doing nothing. Items 2 and 3 have no pin of their own. Both are failure paths of the cleanup itself, reachable only by making a browser or a socket refuse to close, and standing something in front of Playwright to do it is what the anti-slop gate refuses in this file's suffix. The rollback's own pin still covers the path that matters, and both changes are read by it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
0f09a1f556 |
test(mobile): type the listener wrappers the mount pins install
The mobile tests-typecheck ratchet was red on
|
||
|
|
1b9d218df5 |
fix(release): force draft publishes on tag checkouts (#21842)
Build jobs check out the release tag, so electron-builder still used releaseType:release from older SHAs and published v1.4.206 as latest with only Linux assets. Override publish.releaseType=draft on the CLI (workflow YAML comes from main) and restore the draft helpers from the workflow ref. |
||
|
|
63eb8a40ae |
fix(mobile): hand the started document to the mount in the turn that started it
Round 6's two LOW items, and the pins for the owner-checked release.
LOW 1. `started` was assigned in the `.then` after the build, a microtask later
than the start sequence and the resize listener it installs. A dispose in that
window found nothing started, skipped the teardown and released the page with the
document still running on it. The build now takes an `adopt` callback and calls it
as its last statement, inside the guarded region, so whoever has to undo the
start is holding it before that turn ends. Pinned by queuing the dispose behind
the document import the build awaits, which lands in exactly that window: without
the change the started document's resize listener survives the dispose, five red
runs out of five.
The owner-checked release, which landed in
|
||
|
|
eb6068a434 |
fix(relay): stop a terminated checked-out PostgreSQL client from killing the cell (#21840)
pg-pool removes its own `error` listener when it hands a client out (pg-pool@3.14.0 index.js:344) and only reattaches it in `_release` (index.js:385). Between acquire and release the client therefore has no `error` listener, so when Cloud SQL terminates that session mid-statement the emit becomes an unhandled 'error' event and the process exits. `absorbPostgresIdleClientErrors` cannot see it: pg-pool routes to `pool.on('error')` only from the idle listener. Attach a per-checkout `error` listener in the one seam every relay checkout passes through, log a single warn line, and release the client with the error so pg-pool destroys it instead of pooling a dead connection. The listener is removed on release so it cannot accumulate. The in-flight query still rejects, so existing failure reporting and the transaction retry ladder are unchanged. Claude-Session: https://claude.ai/session/ced32ebb-7155-4413-adad-1eccd14c2010 |
||
|
|
8b37221b57 |
test(config): read the render fixture's rollback in a temp root of its own
Two defects in the case I committed in
|
||
|
|
cb1833e675 |
fix(config): give the render fixture's server and scratch tree back when it cannot start
CodeRabbit on the render fixture, plus its note on `release`. The fixture. `chromium.launch` is the last step of the setup and the one that fails in practice — no Chromium on the machine, an `ORCA_MOBILE_WEB_RENDER_BROWSER` pointing nowhere — and by then the bundle server is listening and the scratch tree is on disk. Rejecting there left the caller without a handle, so `afterAll` had nothing to close and both stayed allocated; the listening socket is the one that bites, because an open server handle keeps the vitest worker alive after its last test has reported. The setup after `mkdtemp` is now wrapped, gives back whatever it managed to take, and rethrows the original error rather than anything the cleanup raised. The normal close path awaits the server-close callback instead of firing it. `release` in the page mount. The ownership check covered the claim but not the two lines that make the terminal disappear, so a release that skipped the claim would still empty the host and drop its class. The check now guards the whole function, and round 5's caller-side check is gone as a duplicate of it: one rule, inside the thing it governs. Both existing callers are unchanged in behaviour — the synchronous planting catch always owns the page, and the rejection handler was already guarded. Pinned red first. The new case points the launch at an executable that is not there, then asks the port the fixture actually served on for a connection and reads the scratch directories in the temp dir. Without the rollback the port still accepts and the scratch tree is still there; with it, neither. The port is recorded by wrapping the real `createBundleServer` rather than standing a double in front of it, and the case asserts a server was created at all, or the refusal would mean nothing. Two oracles were discarded on the way. `rejects.toThrow()` with no argument passes for a build that broke for its own reason, so the rejection is matched by message. `process.getActiveResourcesInfo()` reports `TCPServerWrap`, not `TCPSERVERWRAP`, so a count filtered on the upper-case spelling was zero in both arms and agreed with everything; it also still lists the handle at the moment the close callback runs. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |