Repository navigation
Conversation
…an relay_invites (stablyai#21301) * perf(relay): index and gate the credential cleanup sweeps that seq-scan relay_invites The credential cleanup ran every 30s in all 23 cells as well as the director. Both of its relay_invites passes matched columns no index covered, so each one seq-scanned the whole table inside the maintenance transaction: 56 calls/min fleet-wide, 129ms and 63ms typical and 57s at the tail, to return about one row every nine minutes. Adds partial indexes matching each sweep predicate, gives the cleanup the same owner as the assignment sweep, and reaps terminal invites after seven days so the table stops growing for the life of the database. Every index carries the schema-deferrable marker: an operator builds them with CREATE INDEX CONCURRENTLY, and the catalog pre-check skips them from then on. * perf(relay): index live bases and reap settled connection authorizations relay_connection_bases is the dominant cost in the cleanup transaction: 195 ms of the 268 ms average, with ~5,800 shared buffer hits per call even though it already uses relay_connection_bases_active_deadline. That index spans all 6.65M rows, and only a few hundred are ever active. Adds a partial index on the live rows alone, and reaps settled rows from relay_connection_bases and relay_direct_authorizations once their deadline is more than a day past. Both readers of either table require the row active/unconsumed and inside its deadline, and every deadline is set at most 30s past insert, so a settled row can never authorize anything again. The composite index stays: it is the only one covering active = 0, and it is what lets the drained reaper learn there is nothing to do from the index rather than the 1.5 GB heap. Measured at 200k rows, 5 buffers with it and 1,274 without. * test(relay): accept either bases index in the sweep plan assertion The negative assertion pinned a planner choice rather than the invariant: either index keeps the sweep off the 1.5 GB heap, and which one wins on cost is not something the test should fix. Matches how the same file already handles the two invite sweep indexes. Also names the column the authorization reaper actually measures, which is consumed_at rather than deadline.
…y enums where tsc looks (stablyai#21298) * fix(mobile): move the last six reply-enum pins where tsc looks mobile/tsconfig.json excludes *.test.ts, so a `Record<HostUnion, true>` coverage record in a schema test is never typechecked: the two that existed (SshConnectionStatus, GitHubProjectOwnerType) checked nothing, and the four closed enums beside them had only a doc citation of the host type. Each arm list moves into its schema module as hostUnionArms<Union>(), which stablyai#21269 introduced for the same reason, and each test iterates the exported list instead of holding its own copy: - SSH_CONNECTION_STATUS to SshConnectionStatus - PROJECT_OWNER_TYPE to GitHubProjectOwnerType - DETAIL_FILE_STATUS to GitHubPRFile['status'] - PUSH_TEST_REFUSAL_REASONS and PUSH_REGISTER_REFUSAL_REASONS to the refusal arms of MobilePushTestResult and MobilePushRegisterResult - SETUP_RUN_POLICIES to SetupRunPolicy openEnum's parameter widens from a non-empty tuple to `readonly string[]` so a hostUnionArms list can feed it. z.enum already accepts the same, so the tuple constraint only excluded callers zod itself takes; behaviour unchanged. Twelve mutations prove the pins: dropping one arm and adding a bogus one each fail mobile tsc in all six places. Zero goldens move, the schemas' behaviour being unchanged, and the 21 recording suites pass at the existing baseline. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): fix the type errors in eighteen test files Found by typechecking the tests for the first time (see the config that follows). All mechanical, none weakens a product type: - 67 `act(() => vi.advanceTimersByTime(...))` callbacks return VitestUtils where act wants void, so each becomes a block. The async ones await only a genuinely promise-returning call, so no extra microtask tick is introduced. - Four fixtures were stale against a product type that gained a required member: MobileViewState.alwaysShowDefaultBranch, PrSidebarData.checksError, the branch-compare summary's errorMessage, and SessionOptionDescriptor's transport, which stablyai#20884 added precisely so a producer could not inherit the wrong lane's rendering by omission. - `getLastConnectedAt` on the shared relay fake was typed `() => null`, which refused the timestamp two escalation suites assign to it. - Two holders used before assignment take `!`, one `advance!.kind === ...` becomes `advance?.kind`, one widened status arm takes `as const`, and the Expo notification fixture keeps `data` required because the dismissal cases assign through it. 631 test files pass, 6222 tests, unchanged. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): typecheck the test files, on a ratchet mobile/tsconfig.json excludes *.test.ts so Metro never compiles tests into the release bundle, and vitest transpiles without checking types. Nothing had ever typechecked a mobile test, which is why a `Record<HostUnion, true>` pin written in one proved nothing and why 144 of the 630 test files had drifted. tsconfig.test.json is that program with the tests put back, behind `typecheck:tests`. Four files stay out: they import the desktop main process or src/shared/child-process, which are written against @types/node, and this program's libs are React Native's, where setTimeout answers a number rather than a NodeJS.Timeout. Pulling that graph in reports ~280 errors about the desktop rather than about mobile; vitest runs those four under Node, which is where they belong. The CI gate is a ratchet rather than the raw typecheck, modelled on check-ts-nocheck-ratchet.mjs: 126 files still fail, so the gate freezes that set and fails when a file that checks today stops checking, or when a baseline entry starts checking and was not pruned. The list may only shrink. Why not zero: 180 of the remaining 510 errors are one seam — tests locate mocked react-native components by string name, which `ElementType` does not admit — and closing it means either 180 casts or a global JSX declaration for the mocked names. That is a design decision, not a mechanical fix, so it is left for a follow-up rather than made here. The rest are smaller clusters of the same kind: vi.fn mocks assigned into typed slots, call-arg tuple indexing, and createElement props fixtures. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs(mobile-recorder): correct the corpus counts and the salvage claim The oracle section still quoted the corpus as 368 scenarios and 727 goldens; it is 393 and 778, and the three replay suites report 781 tests. Each number now names the command that measures it. "No golden carries one" was the load-bearing error: 44 goldens carry a recorded `reply-salvage` today, starting with the push-test unknown-reason scenario stablyai#21176 added for exactly that purpose. The paragraph claimed the observation pins an absence when on those families it pins a recorded drop. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): pin the tests-typecheck ratchet's parser The gate reads tsc's output, and tsc indents the "Overload 1 of 2, ..." detail under an error. Counting those as filenames would write unparseable entries into the baseline and leave the gate unprunable, so the parser is pinned on that shape as well as on the added/stale diff. Written against the gate itself: it flagged this file before the directive it carried was removed, which is the end-to-end proof the spawn half works. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): await the timer advances the act() rewrite dropped Rewriting `await act(async () => vi.advanceTimersByTimeAsync(n))` into a braced body left the returned promise floating at 27 sites, so the advance was no longer ordered before the assertions that follow it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): unshadow MobileHostCard's .tsx suite A wildcard `include` keeps only the higher-priority extension, so MobileHostCard.test.tsx sat outside every tsc program while MobileHostCard.test.ts existed beside it. Its one error is the same react-test-renderer seam its sibling is baselined for. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): census every test file into the typecheck program The ratchet diffs only files that error, so a test excluded from tsconfig.test.json or shadowed by a sibling extension left the gate silently. Every *.test.ts(x) on disk must now be in the program or named in TESTS_OUTSIDE_PROGRAM with its reason. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(shared): make the enum helpers refuse the ways they can prove nothing openEnum takes a `const` T so a bare literal keeps its arms rather than widening to string. hostUnionArms blocks inference of U with NoInfer and defaults it to never, so a call that omits the host union — where the record would only pin itself — no longer compiles. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs(mobile): describe the census and correct the baseline count Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): give the push fixture cast its SAFETY rationale Widening the pre-existing cast made the changed-code gate attribute it as a new finding. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): build the push fixtures as typed notifications Replaces the `as unknown as` cast with Expo's own types, filling FirebaseRemoteMessage and its notification once in two builders, and passes the data payload in rather than mutating through an optional member. Typing the fixture showed one assertion comparing the scheduled content against the whole arriving content, which only held while the cast let the fixture omit the two members the presenter drops; it now names the four members the presenter forwards. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): keep the grouped-question advance read non-optional `advance?.kind` let an absent advance take the null-draft branch instead of failing. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): run the tests-typecheck ratchet on Windows Spawns tsc's JS entry on this Node instead of the node_modules/.bin shim, which is a POSIX shell script that Windows resolves to tsc.CMD and then appends .exe to. Parsed paths are normalised to POSIX so a Windows run does not read every baseline entry as both stale and added. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): close the ratchet's @ts-nocheck hole and read tsc once tsc exits 0 on a @ts-nocheck file, so a baselined test could be "fixed" with one line, pruned, and never checked again; the census now names any program test file whose leading comment carries the directive. `--noEmit --listFiles` answers both questions in one pass, so the gate spawns tsc once rather than twice. Corrects the two stale counts, and states hostUnionArms' real reason for living in the schema module now that tests are typechecked. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
…r picks (stablyai#21305) Added in stablyai#21301 on the reasoning that the composite (active, deadline) index spans all 6.65M rows to find a few hundred live ones. Measured post-merge against production-shaped history, that reasoning does not hold: a basis is inserted active and flipped to 0, so the partial index accumulates one dead entry per deactivation exactly as the composite one does. Scan buffers are identical to composite-only in every state, 11,099 cold, 2,522 warm, 9 after VACUUM, and the planner picks the composite index throughout. The extra index costs ~65 bytes of WAL per basis insert, about 22% more. The relief is the reaper plus vacuum, which stablyai#21301 already ships. Removes the statement from SCHEMA, restores the plan test to pinning the composite index by name, and records why a narrower index is not the cure so the next reader does not re-derive it.
…te transaction per host (stablyai#21303) * perf(relay): batch control lease renewals per cell instead of one write transaction per host Every connected desktop renewed its own control lease with its own single-row write transaction every 30s. At ~14,000 hosts that is ~470 write transactions per second fleet-wide, each with its own transaction id, all updating the same few heap pages of relay_assignments and relay_assignment_activity_leases. Sampling three onsets at 250ms showed no lock queue and no slow statement: 60-144 backends piled into LWLock:BufferContent and Timeout/SpinDelay inside that one statement, and Query Insights attributed 152 of 157 seconds of lightweight-lock wait in the onset minute to it. The heartbeat now enqueues a due renewal instead of issuing it. A cell flushes its queue once per second, or as soon as 500 rows are waiting, through one statement that unnests the parameter arrays and applies the same CTE row-wise. Concurrent writers drop from the host count to the cell count, and transaction ids with them. Measured against a 20,000-row table: 1 row 4.1ms, 12 rows 3.3ms, 100 rows 6.6ms, 500 rows 22.7ms. Per-session semantics are unchanged. Each enqueue still resolves on a renewal and rejects with the outcome as its message, so the completed-attempt counter, the staleness guard, and every close path route exactly as before, and one outcome per row is recorded against the flush latency. Lock order is (user_id, relay_host_id), the primary key of relay_assignments, applied in JavaScript and repeated as the statement's ORDER BY. EXPLAIN confirms LockRows sits above that Sort, so a batch acquires its assignment rows in one global order. Every writer in the store locks a host's assignment row before its migration or lease rows and only ever touches one host, so a batch can only wait on a row a single-host writer holds, never the reverse. One statement also means one contended row could fail the whole batch, so a failed batch degrades to the per-host statements it replaced rather than costing every other host on the cell its renewal. * perf(relay): batch control lease renewals per cell instead of one write transaction per host Every connected desktop renewed its own control lease with its own single-row write transaction every 30s. At ~14,000 hosts that is ~470 write transactions per second fleet-wide, each with its own transaction id, all updating the same few heap pages of relay_assignments and relay_assignment_activity_leases. Sampling three onsets at 250ms showed no lock queue and no slow statement: 60-144 backends piled into LWLock:BufferContent and Timeout/SpinDelay inside that one statement, and Query Insights attributed 152 of 157 seconds of lightweight-lock wait in the onset minute to it. The heartbeat now enqueues a due renewal instead of issuing it. A cell flushes its queue every second, or as soon as 200 rows are waiting, through one statement that unnests the parameter arrays and applies the same CTE row-wise. Concurrent writers drop from the host count to the cell count, and transaction ids with them. Per-host buffer traffic is unchanged: 30 hits for one row, 25.5 per host at 12 rows, 30.1 per host at 200, against the 28.7 the single-row statement reports in production. Per-session semantics are unchanged. Each enqueue still resolves on a renewal and rejects with the outcome as its message, so the completed-attempt counter, the staleness guard, and every close path route as before, and one outcome per row is recorded against the flush latency. Row locks live until the statement commits, so a batch that waited on a contended row would hold every other row's lock for that whole wait. The assignment pass therefore takes its locks with SKIP LOCKED and reports a contended host as assignment_lock_unavailable, which the registry retries on the next tick instead of closing the control. That keeps the hold to the statement's own execution: 11.5ms for 200 rows against a 20,000-row table, and 9.4ms with a host wedged in a per-host transaction, where a blocking FOR UPDATE spends the pool's whole 1s lock_timeout and then fails every row in the flush. An unlocked present_assignment probe separates a host with no assignment row from one the skip passed over, so a skipped row can never be mistaken for a missing assignment and close a live desktop. With no wait on the assignment pass the lock order is only needed for the two later passes, and it holds: every writer takes a host's assignment row before that host's lease rows, and a host whose assignment row is held was skipped, so the batch never reaches its lease. markMigrationTargetRegistered is the one writer that locks a migration row first, and it takes no further locks. * fix(relay): answer every row of a control-renewal batch from its own lease update Review findings on the batched renewal. Two control leases on one host in one batch made the second report control_activity_not_found although both were renewed: the assignment UPDATE is offered the same target row twice, applies one source row and returns one, so the other row_index never came back. The verdict now reads renewed_lease, which has a row per input row, and the assignment UPDATE groups per host so it also stops taking an arbitrary one of the two expiries instead of the later one. The queue now partitions per (userId, relayHostId) rather than per activity, so a second control activity for one host opens the next flush instead of sharing this one. Belt to the statement fix, not a substitute: the store API has to be right for the rows it accepts. renewControlActivities recorded no outcome for a one-row flush that threw, and none at all when every row failed validation, where a mixed batch recorded its invalid_* rows. Both now record in a finally, the way the single-row path's finally always did, and the error-to-outcome mapping both paths share is one function. The four flush fields the runtime metrics event emits had no log-based metric, so add them next to the existing controlRenewalLatencyMs* entries. Applying the Terraform is a separate manual step. controlRenewalLatencyMsP50/P95/Max now measure a batched row's flush duration rather than its own statement latency. Left named as they are for history, with a line at the emit site recording that the meaning changed here.
…1155) * feat(agent-launch): deliver a launch prompt from the host `agent.launch` created the surface and then reported the caller's text as `not-delivered`, always: delivery lived in the renderer, so mobile and any other caller got an agent and no prompt. The host now commits a `submit` prompt to the structured session it just created, through the same send path `agentSession.send` runs, and reports `journaled` with the transcript row's id. Nothing is queued — the durable record that the text is owed is the journal's own submission row, which the send appends before dispatching, so a host-side copy could only disagree with it. The outbox's entry and envelope builders are reused so this send is shaped exactly like a client's, fingerprint included. Everything else under-claims as `not-delivered`: a terminal's paste is observed by whoever owns the pane, a `draft` has no host-side home, and a refused or thrown send commits nothing. There is no fourth "maybe" arm — a caller holding one could neither resend nor drop the text — and dispatch doubt stays on the submission row where it already lives. * fix(agent-launch): recover committed prompt after send errors
…ap waves (stablyai#21307) c17 and c18 hold no hosts and sit outside general admission, so rolling one displaces nobody. They are the only zero-displacement canary for a new cell image, but the same-cap wave refused them at the dispatch validator and would have promoted them to general at the end if it had not. Add them to the approved list and teach the wave a cell's entry admission class: the precheck demands the class the cell is declared to serve in, the restore hands it back that class, the isolate on an already-isolated cell is asserted to change nothing, and the selector generation advances by 2 for a general cell and by 0 for a migration-only one. One wave may not mix the two, because every cell after the first offsets from a single per-wave delta. Neither cell is a declared regional-rehome source, so its template carries no rehome trust lines. The source-membership guard now fires exactly when a roll expects those lines instead of for every US cell, which is the invariant it was standing in for, and which limits c17 and c18 to rehome protocol 0.
…batch authority (stablyai#21313) A batch-apply wave verified only that the sealed canary named some approved same-cap cell, so a canary rolled on the migration-only, zero-host, 600-cap c17 or c18 was accepted as authority for a general 1000/3000-cap batch. The verify step now hands the batch's own cells to the check, which requires the sealed cell's entry admission to equal the batch's class.
…tablyai#21311) `readProjectedConflictOperation` and `readProjectedCompareStatus` constructed a `z.enum` on every call, so every `git.status` and `git.branchCompare` reply paid the constructor. Hoisted to module constants; the git-status payload schema reuses the same instance. Behaviour is unchanged: same arms, same fallbacks, identical reader output on all eleven recorded matrix cases. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
…e can roll (stablyai#21314) c17's canary-apply failed closed at plan validation. Its instance template is from 2026-08-07 and predates the ORCA_RELAY_CAPACITY_SERVICE_ACCOUNT line that every cell rolled since already carries, so the plan legitimately added it. The same-cap validator holds the whole startup script identical before and after except the image, and that line is not one it excluded, so the wave stopped with nothing applied. Pin the line for same-cap-cell exactly as bootstrap-cell already does, and exclude it from the before/after comparison. The cell may gain it; the pin is what refuses a roll that drops it or rewrites it to another identity. Both plan validations in the job now pass the capacity identity the job already requires. The same-cap contract is otherwise unchanged: any other stale line still fails closed, and needs a convergence apply before the cell can roll.
…hosts (stablyai#21315) c17's canary stopped at the pre-apply predecessor check with `runtime predecessor mismatch fields=draining`. The flag is residue: the previous canary (run 35290908836) drained c17 at 00:26:01, its terraform apply then failed, and the failsafe re-isolates without restarting the VM, so nothing cleared it. The same run had passed this very check a second earlier, which is what proves a parked cell is not draining at rest. Draining means connections are being shed, and a migration-only cell holds none, so the flag is not a precondition there. Accept it on entry for that class only. The replacement VM is still required not to be draining, on every path, and the incarnation check still proves it was replaced. Both predecessor checks now read one decision instead of computing the rule twice, so the assertion and its diagnostic cannot disagree. Every general-cell and rollback path keeps the value it had; a census test runs the real block over all eight mode and class combinations to hold that.
…#21321) * fix(relay-ops): roll a cell a wave stranded after its drain A wave that stops any time after its drain leaves the cell migration-only and draining on the rollback image, and nothing clears it: the drain flag is a one-way latch on the running process, and the failsafe restarts nothing. Both recovery modes then refuse the cell. Apply wants it general and not draining. Rollback sees the rollback image, reads it as a resume, refuses the draining, and would not have restarted it anyway. The image alone cannot separate a rollback that failed after its template apply from a wave that stopped before one. The restart can: the first left a fresh process, the second did not. Classify on that, so the cell that never restarted takes the rolling path instead of the resuming one. Its template still carries the image it serves, so that is the predecessor its plan is reviewed against, and a template already moved on to the target is refused rather than rolled backwards under a stale review. When the reviewed template is already in place the plan changes nothing, so the MIG is rolled explicitly on the same replacement policy a template change uses; the existing incarnation check is what proves the instance came back. Every other combination of mode, live image, and drain flag keeps the value it had, held by a census that runs the real block over all nine. * fix(relay-ops): pin the replacement method on the explicit MIG roll gcloud persists every rolling-action bound into the group's update policy, and it defaults the replacement method to substitute on a group with no stateful config. Passing surge and unavailable without the method would patch the policy off the declared RECREATE, and the next targeted plan would then carry a MIG change outside version.0.instance_template, which the plan validator refuses. Pass all three so the patch is identical to the declared policy, and read the declared values in the census instead of restating two of them. Dropping the flag, or moving any of the three in Terraform, now fails the census.
…rved mobile web bundle (OTA phase A, 1/5) (stablyai#21325) * feat(mobile-web-bundle): add the manifest contract and content-addressed build id The schema every later Phase A lane parses against: the ceilings that bound host memory (256 assets, 32 MiB total, 10 MiB per asset), and a build id that is a pure function of content so a client can use it as a cache key unconditionally. The serializer sorts its input rather than trusting the caller, so a producer that emits assets in any order still lands on the same id. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile-web-bundle): add the bundle RPC payload contract Method names, capability name, the 48 KiB chunk size, params/result schemas for both methods, and the six error codes as a closed union pinned by a coverage record. Constants and data only; the host wiring and the capability push land in later lanes. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile-web-bundle): hash the build id without node:crypto Metro ships no Node core shims, so a value import from these modules would fail to bundle on the phone. The pure-JS sha256 keeps both contract modules runtime-neutral, which also lets a cached manifest be re-verified on device. Verified digest parity against node:crypto across the 55/56/64-byte padding boundaries before the swap. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile-web-bundle): reject a manifest whose buildId is not its content hash A stale id passed every other check and would then serve the wrong bytes under a cache key the client already trusts. Runs last of the invariants because it is the only one that hashes. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile-web-bundle): require a lowercase content type The pattern carried an `i` flag over lowercase character classes, so the same bytes described as `Text/HTML` and `text/html` produced two different build ids. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile-web-bundle): move the capability name to a zod-free module A4 wires this constant into protocol-version.ts, which the phone reads on the capability path. Leaving it in the schema module would have dragged zod along with it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile-web-bundle): name the chunk reply's length assetByteLength It is the whole asset's length, not the chunk's, and sitting beside dataBase64 under the old name it read as the chunk's. Both are non-negative integers, so a producer that emitted the wrong one would only surface at the final hash check. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile-web-bundle): reject asset paths that are not portable or that fold together Two paths differing only in case are one file on macOS and Windows, so the host would serve the same bytes under two entries and one of the two hashes could never match. Windows-reserved segment names and trailing dots cannot be written to the bundle root at all. Both follow skill-package-manifest's checks, the folded-path Set and the reserved-segment pattern. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile-web-bundle): accept one spelling of a parameterised content type The optional space in `; ?charset=` let the same bytes carry two content types and therefore two build ids. Pinned to the single-space form the bundle builder emits. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * perf(mobile-web-bundle): stop hashing a manifest a cheaper invariant already rejected zod runs superRefine even after the asset-array ceiling has failed, so a 257 asset manifest was still sorted and hashed. Each invariant now returns on its own issue and the count is checked first, which is what the comment claimed. The tests read the issue paths: an oversized or otherwise invalid manifest with a deliberately wrong buildId reports no buildId issue, while the same wrong id inside the ceiling does. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs(mobile-web-bundle): say that the manifest has no additive path `.strict()` plus a literal schemaVersion closes the shape completely, so the version bump is the only way to change it. The phone value-imports this schema, so Phase B must read an unrecognised schemaVersion as a bundle to re-fetch rather than as a parse crash. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
…ry desktop release (OTA phase A, 2/5) (stablyai#21326) * feat(mobile-web): add the Phase A bootstrap web source A peer of src/ so the root workspace owns it and mobile's separate lockfile stays out of packaging. Four assets across four content types, enough to exercise multi-asset manifest handling rather than assume it. The page reads buildId from manifest.json at runtime: buildId hashes the asset list that index.html belongs to, so injecting it into a hashed asset would make that asset's hash depend on itself. Registered as a fourth typecheck project; without it the entry would be the only TypeScript in a release path that tsc never sees. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(build): build and verify the mobile web bundle from the root workspace Root esbuild over mobile-web/ into out/mobile-web/, content-addressed as assets/<sha256>.<ext> with index.html the only stable name. buildId is the sha256 of the canonical serialization of the sorted asset list, so it is a pure function of content and usable as a cache key with no further reasoning. The verifier builds twice into scratch dirs and compares: a timestamp, an absolute path, or an unstable ordering fails the build when someone introduces it, not the first time a phone gets a spurious cache miss. It also enforces the Phase A budget of 16 assets and 256 KiB, separate from the permanent contract ceiling. build:release does not call build:desktop, so build:mobile-web is wired into build:desktop, build:release, and build:release:parallel. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(packaging): fail the release when the mobile web bundle is missing or stale electron-builder only warns about a missing input, so without a beforePack guard a release ships an app that advertises the bundle capability and then errors on every request. The hash check, not the existence check, is what catches a half-written or stale out/. The source tree is excluded from app.asar; out/mobile-web ships inside it under the existing out rules, exactly as out/web does. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile-web): narrow the manifest with `in` instead of a cast The changed-code casting gate rejects assertions, and `in` narrows the same untrusted JSON without one. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile-web): move the bundle source under src/ so the root guard passes .github/scripts/check-root-directory-entries.mjs blocks any new top-level entry by name, so mobile-web/ could not live at the root. The source is excluded from app.asar by the existing '!src{,/**/*}' rule; the explicit '!src/mobile-web{,/**/*}' entry stays as a marker. out/mobile-web is unaffected and still ships under the out rules like out/web. No tsconfig includes src/**, so node, web, cli, and relay do not pick the tree up; it is registered as a knip entry so audit:dead-code does not call it unused. buildId is unchanged at 9d78435e: the builder hashes content, not paths. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(build): resolve the entry-script guard through pathToFileURL `file://${process.argv[1]}` never equals import.meta.url on Windows, where that url is file:///C:/... So the builder exited 0 having written nothing and the Windows packaging job failed later, at the guard, with no clue why. Every other script in config/scripts already uses pathToFileURL; this one now does too, via an exported predicate a posix runner can exercise with a win32 path. The verify script had no entry guard at all, so importing its budget constants ran the whole verification — including its process.exit — inside the test worker. It is now a function behind the same guard. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(ci): build the mobile web bundle in the PR package job That job assembles packaging inputs step by step instead of calling build:release, so the new beforePack guard hard-failed it. The census test added here is the oracle: it walks every workflow job that invokes electron-builder without --prepackaged (which short-circuits doPack before beforePack) and requires a bundle-producing script in the same job. It goes red on exactly pr.yml's package job when this step is removed. Ten jobs covered; the other nine already ran build:release, build:release:parallel, or build:desktop. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile-web): pin source line endings, because CRLF changes the buildId Every text byte under src/mobile-web is hashed into an asset digest and from there into buildId, so a CRLF checkout produces a different bundle id for the same commit: 91af2897 instead of 9d78435e. That would make a Windows-built desktop disagree with a mac-built one about which bundle a phone has cached. .gitattributes pins eol=lf for the text sources and -text for the PNG, matching the four trees already pinned for byte-hashing. The verify script asserts no source file carries a CR, so the build fails if the pin ever stops applying rather than silently shipping a second bundle identity. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * style(build): read the test's own path from import.meta.filename oxlint unicorn/prefer-import-meta-properties. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(test): census packaging jobs over raw workflow text, not re-serialized YAML yaml.stringify folds long lines, and in dev-channel-win-build.yml's build-win the fold landed between `electron-builder` and `--config`, so a real packaging job was invisible to the census: 11 jobs exist, the test saw 10. Slice each job's raw source by its parsed boundaries instead, and pin the inventory so a new packaging workflow has to be added here on purpose. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(build): assert the script chain the packaging census trusts The census only checks that a packaging job invokes one of ten build scripts; that those scripts still reach build:mobile-web was asserted nowhere, so a dropped link would leave every job looking covered while packaging failed at beforePack. Resolve each script for real, and pin pr.yml's hand-rolled step, since that job never calls build:release. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(build): realpath the entry path before the direct-invocation compare Node resolves symlinks in import.meta.url but not in argv[1], so `node /tmp/...` against a /private/tmp realpath compared two different strings: the builder and the verifier exited 0 having written and checked nothing. Same silent-success shape as the Windows file:// bug, so the fix sits next to it, with both seams injectable. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * style(mobile-web): format bootstrap.css with oxfmt It was the only tracked CSS failing oxfmt --check. The buildId is unchanged at 9d78435e8bb73c3341f833c20aaefbd7bfdfc414b68dadf87c1689d86728fe33, because esbuild's CSS minifier normalises the whitespace this touches before the asset is hashed. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(packaging): reject bundle files the manifest does not list The guard only walked the manifest, so a dropped assets/stale.js passed: assets are content-addressed, nothing ever overwrites a stale copy, and it would ship inside asar unreachable and unverified. Require every file under out/mobile-web to be the manifest or a listed asset. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(packaging): give beforePack an explicit mobile web bundle root The bundle guard read the repo's out/mobile-web unconditionally, so the two arch-aware packaging tests that call the real beforePack went red in the unit-test job, which never runs build:mobile-web. beforePack now takes the bundle root as a second parameter defaulting to out/mobile-web, which is what electron-builder gets, and those tests build a real bundle into a temp dir instead. The guard is neither skipped nor made tolerant of a missing bundle. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(packaging): census sees script-wrapped packers; dev verify reuses the guard The workflow census only matched a literal `electron-builder --config` line, so daemon-relocation-spike's `pnpm run build:unpack` (which packs and runs beforePack) was invisible to it. Jobs now count when any `pnpm run <script>` they invoke chains to electron-builder without --prepackaged; the spike joins the pinned list (12 jobs). verify-mobile-web-bundle.mjs re-implemented a weaker subset of the packaging guard (no safe-path check, no buildId recompute). It now calls assertMobileWebBundleBuilt, so a manifest edited after the build fails at `pnpm build:mobile-web` exactly as at beforePack. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix: make worktree scan failures actionable * fix: preserve remote worktree scan diagnostics
…he silence bound (STA-7672) (stablyai#21076) * fix(relay): probe a half-open control socket instead of waiting out the silence bound (STA-7672) A socket CLOSE rejects a pending request as relay_control_closed_<code>, so a relay_control_request_timeout is positive proof the socket stayed open and simply never replied. The only thing that reaps such a socket is RELAY_CONTROL_SILENCE_LIMIT_MS = 75_000, combed every 15s, against a 10s request deadline. On Windows behind NAT/VPN or across sleep-resume a half-open TCP socket accepts send() into a dead pipe and stays invisible for 75-90s, so every pairing attempt in that window times out. The reporter burned ~7. A request that times out with no inbound frame since its send now arms an RFC 6455 ping probe. Terminating on the timeout alone was rejected: relay control ops run DB transactions that can outlive the deadline, and the existing comment in handleMessage records that self-closing on a late reply was strictly worse than ignoring it -- it orphaned the relay session and answered the phone with HOST_OFFLINE for minutes. The probe distinguishes the two cases instead of guessing. The probe deadline deliberately exceeds the relay's own 15s application-level ping cadence. Relay liveness never depended on RFC 6455 control frames surviving end to end, so a shorter window would let a middlebox that swallows pongs turn every request timeout into a reconnect loop. At 20s a healthy cell clears the probe either way -- with a pong, or with the ping it was going to send anyway -- so a probe that fires means the pipe carried neither. Detection drops from 75-90s to ~30s. A pong clears a probe but deliberately does not feed the silence watchdog: it proves the pipe, not that the relay still indexes the session. The timeout error also stops being a bare string; it now names the request kind, the cell, the socket age, the time since the last inbound frame, and whether a probe was armed. The silence watchdog, the probe and the socket age now live in one RelayControlLiveness owner rather than scattered across RelayControlClient. * fix(relay): require a run of unanswered probes before tearing down a control A single unanswered probe was treated as proof of a dead pipe. STA-3320 already established that it is not: a cellular/VPN blackhole or a stalled TCP retransmit routinely swallows one pong from a peer that is still there, which is why RemoteRuntimeServerHeartbeat requires three consecutive misses. The networks this detection exists for are exactly the ones that drop a lone frame, so the first cut was more trigger-happy than the rest of the product. Three changes, all aimed at the cost of a false positive rather than the detection itself: - Three consecutive unanswered probes are now required. The interval drops to 8s so the full run (24s) still outlasts the relay's 15s application-level ping, preserving the property that a healthy cell clears the probe even where a middlebox swallows RFC 6455 control frames. Detection lands at ~34s rather than ~30s, against 75-90s before the fix. Any inbound frame retires the whole run, so a later probe never inherits an earlier miss. - The deadline carries the fleet's existing +/-10% jitter (RELAY_RENEWAL_JITTER_RATIO). Without it every host timing out against one slow cell would probe and terminate on the same boundary -- the synchronized cohort burst that constant was introduced for. The pre-existing 75s watchdog comb has the same defect; this path does not add to it. - A liveness teardown now names its cause in the log. It reaches the origin as an ordinary 1006 close, so without a label a probe-driven reconnect is indistinguishable from any other drop, and a fleet-wide false positive would be invisible in exactly the incident where it matters. Mutation-checked: a miss limit of 1 fails four tests, 2 fails one, and removing the jitter fails one. * fix(relay): keep the request-timeout rejection classifiable The diagnostics added in the previous commit were appended to the rejection's message, which silently destroyed the signal they were meant to add. `mobileRelayMintFailureFromUnknown` classifies a relay failure by testing `error.message` against an anchored `/^relay_[a-z0-9_]{1,74}$/`, so `relay_control_request_timeout reqKind=invite cell=...` stopped matching and every pairing timeout was reported as the generic `relay_mint_failed` instead -- in exactly the flow STA-7672 is about. The pairing path logs only the resolved code and discards the rejection's text, so nothing ever surfaced the suffix: the change was a net loss of diagnosis. The message is bare again and the diagnostics are logged from RelayControlLiveness, which is the only place they survive. Added relay-control-timeout-classification.test.ts to pin the contract end to end through the real classifier, since the coupling is invisible at both sites: restoring the suffix turns the assertion into relay_mint_failed. Found in adversarial review. * refactor(relay): collapse the half-open detection onto one object Design review of the three commits on this branch. No behaviour change: the 184 relay tests pass unmodified, and reverting PROBE_MISS_LIMIT to 1 or 2, or dropping the jitter, still fails them. Dead plumbing. `probeIntervalMs` had zero callers across three layers (client options -> conditional spread -> liveness default), and `silenceLimitMs` the same -- the only production construction site, relay-control-origin.ts, passes neither. Both are gone. `livenessRandom` stays; one test uses it. The conditional-spread idiom went with them: `exactOptionalPropertyTypes` is off for src/ (only cloud/apps/relay-ops sets it), so it bought nothing that `?? Math.random` does not already do. Teardown owns its own log. A two-member reason union crossed a module boundary just to reach a console.warn, and the client re-derived `cell=` from relayOrigin when liveness already held `cellUrl`. Liveness now tears itself down and calls `terminate`; the client lost the import, the method, and the exported type. One probe object, one interval. `probeTimer` + `missedProbes` are now `probe: { timer, misses } | null`, so "no timer implies no misses" is structural instead of maintained by resetting in two places, and the sendProbe/onProbeUnanswered mutual recursion is a plain setInterval. Jitter is computed once per run rather than per tick -- one offset already desynchronizes the cohort. Honest probe label. If ping() throws, the old arm path returned false and the caller logged `probe=in-flight/0` moments after terminating the socket -- a false statement in the line that exists for incident forensics. The arm path now returns the label it means, including `probe=send-failed`. Absorbed RelayControlSilenceWatchdog. It had one consumer and no test file, and this branch had to punch a `lastInboundTime` getter through it purely so liveness could read state it holds. `lastInboundAt` now sits next to `openedAt`; the file, the getter, the import, and the onDead('silence-limit') lambda are all gone. Also: dropped `RelayControlRequestTimeout.reqId` and `PendingRequest.sentAt` (both written, never read -- the timeout closure captures the local `sentAt`); dropped the two `'n/a'` branches, unreachable because a request timeout can only fire after sendActive succeeded, which requires a state only handleProofMessage reaches on the line before it calls liveness.start(); moved the classifier invariant off a void-returning callback type and onto REQUEST_TIMEOUT_CODE, where an edit to the string is next to the warning about editing the string; and replaced the `live` parameter with an `isLive()` option so liveness asks rather than being told, which also let `liveness` be constructed before `requests` instead of a closure reading a field assigned on a later line.
Co-authored-by: m4air <m4air@Mac.localdomain>
* fix(renderer): cancel copied prompt reset on unmount * fix: address memory PR review regressions and withdraw false positives --------- Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
* fix: release scroll intents for closed parked tabs * fix(renderer): release scroll intents on worktree removal * test(renderer): cover parked worktree intent cleanup --------- Co-authored-by: m4air <m4air@Mac.localdomain>
…tablyai#20946) Co-authored-by: m4air <m4air@Mac.localdomain>
…i#20980) Co-authored-by: m4air <m4air@Mac.localdomain>
…yai#21010) * fix(browser): release page callbacks when a guest is destroyed * fix: address memory PR review regressions and withdraw false positives --------- Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: m4air <m4air@Mac.localdomain>
…21138) Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: m4air <m4air@Mac.localdomain>
* fix: skip empty chunks in streamed agent text * test: lint empty-delta retention reproducer --------- Co-authored-by: m4air <m4air@Mac.localdomain>
…1144) * fix: release canceled working-directory waiter references * test: normalize working-directory proof patch --------- Co-authored-by: m4air <m4air@Mac.localdomain>
…er's hardware back on web (OTA phase C, C2.10) (stablyai#21729) * fix(mobile): name the narrow host header's controls The header renders two toolbars and the phone sees the narrow one, whose controls carried neither a role nor a name. A screen reader could not find them, and C2.9's render check could only assert their absence at 390 px. The wide toolbar already names every control from the same state, so the fix is to say the same thing rather than invent wording: filter, sort, group, accounts, tasks and the search toggle take their wide sibling's role and label expression verbatim. The census names a seventh site the plan did not: the Reconnect button in the status bar above both toolbars, which is shared rather than narrow and has no wide sibling. It announces through its Text child today, so it takes the role and the string it already renders. No layout, style, handler or order changed; the diff is accessibility props only. The census parses the file with the TypeScript API and rules that every Pressable carrying an onPress has a button role and a name, and that a control both toolbars render is named the same way in both. It keys the pairing on the handler, because that is what makes two elements the same control, and asserts each shared handler is found exactly twice, so a control deleted from one toolbar cannot leave the naming rule comparing a group of one with itself. Red first, naming all seven sites by path and line. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): stop the right drawer arming hardware back on web React Native Web logs "BackHandler is not supported on web and should not be used." and returns an inert subscription, so inside the shell's page every open of this drawer put that line on the console and armed nothing. The gate is the one mounted-bottom-drawer and the file preview already carry, with the same comment stating the degradation: there is no hardware back in a WebView, and the shell owns the one the phone has. The drawer had no render test. This one mocks react-native, the safe area, gesture handler and Reanimated the way the bottom drawer's hand-back test does, and reads the call rather than the console: on iOS and on Android the handler is registered once for 'hardwareBackPress' and released when the drawer hides, and on web it is never reached. The three native cases are the control that keeps the web case honest; they passed before the fix, which is what makes the single red meaningful. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): type the right drawer test's element helper The tests-typecheck ratchet was red on the previous commit: the drawer's props declare `children` as required, so passing it as createElement's third argument left no overload matching. It is a prop here, and the helper answers a ReactElement rather than a return type borrowed from createElement. Test files sit outside `tsc --noEmit`, so only the ratchet sees this; it is the gate that exists because a type-level pin in an unchecked test proves nothing. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): write the right drawer test in JSX Lint was red on the previous commit and I ran it in the same command as the commit, so it landed: passing `children` as a prop to satisfy the type checker is exactly what react(no-children-prop) refuses. The canonical form settles both, so the test is JSX in a .tsx file and the drawer takes its body as a child again. The StyleSheet mock's generic needs the trailing comma a .tsx file requires. Re-proved in this form: with the web gate removed the web case fails and the three native cases still pass. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): derive the header's naming groups, and read a spread as unknown Round 1, four folds. The drawer's comment claimed the console line was observed inside the shell's page. It was not: the drawer's one caller is the review screen, whose route C4 serves, so no page closure reaches it today. The gate is pre-emptive and now says so in its own words rather than borrowing the bottom drawer's sentence. The naming rule iterated a hand-written list, so it only ever compared the six controls both toolbars render. Giving the two `actions.openFloatingWorkspace` sites different labels left the census green. The groups are derived from the discovered controls now, keyed by the handler text, so any handler this header presses from more than one place is compared and the failure prints both names. The declared list stays as the precondition it always was: each of the six is found exactly twice, which is what keeps the derived rule from holding vacuously over a file with no repeated handler. The scan read `Pressable` only and dropped any control whose `onPress` read as empty text, which is what a spread reads as. It reads `TouchableOpacity` too now, and a spread answers unknown rather than absent: a control whose handler or whose accessibility props arrive through one is kept, fails both rules, and prints `spread` rather than `none`, so it can never be mistaken for a control the scan judged. Red first on all four: the reviewer's disagreeing-label mutation, a spread over the a11y props, a spread over the handler, an unlabelled TouchableOpacity, and a shared control deleted from one toolbar. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): read a braced-empty label as unnamed, from one shared reader Round 2, two folds. Only a bare `""` or an omitted attribute read as unnamed, so `accessibilityLabel={undefined}`, `{''}` and an empty template all left a control with nothing to announce and the census green. Reproduced on both Tasks sites with each of the three shapes before the fix. The reader unwraps a braced expression now: a string or a no-substitution template answers its own text, and the identifier `undefined` answers empty, so all three read as unnamed. That reader was a near-verbatim copy in both censuses, which is how one of them could have gained this rule and the other kept the hole. It lives in one module under mobile-web-shell now, named for what it reads and typechecked by mobile tsc rather than by the ratchet alone. Both censuses import it and neither changed an assertion; their diffs are the deleted copies and the import. Red first, five mutations: the three empty shapes on both Tasks sites here, and `{undefined}` and `{''}` on the tasks Back, which the page census now catches too and did not before. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(editor): keep filename visible in long paths * fix(review): keep diff filenames visible * fix(review): prevent path metadata overlap
…tablyai#11955) * feat(editor): add opt-in collapsed unchanged regions for file diffs The combined "View All Changes" diff already collapses unchanged lines into expandable bands (DiffSectionBody sets Monaco's hideUnchangedRegions), but a single-file diff opened from Source Control renders the whole file. Reviewing one changed line in a long file means scrolling past everything else. Adds a General > Editor setting, default off, that applies the same Monaco option to the single-file diff viewer. Off keeps today's full-file rendering. The option is always emitted rather than omitted when off: Monaco retains the last applied value across an options update, so dropping the key would strand an open diff in collapsed mode after the setting is turned back off. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(settings): register collapse unchanged search entry * fix(editor): keep diff viewer under line limit * fix(editor): satisfy diff viewer line budget --------- Co-authored-by: Dan Cieslak <dcieslak19973@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Neil <neil@stably.ai>
* perf(terminal): skip background SGR scan without escapes * perf(terminal): avoid duplicate renderer risk scans * test(terminal): pin the carried renderer risk scan tail The foreground renderer-risk scan splices the carried tail onto the incoming chunk before classifying it, and nothing covered that ordering: a pre-gate moved back above the concatenation would silently drop the refresh for a background SGR split across ConPTY chunks. Also pins the escape-free ASCII path and the shared global SGR pattern's statelessness across calls, since the background hit returns mid-loop.
* perf(terminal): skip kitty scans for plain output * fix(terminal): keep the kitty scan fast path total for absent chunks The new escape-byte fast path dereferences the chunk before the string concatenation that used to coerce a nullish value, so an unchecked caller now throws instead of no-opping. Normalize once at the top. Also type the AgentTerminalPreview connect mock against the real preload signature, which turns the stale bare-string replay fixture that tripped this into a compile error.
* fix(terminal): align IME preedit to terminal cell grid * fix(terminal): preserve native shaping and reuse IME preedit on repaint * fix(terminal): preserve native shaping with bounded IME spacing runs * test(terminal): account for inline preedit subpixel rounding * test(terminal): keep the IME grid fixture wide at every DPI * chore: regenerate xterm patch after rebase * test(terminal): remove IME assertion lint findings * test(terminal): avoid reflective IME fixture access * test(e2e): run IME renderer matrix with WebGL available * fix(ci): restore editor line budget
Co-authored-by: m4air <m4air@Mac.localdomain>
…blyai#21014) * fix(runtime): reject provider inventory across PTY lifecycle changes * fix(runtime): canonicalize SSH inventory generation keys --------- Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: Neil <neil@stably.ai>
* fix(browser): bound CDP output for stalled clients * fix(browser): log CDP outbound overflow before terminating the client The outbound queue terminated the automation client silently on overflow, so the client saw a socket close indistinguishable from a crash. Surface the cap that tripped and the backlog held when it did. The queue dropped its backlog before invoking onOverflow, so the counters were already zero at the callback. Snapshot them first and pass them through. --------- Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: Neil <neil@stably.ai>
* fix(claude): enforce history window quota while reading * test: repair history quota audit dependency and CI import * fix(native-chat): record why restart reconciliation leaves work unconfirmed Two silent paths hid the cause of an unconfirmed submission. The reconciler's bare `continue` on an `unknown` outcome dropped the reason it already carried, and the transcript read swallowed its error, collapsing an oversize file and a genuine read failure into the same verdict. Log both. No control flow changes. --------- Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: Neil <neil@stably.ai>
…21020) * fix(runtime): persist acknowledged terminal tab retirement * test(runtime): drain tab retirement fixture writes before teardown * fix(runtime): explain a refused workspace terminal close The Sleep-workspace path threw the raw refusal enum ("stale-terminal") as an Error message, which reaches a CLI user verbatim and a Sleep toast via describeSleepFailure. Map each refusal reason to a sentence instead. Also pins two behaviours that had no coverage: the user-visible outcome of a republished stale-terminal refusal on the web client (the caller cannot tell it from a real close), and the one-call-per-close invariant that keeps a successor terminal alive. The bounded close retry was NOT implemented: notifier.closeTerminalTab carries only a tab id, so a second call destroys whatever successor took that id. * test(runtime): build refusal fixtures without type assertions The changed-code quality gate rejects new `as` casts. Replace the branded-outcome cast with refusedMobileSessionTabClose, and model the wire-skew reason as a decoded host answer instead of `as never`. --------- Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: Neil <neil@stably.ai>
…#20963) * fix(ai-vault): bound incremental transcript record assembly * fix(ai-vault): skip one oversized record instead of dropping the session An agent transcript record over the 10 MiB budget threw out of the JSONL fold, so the whole session vanished from Agent Session History and from search. A 10 MiB base64 image or a runaway tool result is ordinary. The reader now discards the offending record up to its newline and keeps folding. The in-progress record always starts at `consumedThrough`, which is what makes both its running size and the resume offset past a discarded span exact; an unterminated oversized tail leaves the cursor at the record's start so a still-growing record is re-read rather than guessed at. Skips accumulate on the resume point keyed by start offset, and the scanner reports them as a per-session `notice` so nothing is silently lost. The budget itself is unchanged. --------- Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: Neil <neil@stably.ai>
* feat(agents): add OpenCode 2 beta support Co-authored-by: Xiro The Dev <lethanhtrung.trungle@gmail.com> * fix(opencode2): support current plugin lifecycle and session storage * fix(opencode2): preserve lifecycle ordering and full session capture * test(opencode2): cover setup event bridge * test(opencode2): cover setup event bridge * test(browser): satisfy anti-slop naming check * test(opencode2): cover live form lifecycle * fix(relay): preserve OMP config directory selection * test(opencode2): avoid assertions in bridge fixture * fix(rebase): retain OMP resume and fresh launch behavior * test: align upstream OMP resume expectations * test(opencode2): verify rejected form closes waiting state --------- Co-authored-by: Xiro The Dev <lethanhtrung.trungle@gmail.com>
…'s Close from Back (OTA phase C, C4.3) (stablyai#21739) * fix(mobile): name the custom-key drawer's Back for the accessibility tree (OTA phase C, C4.3) `CustomKeyModal`'s Back is a bare `Pressable` with a label and no role, so a screen reader has nothing to announce it as. It is reachable today from the session sheets and the terminal shortcut settings, so this is a gap now, not only inside the page. It surfaces here because `screenTree` takes a screen's directory: C4.4 registering the review route puts the whole of `src/components` under the Back rule. Fixing it in the PR that registers would make a route entry carry unrelated accessibility work, so the census gains a case that holds the arriving trees to the same two rules before the rows land. Red first it printed exactly what the rule would: src/components/CustomKeyModal.tsx:191 role=none label=Back Once `PAGE_SERVED_SCREENS` has the two rows, `CONTROLS` covers these trees and the new case becomes a second reading of the same thing; it says so. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): split the source-control header's Close from its Back (OTA phase C, C4.3) One `Pressable` served both modes — `onPress={onBack}` with a conditional label, `X` docked and `ChevronLeft` full-screen. The Back census reads a control by what its handler does, so it sees that one as a Back and then requires a label starting with "Back", in a mode where the control dismisses the dock beside the terminal. The cheap way to go green is to call a close "Back", which satisfies the rule by making the wording wrong. So the modes become two controls. Embedded presses `onClose` and is named "Close source control"; otherwise it presses `onBack` and is named "Back to session". Both carry the button role. The panel stops choosing by mode and passes both handlers; the dock keeps exactly the behaviour it had, its dismiss still `onRequestClose` falling back to a pop. Probed before it was written: run through the census's own predicate, the post-split shape yields one back control rather than two — `onClose` matches neither the handler pattern nor a declaration this file holds, being a destructured prop — so the Close is invisible to the rule and the presence precondition still holds on the Back. The component test is what the census cannot do. Nothing else pins this: no golden names this component and no parity census covers `src/source-control`. Red first against the single control, both cases failed on the missing role. And renaming the Close to "Back to close source control" — the gaming this split exists to prevent — reds the component test while leaving the census green, which is the whole argument for having both. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): assert a Back per arriving tree, and name the refresh control (OTA phase C, C4.3 round 1) The arriving-trees presence case counted controls over the union of both trees, so one tree answered for the other. Per tree now, in the shape the block above it already uses per screen module. Red first, with the mutation round 1 named: rename the route branch's handler to `onDismiss` and its label to `Return to session`, and the only Back in `src/source-control` disappears. The per-tree case names that tree. The same mutation against the union count passes all six cases, which is what the change is for. The refresh control had a label and no role, so react-native-web renders a `div` carrying `aria-label` and a screen reader announces no control. Its two siblings in this header already carry one. Two claims in my round-1 report were wrong and I am the reason the body carried them. The Close pressing `onBack` reds the census's label rule as well as the component test, not the component test alone; and no fixture of the post-split shape exists — the shape is read from the real file. Both were stated from reasoning rather than from a run. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): assert an arriving Back per screen, not per tree (OTA phase C, C4.3 round 2) Round 1 moved the arriving-trees presence assertion from the union to each tree, which was not far enough. `src/components` holds two Backs, so the tree answers for both: renaming `MobileDiffReviewHeader`'s handler to `onDismiss` and its label to `Return to session` leaves every case in the file green, with `CustomKeyModal.tsx:191` standing in for the screen that just lost its Back. Reproduced before the change — six passed with the review header's Back gone. Per screen module now, which is what the table above already does and for the same reason its docstring gives: a directory with more than one control cannot say which one a rule was written about. The two modules are named and the trees derive from them, so the pair C4.4 adds to `PAGE_SERVED_SCREENS` is the same pair spelled once here. Both mutations red on the new case and name the file: the review header's rename names `src/components/MobileDiffReviewHeader.tsx`, and round 1's source-control rename still names its own, so this does not trade one cover for another. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb --------- Co-authored-by: Claude <noreply@anthropic.com>
* fix: retire closed editor models from the app shell * test(editor): use checked Monaco attachment calls * Preserve bounded editor view caches when retiring closed models * docs(editor): describe batched model retirement * fix(editor): preserve cleanup work across registry replacement * fix(editor): build editor model URIs with the file scheme Monaco keys its model registry by `uri.toString()`, and both `@monaco-editor/react` (via the `path` prop) and the closed-tab disposal path built that key with `Uri.parse`. On Windows a raw path such as `C:\repo\a.ts` parses as scheme `c`, which fails the scheme gate in `modelService._schemaShouldMaintainUndoRedoElements`, so closed-file undo history was dropped for every file at any size — not only the large files the tradeoff note covers. Add `toEditorModelUri`, the one filesystem-path -> model-key function, built on `Uri.file` so the result always carries the `file:` scheme and re-parses to itself. Route model creation, disposal lookup and the still-open ownership comparison through it so all three agree; a divergence there would dispose a model an open editor is still editing. --------- Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: Neil <neil@stably.ai>
* fix(daemon): pause producers when stream backlogs grow * fix(daemon): reset stream backpressure on socket replacement * docs(daemon): point retention audit at current reproducer * test(daemon): validate stream retention audit outcomes * fix(daemon): bound the stream producer stall and leave a visible gap Stream backpressure pauses a session's PTY with no deadline: the only un-pause comes from the consumer draining, so a half-open peer that stops reading without closing freezes the shell for the rest of the session. Arm a 60s watchdog on the false->true stream-pause transition (not on the re-assertions refresh() makes for neighbouring sessions). On fire, mark the session stall-released: it becomes keep-tail droppable, its backlog is thinned behind a dataGap, and the producer runs again. The existing dataGap path makes the renderer restore that pane from the daemon's snapshot, so the user sees the terminal jump to current rather than sit frozen. The mark clears once the session's last byte leaves the daemon, restoring ordinary pausing. Nothing here reports a process exit - loss of contact with a consumer is not evidence about the child. Also enable TCP keepalive on the stream socket so a genuinely dead peer closes and onStreamDisconnected clears the pause. * test(daemon): put each casting SAFETY: directive on one line `oxlint-disable-next-line` covers only the line directly after it, so a rationale wrapped onto a second comment line suppressed nothing and the casts failed the changed-code quality gate. Drop the remaining JSON.parse cast for an annotated binding. --------- Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: Neil <neil@stably.ai>
… source-control domain (OTA phase C, C4.1) (stablyai#21732) * refactor(mobile): open PR sidebar URLs through the external-link seam (OTA phase C, C4.1) The three openers in the PR sidebar called `Linking.openURL` directly: a check's "open on the web", a comment's permalink, and a link inside comment Markdown. Inside the shell's WebView react-native-web routes that to `window.open(url, '_blank', 'noopener')`, which both shells refuse and which resolves anyway, so the tap reports success and opens nothing. Both C4 routes reach the sidebar, so both would have shipped that. The census is the point rather than the three edits. It derives the two future route closures through `mobileWebAppRouteClosure` and holds every module in them to the seam, so a module entering either closure later is ruled without anyone adding it here. Red first it named all three by `path:line`: CommentMarkdown.tsx:2, PRChecksSection.tsx:2, PRCommentCard.tsx:2, on both routes. The walk it runs was the third copy of one function, so it moves into the seam's own module beside the predicate that module exists to share, and the files and tasks censuses now call it too. It reports `path:line` where the copies reported paths; `reachesReactNativeLinking` keeps its name and its meaning and is now derived from the line list, so there is one rule. An empty offender list is empty in either shape, which is why repointing the two landed censuses moves nothing they assert. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): copy through the platform clipboard seam in review and conflicts (OTA phase C, C4.1) The two copy actions both C4 routes reach called `expo-clipboard` directly: the conflict section's refresh commands and the review sheet's notes. On the web that module is `navigator.clipboard`, which needs a secure context — the iOS shell serves the page from a custom scheme and Android from https, so the path works on one platform and silently not on the other. `useClipboardWriter` is the seam C2.4 landed for exactly that. Red first, the census named both routes: `ExpoClipboard.web.js` in each closure, and `src/platform/clipboard.web.ts` in neither. Both call sites also stopped ignoring whether the pasteboard took the text. The conflict section already returned on a throw, so the seam's rejection reaches an arm it had. `copyNotes` had none and its only caller is `void controller.copyNotes()`, so a rejection would have been unhandled with "Review notes copied" left on screen; it now catches and reports through the screen's own error line. That is the one behaviour change here and the reason `clipboard` joins its dependency array. Its suite mocked `setStringAsync` as resolving `undefined`, which the seam reads as a pasteboard that refused, so every copy would have gone down the new refusal arm unseen. The mock now resolves `true` and two cases pin both arms; mutating the catch away kills the refusal one. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): take the source-control router from the handoff seam (OTA phase C, C4.1) The hub takes its router once, in the openers hook, and passes it down to the runners and the panel — so one `useRouter()` is this domain's whole reach into routing, and it was expo-router's own. Inside the shell's page that posts no `navigate`, so the hub's push to review would stay in the document whatever its grants, and its push to a native route would paint Unmatched over the page. Inert today: no C4 route is registered yet. `use-mobile-source-control-runners.ts` is the second case and the reason the rule reads value imports rather than identifiers: it named expo-router only to write `ReturnType<typeof useRouter>`, a value import in a type position that keeps the module in the graph. `RouteHandoff` is the seam's own name for that type. The census is C3.1's, and its walk moves to `src/navigation` rather than being copied a second time; each domain keeps only its own evidence, the list of modules meant to hold a router. Red first it named both modules on the expo-router rule and reported no handoff caller at all. The C2.9 hop census is unchanged and cannot move: its targets come from the call sites, and the derivation over this tree returns the same ten targets and the same 26 unresolved sites before and after this commit, byte for byte. Its `HANDED_OFF` pin is over registered routes, of which this adds none. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): move the shell route guard out of the files domain (OTA phase C, C4.1) `src/files/mobile-file-shell-route.ts` was never about files: it parses a route against `BridgeInitRouteSchema` and builds the key a shell screen remounts on. It moves to `src/mobile-web-shell/shell-screen-route.ts` as `shellScreenRoute` / `shellScreenRouteKey`, with its test. The move is pure — with the rename applied and comments stripped, the old file and the new one diff to nothing. Three routes had grown their own copy of the call and two had none. The copies go: `agent-history` and `tasks` now ask the shared predicate, which is the same schema and the same fallback they already had. `index.tsx` had no guard at all, so a `.` or `..` host id was handed over and came back as "Update Orca to open this workspace" painted over the native list behind the switch; it now stays native. That is the one behaviour change here, pinned red first and killed by mutation. `web.tsx` keeps handing that route over on purpose and is exempt by name: its fallback is a redirect to the route the user came from, so the host's own verdict is the better answer there, which `mobile-web-shell-route.test.tsx` already pins. No `key=` expression moved; the three switches still key differently (host id, pathname, pathname plus params) and making them agree is a behaviour change for another PR. The census walks the route tree rather than a list, so a switch added later is held to both rules without being added here. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): mount the copy cases without a client instead of casting one (OTA phase C, C4.1) The changed-code gate flagged four type assertions on the two cases added with the clipboard seam: they stubbed an `RpcClient` the way the file's older cases do, and the gate reads changed lines. Copying reaches no client at all, so they mount without one, which is both cast-free and a truer statement of what the path needs. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): say what the censuses report and sort the red list by line (OTA phase C, C4.1 round 1) Round-1 folds, four wordings and one ordering. `externalLinkOffenders` said "every call site" and reports the line the name enters the module: a named import once, however many times the module calls `openURL`, because the import is what the rule is about and what has to go. Only a namespace import reports its uses, there being no single line to name. The docstring now says that. Its red list sorted the rendered strings, which puts `:10` before `:2`. It now sorts by path and then by line as a number. Pinned against a written fixture rather than the tree, because the case needs a module with sites either side of line ten and no module in a closure has to keep having one — the first fixture used lines 11 and 12, where both orders agree, and the mutation walked straight through it. `shell-screen-route.test.ts` still named the files screens in its describe after the guard stopped being theirs; it names what a switch does now. `router-seam-census.test-support.ts` excluded `.test-support.ts` from the walk, which the files census it was extracted from never did. Dropped, so both censuses walk the same set. Inert today: neither `src/files` nor `src/source-control` holds such a file, so it only decides the next one. The `web.tsx` exemption claimed a redirect "that looks like nothing happened". What was measured: adopting the guard there sends a `..` deep link through `Redirect href="/h/.."` to the host route, which this PR keeps native, so the developer lands on the host list with nothing said about why the page did not open. The route is `__DEV__`-only and the host's own failure screen is the better verdict. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): see every react-native alias, normalise the host id, surface a refused copy (OTA phase C, C4.1 CodeRabbit) Three bots findings on stablyai#21732, all real. **Every alias, not the first.** `reactNativeLinkingSites` found namespace imports with `exec` and inspected only the first binding, so a module importing the namespace twice and calling `Linking.openURL` on the second reported no site at all. It reads every alias now and counts a line once however many meet on it. Red first with exactly that fixture. **The host id can be an array.** `app/h/[hostId]/index.tsx` read it bare, and Expo Router answers a repeated key with one: `String(['a','b'])` is `a,b`, `encodeURIComponent` makes that the single segment `a%2Cb`, and the segment rule accepts it — so the shell opened a page for a host nobody has. Through `firstParam`, as the other four switches do. Red first it handed over `/h/host-1%2Chost-2`, and the empty-array case found a second one: `[]` is truthy, so a bare read built `/h/` and handed that over too; `firstParam` answers `''` and the route stays native. That import pulls the source-control screen state, and with it the lucide barrel whose `LucideProvider` re-export is the gap the web build patches, so the suite mocks the barrel as the other suites do. It moves no page closure: the closure resolves `index.web.tsx`, which this does not touch, and the index route still measures 3426 modules, 289 local, 22 families. **A refused copy said nothing.** `PRConflictingFilesSection` caught the rejection and returned: no tick, no message, a tap indistinguishable from one that copied. The label now carries the third state, reusing the tasks page's own wording for it, and the component has its first test. Mutating the failure arm away reds it. Its prop narrows to `Pick<PRInfo, 'mergeable' | 'conflictSummary'>`, which is what it reads and what let the test drop a cast the gate flagged; every caller holds a full `PRInfo` and satisfies it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): read the censuses' subjects as code, not as text (OTA phase C, C4.1 round 2) Round-2 additions. A separate commit because `d4b14d54e5` was already made and this lane does not amend. **The seam walk parses now.** Matching `X.Linking` in the text named it inside a comment that talks about it and inside a string that quotes it, and the named-import regex did the same for a commented-out import. Checked against the previous implementation, all three fixtures were red there: the comment case reported lines 2 and 3, the string case reported the string's line beside the real call, and `// import { Linking } from 'react-native'` reported line 1. The walk builds a `SourceFile` and reads import declarations and property accesses, so comments and strings are gone by construction and the quote styles stop being a special case. Cost measured on the three closure censuses: 3.3 s, unchanged. **The route census reads the call, not the import.** A switch that keeps the import while the call goes — deleted, or moved behind a branch that never runs — looked exactly like one that asks. It now needs both, proved by mutation: dropping `shellScreenRoute(` from `tasks.tsx` while leaving its import names `tasks.tsx`. A fixture carries the same rule in isolation, since every switch in the tree calls what it imports and the case would otherwise be unfalsifiable against it. **And recognises a switch by its import** of `MobileWebShellScreen` rather than by `<MobileWebShellScreen` in the text, so an alias or a line break the formatter chose cannot hide one and a comment cannot invent one. The `app/h/[hostId]` root stays written out: deriving it from the manifest is not a one-liner from here, the manifest being an `.mjs` this test reads as text. What ties the two together instead is a new case asserting every registered pathname starts with that prefix, so a page route outside it fails rather than going unwalked. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): read the imported name, not the local one (OTA phase C, C4.1 CodeRabbit) `import { Linking as NativeLinking } from 'react-native'` went through the census untouched: the walk compared the specifier's local binding, which is `NativeLinking`, while the imported name lives in `propertyName` when a specifier renames it and only in `name` when it does not. Reproduced before the fix — the aliased import with a call beside it reported no site at all. Reading `propertyName ?? name` closes it in both directions. A module that renames `Linking` is named at its import line like any other, and a module that imports `View as Linking` is no longer named for a local binding that reaches nothing. The second was a false positive the old comparison had by construction. One more of the same class, found while checking and verified rather than assumed: `import RN from 'react-native'` typechecks in this project (tsc accepts it), and a default binding is the whole namespace exactly as `* as RN` is, so `RN.Linking.openURL` through it was invisible too. The default binding joins the alias set, which already reports uses rather than the import. Four fixtures. Three red on the previous walk: the renamed import, the local-only `Linking`, and the default import. The fourth — an alias imported that never reaches `Linking` — passed before and is here to hold the other half of the rule, that importing react-native is not itself the offence. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): parse each module as its own kind, and read re-exports (OTA phase C, C4.1 CodeRabbit) Two ways a module reached `Linking` past the census, both reproduced before the change. Every file was parsed as TSX. In a `.ts` module `const id = <T>(value: T) => value` is a generic arrow; as TSX it is an unclosed JSX element, and the parser folds the rest of the file into the error node. A `RN.Linking.openURL` after one reported nothing, and so did the same call with its import above the arrow. The file name goes into the parse now and TypeScript reads the kind off the extension; `externalLinkOffenders` passes the real path, which it had all along. `ExportDeclaration` was never inspected, so `export { Linking } from 'react-native'` put the name back in reach of anything importing that module while the census saw an import list it was not on. All four shapes are read — named, renamed, `export *` and `export * as` — and reported at the export statement, which is the line to delete exactly as an import is. A re-export of another name, or of `Linking` from somewhere that is not react-native, stays unnamed. Seven fixtures. Five red on the previous walk: the `.ts` generic arrow and the four re-export shapes. The two that pass before and after hold the other half, that re-exporting is not itself the offence. The named-import and re-export clauses read `propertyName ?? name` through one helper rather than two spellings of it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb --------- Co-authored-by: Claude <noreply@anthropic.com>
* feat(composer): restore compact branch picker UX * fix(composer): address picker review feedback
…auto-sync-fork-with-upstream-stablyai-orca-ru-20260921T1400 # Conflicts: # src/renderer/src/components/native-chat/native-chat-runtime-image-send.ts # src/renderer/src/components/native-chat/use-native-chat-pty-composer-send.ts
Resolve every differing path against config/fork-ownership.json at the re-cut upstream v1.4.206 (c62eca3), which replaces the retracted tag main was built on (c464b10). - checkout.txt 3782 paths reset to the tag - remove.txt 33 paths removed - ours.txt 156 exception paths restored to the fork side; 23 of them took a three-way merge against the retracted tag as base, 14 merging clean and 9 resolving fork-side where the exception's reason is what upstream would undo - merge-review.txt 1193 seam/feature paths reviewed against declared lines - regenerate the RPC params catalog, the one undeclared fork edit the reset drops - advance all 16 FORK-COPY-SHA headers; no tier-2 source moved between the two v1.4.206 commits - repoint two new cross-version-wire PRE_STACK_REF pins off the bare upstream v1.4.199 tag, which a fork clone cannot resolve, onto v1.4.201-rc.0.zy01, whose harness modules are byte-identical to it, and declare both as seams
…6 merge Twelve seam files drifted from their recorded residual budgets; each was re-resolved as a real three-way merge against the retracted tag, composing upstream's hunk with the fork's declared seam lines instead of letting -X ours keep the fork's stale side. - NativeChatApprovalCard.tsx, NativeChatExperimentalSetting.tsx and use-filters.ts had lost ~25 upstream lines each; the approval card's wrapper was rewritten upstream, so the width seam moves onto the new wrapper and its declared line is re-recorded - use-native-chat-session-options.ts composes the fork's reportedSessionOptions with upstream's new paneKey model discovery rather than displacing it - rpc-params-type-parity.ts, protocol-version.ts, global-settings-types.ts, default-global-settings.ts, project-group-operations.ts, bundled-skill-guides.ts, .gitignore and AGENTS.md compose both sides - fork-artifact-passwords/ArtifactPublishButton: upstream removed orcaProfileConnecting, so the fork's copy drops the connecting state with it - native-chat-runtime-send.test.ts stays fork-side: upstream's new OMP image framing lives in a module this fork deletes, so its 5-argument tests cannot compile against the fork's target-based signature - re-baseline two shrunk residual budgets upstream absorbed
…merge verify:skill-bundle-manifest reported both generated artifacts stale: the merge changed shipped skill sources, and the fork's copies index its own release history rather than upstream's revision numbers, so they regenerate rather than take the tag's.
v1.4.206 adds a `mobile web app bundle` job that bundles mobile/src for the browser, where `node:buffer` does not resolve: src/browser/mobile-browser-frame-state.ts:1:23: ERROR: Could not resolve "node:buffer" The fork rewrote that specifier to satisfy the changed-lines React Doctor scan, which does not read mobile/.oxlintrc.json. Its own ledger entry scopes that rewrite to test files, "since Metro — the reason mobile avoids the node: protocol — never bundles a test file"; this module is production mobile source, so the rewrite never applied to it and was latent until a job started resolving the import. Restore upstream's specifier, and drop the manifest exception and its ledger sentence together.
v1.4.206 adds a packaging-job census asserting an exact inventory of every job that reaches beforePack. Its list names `pr.yml package_windows`, which this fork's pr.yml deliberately does not run, so the census finds 11 where the inventory expects 12. Drop that one entry and fork-own the file, mirroring the existing pr-workflow-parallelism.test.mjs exception. Nothing is weakened: the census itself, the out/mobile-web producer assertion and the install-mobile-dependencies assertion all still run at full strength against every job the fork does have.
…sRef The dock's pane-retirement seam read deps.settingsRef.current directly. v1.4.206's pending-close fixture is upstream's own and supplies no settingsRef, so three pending-close suites threw "Cannot read properties of undefined" across shards 4, 5 and 6 — a fork seam failing upstream's tests, not the feature. Also mirror CI's two new relay-region excludes into the sandbox runner's UNIT_EXCLUDES; its own contract test asserts the two lists match, and unit-tests.yml added them in this release.
zpyoung
deleted the
zpyoung/auto-sync-fork-with-upstream-stablyai-orca-ru-20260921T1400
branch
September 22, 2026 00:23
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
Absorbs upstream v1.4.206 at
c62eca3988— the re-cut tag, not the one this fork already carries.Upstream retracted its first
v1.4.206(tag object669a70d444→ commitc464b10149, tagged 2026-09-18 04:05Z), which PR #84 merged andv1.4.207-rc.0.zy01shipped on. It has since re-taggedv1.4.206atc62eca3988(tag objectb3095f4330, tagged 2026-09-20 20:10Z), cut from trunk rather than therelease/v1.4.206-adhocbranch.The two commits are not related by ancestry:
git cherrymarks all four as equivalent-patch — so resolving upstream-owned files to the new tag regresses none of them.Consequence to expect:
mainends up with two distinctrelease: v1.4.206commits. That is inherent to the retraction, not something this PR chose.The local
v1.4.206tag was force-updated to upstream's current object; the retracted tag object is archived locally atrefs/fork-archive/upstream-v1.4.206-retracted. Between that update and this PR merging,git describe --tagsonmainreportsv1.4.205, so a hand-runreleaseduring that window would compute a version below the published1.4.207-rc.0.zy01. Once this merges, both commits are ancestors and the anchor readsv1.4.206again.Resolution
Mode
auto-ours+tree. Base for every three-way merge isc464b10149, the retracted tagmain's upstream-owned files actually came from.Two tree conflicts, both deleted by us / modified by them, both declared
deleted: trueexceptions, both resolved as the fork's deletion:native-chat-runtime-image-send.ts,use-native-chat-pty-composer-send.ts.Classifier over 5164 differing paths: checkout 3782, remove 33, ours 156, merge-review 1193.
ours.txtauditUpstream moved on 23 of the 156 exception paths. Each took a real three-way merge:
pr.yml(gaining the newmobile_web_appjob),release-cut.yml,release-mac-build.yml,electron-builder.config.cjs,reliability-gates.jsonc,check-changed-code-quality.mjsand four contract tests.pr.yml's deletedpackage_windowsjob,pr-workflow-parallelism.test.mjs's aggregateverify.needsassertion (the exception says it must not restore upstream's exact list), the two skill-revision artifacts, the README badge SVG, and fourpending-upstreamnative-chat composer files.Checked deliberately for exceptions discarding upstream work:
mobile_web_appdid merge into the fork'spr.yml, and its contract test already assertsMOBILE_WEB_APP.uses:/version:/node-version:/packageManagerbumps anywhere in.github/to replay.checkout.txtsweepOne undeclared fork edit reverted:
rpc-params-catalog.generated.ts, regenerated as usual.Deleted-exception sweep
3 of 15 flagged; all three are imported only by the fork's own exception-owned files, none by upstream's new files. No withdrawal needed.
Pinned upstream tags
This release adds two cross-version-wire specs pinning a bare
PRE_STACK_REF = 'v1.4.199'. The harness resolves refs with a localgit rev-parseand never fetches, and this fork skipped v1.4.199 entirely, so both would abort the suite in CI.Repointed to
v1.4.201-rc.0.zy01, proven exact rather than approximate — every module the harness loads is byte-identical to upstream v1.4.199:terminal-retirement-proof-ledger.ts4d570497f64d570497f6mobile-session-terminal-retirement-proof.tsd5b6c62095d5b6c62095mobile-session-terminal-retirement.tse8caba4ddfe8caba4ddfworktree-identity-migration.ts2d2fa484ad2d2fa484adv1.4.199-rc.0.zy01was rejected: it predates the proof ledger module entirely. Both files are now declared seams with residual budgets.Tier-2, tier-4, locales
FORK-COPY-SHAheaders advanced.pending-upstreamentries: every ledger anchor resolves. Upstream moved on 9; all replayed or resolved above.Seam repairs
--verify-residualsflagged 12 drifted budgets. No module split — the line-count check cleared every one. Four had lost ~25 upstream lines each to-X ours; all 12 were re-resolved as three-way merges composing both sides:NativeChatApprovalCard.tsx— upstream rewrote the wrapper element; the width seam moves onto the new wrapper and its declared line is re-recorded in the manifest.use-native-chat-session-options.ts— the fork'sreportedSessionOptionsnow composes with upstream's newpaneKeyOMP model discovery rather than displacing it.rpc-params-type-parity.ts,protocol-version.ts,global-settings-types.ts,default-global-settings.ts,project-group-operations.ts(upstream renamedremoveWorkspaceSessionOwner→…Everywhere),bundled-skill-guides.ts,use-filters.ts,NativeChatExperimentalSetting.tsx,.gitignore,AGENTS.md— both sides composed.Two budgets re-baselined after re-reading the seam; both shrank because upstream absorbed lines the fork was carrying.
Upstream API change
Upstream removed
orcaProfileConnectingfrom the store. The fork'sfork-artifact-passwords/ArtifactPublishButtoncopy carried upstream's old connecting spinner, which is not fork behaviour, so it drops with upstream's.Feature collisions
agent-composer— confirmed. Upstream added OMP image-reference framing tonative-chat-runtime-image-send.ts, a module this fork deletes in favour of its own composer core. Upstream's new 5-argument tests cannot compile against the fork's target-based signature, sonative-chat-runtime-send.test.tsstays fork-side and upstream's new OMP image framing is not adopted. No fork behaviour lost; the fork simply continues not to have that upstream path.native-chat-session-options— possible, resolved with no loss. Upstream's own model discovery landed beside the fork's; they compose (see above).native-chat-coloring— possible. Upstream landed shared Markdown scanners. No fork loss observed.none.Commit accounting
449 fork commits, all 449 still reachable at their original SHAs. The range on the merged head reports 450 — the extra is the ownership-resolution commit. N includes the 4 commits from the retracted release branch, which are fork-reachable but absent from the re-cut tag by SHA.
Verification
pnpm install --frozen-lockfile--verify-seams--verify-residualstypecheck:node/:cli/:web/mobile-webpnpm lintTests are left to this PR's own CI, per the skill.
Pre-existing, not gating:
typecheck:e2efails at the pristine tag too —toggleWorktreeCardPropertyis referenced bytests/e2e/worktree-lineage-*.tsbut exists nowhere inc62eca3988. CI'spnpm typecheckruns node/cli/web/mobile-web only, so this is upstream's own defect and outside the gate.Backup
origin/backup/main-20260921-225928Zate83b210e49.