interim - #102
Draft
alichherawalla wants to merge 785 commits into
Draft
interim#102alichherawalla wants to merge 785 commits into
alichherawalla wants to merge 785 commits into
Conversation
… able to fail the search Two defects in the degradation slice committed as 8bf3675, plus the groundwork for a third that needs a line in another owner's file. AN OBSOLETE REPLY COULD CLEAR A LIVE DEGRADATION. `semanticHits` awaited the source pass with no claim check after it, so a vector reply belonging to a query the user had already typed past could return, look like success, and clear a standing "semantic search is not running" that was still true for the query actually in flight. The claim is now checked after the last await, before anything is reported - and on the FAILURE side too, which the report did not name but has the identical shape: a fault raised for a dead query would have published a degradation nobody was waiting for. Superseded now takes precedence over the fault and rethrows. No answer from a dead query publishes anything, success or failure. REPORTING COULD FAIL THE SEARCH IT WAS ONLY OBSERVING. The previous comment claimed the reporter could never fail the caller; that was false, and false in committed code. Its catch called the diagnostic writer, which can itself throw through its stream - and the fault branch logged BEFORE returning the fallback, so a throwing logger destroyed keyword results that were perfectly good. Now one guard around the whole reporting body, no nesting, and the catch is silent rather than logging through the thing that just threw - the same answer `stream-guards.ts` already gives for a throwing console stream, and for the same stated reason. The degradation report is ordered BEFORE the diagnostic, so a throwing logger cannot cost the user the visible signal. The comment now separates what is guaranteed from what is attempted: the caller always gets its result; the projection report and the log line are attempts; if both fail the degradation is lost, and that trade is stated rather than implied. Still `{ reason }` only - no query text, no truncation, no hash, with a comment saying why so it does not regress. BOUNDED OBSOLETE WORK, no retries: a checkpoint after the vector read, which was the longest unguarded stretch and gated N per-hit probes for a dead query, and one after the source pass. Plus `claim.release()` in a `finally`, so the per-stream map is bounded by IN-FLIGHT streams rather than growing for the lifetime of the process - guarded so only the newest holder deletes, and an older request cannot erase a live token. No backoff, no scheduler, no retry loop. THE PREMISE I WAS GIVEN WAS WRONG, AND THE TRUTH IS WORSE. The stream identity is not a global constant here - `claimStream` takes it as a parameter and the map is keyed by it, so this file hardcodes nothing. But NO production caller passes one: not the Pro `search:universal` handler, not the command palette's path, not the tools or IPC one-shots. So the supersede machinery documented in this file is unreachable from the UI and exercised only by tests, and with two windows the bug is not that they cancel each other - it is that neither cancels anything. `searchStreamId(surface, senderId)` is exported here, pure and Electron-free, beside the map that consumes it so the two cannot drift; the handler line that would use it belongs to another owner and is being requested, not edited. Gates: focused eslint 0 · no typecheck diagnostic names this file · verify-model-architecture 0. Code-gated only, nothing executed. The multi-window case CANNOT be demonstrated by reading this file and is NOT fixed until that handler line lands. Claude-Session: https://claude.ai/code/session_01RwwvfNHkF7ohUnbpZ75oZu
…ry beside it Three defects in the reconciliation committed as d2dad8a. THE COMPUTER USE READ COULD STILL SUPPRESS A VALID TEXT & VISION LIST. Moving its state assignment earlier decoupled nothing: both reads were one `Promise.all`, so a rejection from the optional computer-use read rejected the combined promise and a perfectly good model-control projection was never applied. The reported defect surviving through a different door. They are two independent reads now, still started together so they still run concurrently, each with its own failure state - so neither can block or suppress the other. And no guessed empty array: an unread computer-use section says it is unavailable rather than collapsing into "nothing selected", which is the same argument the main inventory already makes, applied one level down. A REJECTED IPC STILL ESCAPED TWO HANDLERS. `choose` and `chooseComputerUse` had `try/finally` with no `catch`, and the control client calls the bridge directly - so a missing bridge, a dead main process or a serialization failure rejected out of an async function invoked from a void click handler. `finally` cleared the spinner and the rejection went unhandled: the user saw the spinner stop and nothing else, indistinguishable from a successful no-op. The same shape already fixed in `load`, two functions away. Both now surface the transport failure inline. `Outcome.ok` DID NOT MEAN THE UNLOAD HAPPENED. `ok` means the command was carried and answered; only one of the five success statuses means the model left memory, and the code asserted that meaning for all of them - so a non-completed unload would have reported "Unloaded" beside a still-resident model. Labelled honestly: this is HARDENING, not a live user-visible fix. The unload path returns `completed` on its only success today, so no other status can currently arrive. The type permitted five and the code assumed one, which is worth closing on its own terms without claiming a bug the user could hit. Unchanged: no visual redesign, no new UX surface - the computer-use error reuses that section's existing paragraph and the unload fix reuses a state value the map already had. The memory-confirmation UI is still unbuilt and still the recorded gap: a model whose memory advice is `caution` returns `confirmation_required`, which this panel now reconciles and reports but cannot confirm through. Gates: focused eslint 0 · no typecheck diagnostic names this file · verify-model-architecture 0. No test suite was run. Code-gated only: the app was not run, and the first live step worth taking is to make the computer-use read reject and confirm the Text & Vision list still populates. Claude-Session: https://claude.ai/code/session_01RwwvfNHkF7ohUnbpZ75oZu
Two defects, one of them introduced by the previous fix in this file. PENDING WAS BEING RENDERED AS FAILURE. Distinguishing "unread" from "empty" left a third state unnamed: on first render the inventory was neither read nor failed, and the panel asserted the models could not be read while the request was simply in flight. That fired on EVERY open for EVERY user, for as long as the read took - the original defect wearing the opposite face, since an unread list had claimed to be empty and a pending list now claimed to have failed. A boolean could not hold three states, so the section read is a discriminated union of pending, read and unavailable: only a projection that actually arrived makes "nothing downloaded" true, and only a real refusal or rejection makes "unavailable" true. Pending uses the spinner this file already uses elsewhere, not a new affordance. Computer Use had the same shape and gets the same treatment - it no longer flashes "no model is selected" before its read lands. A STALE REFRESH COULD REVERT A NEWER COMMAND. The mount refresh is issued before the user does anything but can resolve after an activate or unload has applied its own fresher projection, and the projection was written unconditionally - so the panel reverted to the pre-command state, which reads to a user exactly like the original defect: a click that appears to do nothing, or an active row that flips back on its own. Ownership is expressed with `operationId`, the API's OWN correlation field - accepted on every intent, honoured by the control owner when supplied, and echoed on every success - rather than a revision the projection does not carry or an ordering manufactured client-side. One ref holds the id allowed to write; an answer that no longer owns the store is dropped entirely, checking the ECHOED id so the guard proves the answer belongs to our request. One store with a rule about who may write to it: no second store, no parallel cache, no diffing. Two deliberate scoping decisions, because a blanket guard would have been wrong. Per-modality unload status is NOT the shared store - an unload of Voice is not superseded by an activate of Text, and guarding it globally would have stranded that modality's spinner on "unloading" forever. And `busy` is per-request now, since a stale answer clearing a newer request's spinner is the same stale-answer bug one layer down. Both are reachable, not latent - the first on every panel open, the second whenever a user clicks inside the catalog read's window, which widens toward its ten-second bound. Gates: focused eslint 0 · no typecheck diagnostic names this file · verify-model-architecture 0 · no test suites run. Code-gated only: the app was not run. The first live step is to slow the catalog read and confirm a spinner appears rather than the failure claim, then select a model mid-read and confirm the selection sticks. Claude-Session: https://claude.ai/code/session_01RwwvfNHkF7ohUnbpZ75oZu
… later microtask The callee checked the claim before resolving, which looked sufficient and is not. The caller resumes in a LATER MICROTASK, and another request can claim the stream in that gap - so a check that passed inside `semanticHits` can already be stale by the time `semanticHitsOrDegrade` publishes on it. The check now happens immediately after the await, before the success report, exactly as the failure path already does. The callee's check stays: it protects its own boundary, which is a different question from whether the caller may publish. What this prevents is the worst version of a stale answer, not merely a useless one: clearing a degradation that is still TRUE for the query now in flight. A user whose semantic search is genuinely not running would have seen the indicator disappear because a superseded query happened to succeed. Gates: focused eslint 0 · no typecheck diagnostic names this file. Code-gated only; a microtask race is exactly what source review cannot demonstrate. Claude-Session: https://claude.ai/code/session_01RwwvfNHkF7ohUnbpZ75oZu
Readiness was assembled per surface from whatever each could reach, so the shell, the permission gate and the settings screen could disagree about whether setup was finished. They now read the one shared projection through a narrow host-facts adapter, so there is a single answer and the surfaces render it rather than deriving it. The behaviour this fixes: a device with a paired Sync peer or a saved usable remote route is set up, whether or not it can reach anything at this moment. Treating unreachable as unconfigured meant a working install could be asked to onboard again after a network blip - the least forgivable time to demand setup, because the user has already done it. Post-onboarding resource mode is reachable from Settings rather than existing only during the first-run flow, so the choice can be revisited by someone who now knows what their machine does. The setup panel also observes a refusal, offers a retry, and cannot be overwritten by a stale initialization - the same in-flight ownership rule applied across this round, since an initialization that resolves late must not overwrite what the user has since done. Gates: typecheck:node and typecheck:web carry ZERO production diagnostics · verify-model-architecture 0 with zero temporary entries · the startup consumer contract passes 0/6 packages. Code-reviewed and source-gated only. No onboarding journey has been run, and readiness is precisely the kind of thing that only a real first launch proves: a fresh profile, a paired-but-offline device, and a configured install after a network drop are three separate live checks that have not happened. Claude-Session: https://claude.ai/code/session_01RwwvfNHkF7ohUnbpZ75oZu
`predev` verified the Shared artifacts, and the other ways this app actually starts did not - the gateway and start paths, and the direct platform builds, all went straight past it. A gate on one entry point is a gate people route around without meaning to, and the unguarded paths here were the consequential ones: the direct builds produce something shippable, so the check was absent exactly where a stale or missing Shared artifact costs the most. The same existing verifier is prepended to each path. Deliberately not a second variant: two checks that can disagree are worse than one that is sometimes missed, because then neither can be trusted. No build or test is executed by this change - the verifier is read-only, and it stays the single canonical check rather than gaining a launch-specific twin. Gates: the verifier passes 0/6 packages. Not a build claim: no packaged or production build has been run, and this only ensures the check happens when one is. Claude-Session: https://claude.ai/code/session_01RwwvfNHkF7ohUnbpZ75oZu
…s to Connector auth keyed work by provider rather than by the account inside it, so a second Google account was not a second account - it was the first one overwritten. Subject identity now distinguishes them, and credential guards cover the whole operation rather than only its start, so an in-flight generation cannot be completed against a different account than the one it began with. That is the substance of the multiple-account journey: not more rows in a list, but every stored token, every refresh and every read naming which account it belongs to, so two accounts can coexist without silently becoming one. Gates: typecheck:node and typecheck:web carry zero production diagnostics · focused lint clean. Code-reviewed and source-gated. No account has been connected, no token issued or refreshed, and no second account added - all of which are live steps, and the ones that matter most here, since an identity mistake is invisible until two real accounts exist. Claude-Session: https://claude.ai/code/session_01RwwvfNHkF7ohUnbpZ75oZu
A native OCR read had no bound, so a helper that stopped answering took the caller with it - and the caller here is the path meeting evidence and capture depend on, so one wedged read stalled work that had nothing to do with it. The read is bounded and typed now: a timeout is an outcome the caller can see and act on, rather than a promise that never settles. The Swift dispatch side is corrected to match, so the helper and its caller agree about what finishing means. Gates: typecheck:node and typecheck:web carry zero production diagnostics. Code-reviewed and source-gated: no extraction has been run, and a timeout path in particular is only proved by making the helper actually stop answering. Claude-Session: https://claude.ai/code/session_01RwwvfNHkF7ohUnbpZ75oZu
Child commits first, then the parent gitlink, so the parent never references a commit that does not exist yet. Picks up pro `0e04244` (meeting end detection, live frames, consent controls, and the search stream wiring released in the same file) and `805b8ee` (per-account Google identity, deterministic equal-time ordering, whole-operation credential guards). Deliberately NOT included: `resources/bin/text-extractor`, a tracked generated binary that a development helper refresh left dirty. Whether a rebuilt artifact belongs in a source commit is a repository policy question, not something to answer by sweeping it in with unrelated work. Claude-Session: https://claude.ai/code/session_01RwwvfNHkF7ohUnbpZ75oZu
…erywhere Five copies of `'Alt+Space'` existed - the onboarding tour, the Pro catalogue, the dictation overlay, the Voice screen, and the controller that actually registers the hotkey. Exporting a sixth constant would have been the wrong fix; the point is that the string the app ADVERTISES and the string it REGISTERS must be the same one, not four that happen to match. `src/shared/dictation-defaults.ts` is that owner. It sits in the shared layer for a concrete reason rather than by convention: the open build cannot import from the optional private Pro submodule, while Pro imports `@offgrid/core/shared/*` freely - and that asymmetry is exactly why the copies appeared. All five sites, Pro's controller included, now read from it. Onboarding was the last hardcoded "Hold Option+Space", telling a Windows or Linux reader to hold a key their keyboard has no name for - the same defect the shared formatter was written to fix, pointed the other way. Both static open-build surfaces now format the default through it. Where a surface can read the user's CONFIGURED chord it does, and the default is only its fallback - the overlay and the Voice screen both prefer the real setting. The two static surfaces cannot reach settings, so a formatted default is the honest best available there, which is stated in the module rather than left for someone to discover. The key code and modifier live beside the accelerator because they are the same chord in the encoding the native helper needs, and they are deliberately literal rather than derived - with a note that they change together, since a mismatch between them is the failure this consolidation exists to prevent. Gates: typecheck:node and typecheck:web carry ZERO production diagnostics · focused eslint 0 · verify-model-architecture 0 · startup consumer contract 0/6 · git diff --check 0. Code-gated only: nothing was launched. The non-mac spelling is derived from the formatter's modifier table and has not been seen rendered on Windows or Linux, and a rebound chord has not been seen displayed. Claude-Session: https://claude.ai/code/session_01RwwvfNHkF7ohUnbpZ75oZu
Child first, then the parent gitlink. Picks up pro b1f7422, so the registering controller and the four display surfaces share one owner for the default chord. Claude-Session: https://claude.ai/code/session_01RwwvfNHkF7ohUnbpZ75oZu
CI resolves Pro and Shared by branch with a fallback rather than by verified commit, and the apps and Shared are on different branches right now - so a branch lookup selects the wrong Shared source, and no green run exists against any head reviewed here. Recorded as a release-gate task rather than started, because editing release YAML now would produce evidence about a pipeline nobody has run. The Pro sink comment about library refusals arriving as runtime is still accurate, so it stays until the typed arm it describes is actually integrated. Removing it early would assert a capability the tree does not have, which is the same defect inverted. Claude-Session: https://claude.ai/code/session_01RwwvfNHkF7ohUnbpZ75oZu
…the wrong one
A fresh isolated profile showed five interrupted downloads and a zero-byte model it had never seen.
Nothing had leaked into the profile - the app was reading the REAL machine's model directory, because
`desktopModelDownloads` resolved `llm.getModelsDir()` at module evaluation, which happens on first
import of the composition root, before `app.setPath('userData', ...)` ran. Database isolation was
never the question; a store captured a path before the path was decided.
The hazard was already documented fourteen lines from the violation: `llm.ts` resolves its own
active-model path lazily and its comment names `OFFGRID_USER_DATA` as the reason. So the rule existed
and nothing enforced it. The entry is now thin - stdout, protocol, safeStorage, product name and
userData are established, and only then is the application graph imported - so no store can capture a
path that has not been set. An explicit override that cannot be applied FAILS CLOSED rather than
quietly continuing in the wrong directory.
Worth stating because of what it nearly cost: the leak was read-only only because no retry or clear
was clicked. Either would have written to real user state from a profile the operator believed was
isolated.
Startup also stopped competing with itself. `models.text.prepare` and the demo seeder were both fired
un-awaited, and the seeder issued its own prepare - so the newest-wins per-modality lane refused the
startup stage as `superseded`. One named stage runner now serves both the headless and windowed
paths, and the seeder waits on its SETTLED result instead of racing it, so its own deadline measures
seeding rather than a model load. It takes the facade from the composition-root module rather than
the access proxy, because that proxy throws until the root is registered and this stage runs
concurrently with `application.start`.
An empty profile is now a first-class answer rather than an error: no selection returns `unconfigured`
and startup succeeds, while a malformed or offline SELECTED route still reaches prepare and keeps its
failure. Onboarding completing is not a prepared model, and the seeder treats those differently -
curated artifacts unless a model is genuinely prepared and ready. The seeder also stopped claiming
success it did not have: outer failures propagate, knowledge-indexing failures aggregate, and the
completion marker resets before replacement so a failed forced re-seed cannot leave an old success
standing over partial data.
An unexpected ready-callback rejection is logged and routed to the normal quit path instead of
escaping. The architecture gate's queue-owner guard now covers both the old entry and the moved one -
a filename-keyed check silently stops protecting anything when the code moves, which is the failure
mode that passes for the wrong reason.
`src/main/env.d.ts` restores the `?asset` ambient types for the web program, which had excluded the
one entry that used them; a reference file rather than a `types` array, since that array would replace
automatic `@types` resolution for the very files it exists to type.
Gates: typecheck:node and typecheck:web both ZERO production diagnostics · verify-model-architecture 0
with zero temporary entries · startup consumer contract 0/6 · focused eslint 0 with no limit raised or
suppressed.
Live evidence, precisely: a fresh isolated profile completed onboarding through the real UI and
reached /models with no failure banner, storage confined to the temp profile. That exercised the
UNCONFIGURED path - no text model was selected or downloaded - so the prepared branch, live seeding
and `active.ready` remain unobserved, as does anything Pro-gated in a profile with no licence.
Claude-Session: https://claude.ai/code/session_01RwwvfNHkF7ohUnbpZ75oZu
Some valid PDFs failed to extract with `bad XRef entry` while others succeeded, which read like a malformed-file problem and is not one. The same bytes parse or fail depending on the TYPE handed to `pdf-parse`: a 610-byte PDF with a classic cross-reference table fails as a `Buffer` and succeeds as a plain `Uint8Array`, while a 12 KB PDF parses either way. `fs.promises.readFile` returns a Buffer, so every document through this bridge was taking the failing form. That selectivity is why it went unnoticed and why it matters: this is not a demo-fixture defect, it is user PDFs of a particular shape silently failing to index. The file that exposed it was byte-identical across a failing run and a passing one - proof it was never the producer - and four fresh reads at different slab offsets failed identically, so it is the type rather than an offset. The signature says `Uint8Array` now rather than `Buffer`, so the constraint is stated where a caller will see it instead of living in a comment nobody reads before changing this line back. Nothing else changed: no fixture edit, no dependency change, no version pin. The PDF producer is untouched, deliberately. Gates: focused eslint 0 · no typecheck diagnostic names this file. Live evidence, narrow and real: after this change a fresh isolated profile indexed all three seeded documents including the PDF, where the same profile previously failed that one file. That confirms the fix for this shape of PDF; it does not establish which other real-world PDFs were affected, and I have not surveyed that. Claude-Session: https://claude.ai/code/session_01RwwvfNHkF7ohUnbpZ75oZu
…istered A profile without Pro logged repeated unregistered `meeting:get-state` and `capture:self-view` calls. The renderer asked for meeting state and reported self-view unconditionally, while those handlers only exist when Pro is active - so every free install produced a stream of failures for work it was never entitled to do, and real failures had to be found among them. Both callers now use the activation result the app already computes - `isPro && proReady && proActivation === 'full'` - rather than trying and discarding the error. Asking a question only when someone can answer it is the fix; catching the rejection would have kept the noise and merely hidden it. The self-view call also loses an `as any` and a discarded promise. It was cast to reach an optional method that the typed bridge does exposes, and its rejection went nowhere - so a genuine reporting failure was indistinguishable from the entitlement noise. It is typed and its failure is logged now, and the effect re-runs when activation changes rather than only when the view mode does, so a profile that becomes entitled mid-session starts reporting without a reload. The recorder hook gained cleanup and late-reply invalidation in the same slice: a reply from a request issued before teardown, or before a newer one, can no longer publish into state nobody is watching - the same stale-answer rule applied across this round. Gates: focused eslint 0 · no typecheck diagnostic names either file · production diagnostics remain 0 in both Desktop projects. Code-gated only. The isolated profile has NO Pro licence, so the entitled branch has never run: what a live check showed is the absence of the unregistered-handler noise, not that meeting state or self-view reporting works. Those need a licensed profile. Claude-Session: https://claude.ai/code/session_01RwwvfNHkF7ohUnbpZ75oZu
Four surfaces told every reader to hold the DEFAULT chord, so a user who rebound dictation was given instructions that did not work on their own machine - and the copy could not know better, because the static catalogue and the onboarding tour are evaluated at import time in the OPEN build, where the configured value lives behind Pro IPC. `useDictationShortcut` reads the configured value at render instead, and the surfaces project it. The static catalogue keeps describing the canonical default and exposes a projection for the runtime copy, so nothing calls a hook from a module-scope array. The read is careful in ways worth keeping. A generation counter drops a stale reply, so a slower earlier read cannot overwrite a newer one - the same stale-answer rule applied across this round. It re-reads on window focus, so a chord rebound in another window is reflected without a reload. It carries its own timeout, and on any failure it says the shortcut is unavailable and points at Voice rather than silently showing a default that may be wrong. And it states plainly what it is NOT: a read of the configured value, not proof the hotkey is registered or that the user is entitled to it. Every rendering still goes through the shared spelling, so a Windows or Linux reader is never told to hold a key their keyboard has no name for. Gates: focused eslint 0 across all five files · no typecheck diagnostic names any of them · typecheck:node carries no production diagnostic. NOT claiming a clean tree: `typecheck:web` currently has 2 production errors, both `ModelsScreen.tsx` `refreshLane` used before declaration, from another worker's in-flight diff that is deliberately not committed here. Code-gated only: no rebound chord has been observed rendering, on any platform, and the unavailable path has not been exercised. Claude-Session: https://claude.ai/code/session_01RwwvfNHkF7ohUnbpZ75oZu
`control({type:'activate'})` answers `confirmation_required` for a model whose memory advice is
`caution`, and this picker had nowhere to confirm it - so after the earlier fix it reconciled and
reported honestly, and the model still could not be loaded from this surface. It was the only models
surface missing the flow; the Models screen and the storage panel both had it.
The notice follows the storage panel's shape rather than the Models screen's native dialog, and
renders the domain's own advice verbatim. Declining issues no command at all.
The ownership questions this raised are the substance of the change, because a confirmation is a
SECOND command and consent is only meaningful against the intent that produced it:
Stale consent is retired at ISSUE, not at reply. `setConfirmation(null)` lives inside `claimControl`,
so any newer command - choose, unload, confirm, refresh - retires a pending notice the moment it is
issued, whether or not it later succeeds. The hole this closes was real and reachable: previously
`unload` never cleared the notice and `choose` cleared it only on reply, so a user could be shown a
confirmation for A, click B, have B refused, and still accept a confirmation for a model they had
moved on from - consent given for a different question.
The confirm leg takes a FRESH claim. Reusing the activate's id would let an answer to the
already-completed activate publish; the single-use `confirmationId` survives the new claim because it
travels by value in the call rather than being read back from state.
Busy now separates display from identity in one value: the row spinner keys off the model, the
`finally` clears by the request's own operationId. Keying cleanup on the model collapsed two requests
for the same model into one identity, so a double-click let the first reply strand the second - the
same stale-answer defect narrowed to the case a user produces most easily.
The confirmation flow pushed the component past its line limit. Rather than raise or suppress the
gate, four cohesive pieces were extracted as mechanical moves within the file - same markup, same
behaviour. More structural churn than a fix like this ideally carries, and recorded as such.
Gates: focused eslint 0 · ZERO production diagnostics in typecheck:node and typecheck:web at this
commit · verify-model-architecture 0 · git diff --check 0. That web count is measured with another
worker's `ModelsScreen.tsx` still dirty in the tree and deliberately not committed here.
Code-gated only, and every claim above is an ORDERING property that typecheck cannot prove and the
app was not run to observe. A live check needs a model large enough to trigger `caution`: accept and
confirm it loads and the active row reconciles, decline and confirm nothing changes, then click A,
get the notice, click B, let B be refused, and confirm A's notice is gone rather than still
acceptable.
Claude-Session: https://claude.ai/code/session_01RwwvfNHkF7ohUnbpZ75oZu
Two defects, and the second was a design error in how I briefed the first. The screen narrowed on `status === 'completed'` before applying a projection, at five separate call sites - refresh, cancel-download, download, remove, activate - and a sixth path returned early when a user DECLINED the confirmation, leaving the pre-click state on screen behind a dialog they had just dismissed. Every `ModelControlSuccess` variant carries a fresh projection, and the two non-completed ones carry their own typed failure, so nothing had to be narrowed and no reason string had to be invented. A model that installs but does not activate now moves the grid to real state and says why in the engine's own words, instead of leaving the previous model checked and silent. Then the harder half. Applying at six sites widened a race that was already there: replies could publish in any order, so a slow refresh could land after a switch and revert it. The first attempt gave each command its own lane, which made every writer internally consistent and left the shared projection exactly as racy as before - an old refresh owned its own lane, passed its own guard, and stomped a newer switch anyway. Per-writer ownership is the wrong model when the writers share one piece of state. Two questions, separated. WHETHER A REPLY MAY PUBLISH is one screen-wide authority: every command claims it at issue, and a reply publishes only if its ECHOED operation id is still the newest intent, whichever command produced it. WHETHER A REPLY MAY CLEAN UP ITS OWN spinner or notice stays per-operation, deliberately - a background refresh holding publication must not also gate `setSwitching(null)`, or a newer refresh strands the switch spinner forever. One consequence recorded in the code rather than left to be rediscovered: a download completing while a newer command holds publication has its projection refused, and that is not lost work - the download-completed subscription calls refresh, which claims the authority itself and republishes. The first version of this fix could never have run: an ownership hook was referenced in a `useCallback` dependency array above its own `const`, which throws at render. Typecheck did catch it, as TS2448/TS2454, and it was caught by a canonical run rather than by the greps I had been repeating - which is the more useful lesson. Gates: focused eslint 0 errors with the warning profile IDENTICAL to baseline - 44 pre-existing prettier warnings, none in changed lines · ZERO production diagnostics in typecheck:node and typecheck:web · verify-model-architecture 0 · git diff --check 0. Code-gated only, and the limitation is not theoretical here: a typecheck-green render crash shipped in the previous iteration of this same file. Nothing has been rendered. `installed_not_active` and `projector_installed_not_ready` remain unexercised - both need main-process conditions unreachable from source. First live step is simply mounting this screen; nothing else matters if that fails. Claude-Session: https://claude.ai/code/session_01RwwvfNHkF7ohUnbpZ75oZu
…refusal The first is a consequence of the publication authority rather than a flaw in it: a mutation that settles after losing publication has no completion event to correct the screen, so unlike the download path its refusal is permanent. Recorded with the constraint that the discarded projection must not be re-applied - it is stale by definition, which is why it lost. The second is the unread-versus-empty distinction the picker surface already draws and the Models screen still does not, on the surface a user checks first when they think something is missing. Claude-Session: https://claude.ai/code/session_01RwwvfNHkF7ohUnbpZ75oZu
…imestamp Pairs with the Shared provenance publisher. The gate that guards every launch path now validates the ordered build's content proof - inputs, artifacts and resolved dependency bindings - rather than comparing modification times. The existing runtime-export and declaration checks are unchanged and still run. This closes the gap recorded when the gate was last repaired: mtime could not see a package rebuilt against a stale dependency, so the most dangerous state during a multi-package migration was the one it reported as healthy. It is READ-ONLY and it FAILS CLOSED. Verified: with no proof present it exits 1 and names the missing provenance file with the remedy, rather than passing because it could not tell - which is the specific defect this gate had before and the reason it is worth being strict now. OPERATIONAL CONSEQUENCE, stated because it will surprise someone: this gate is wired into eight launch paths, and no proof exists in any tree yet. Until an ordered `npm run build` in shared/ publishes one, every one of those paths refuses. That is the intended behaviour, not a regression - but it means the next person to run dev must build shared first, and the error says so. Gates: `node --check` · focused eslint 0 with no warnings · gate exits 1 with no proof, as designed. No build was run and no proof was generated or hand-written. The first genuine evidence is a successful ordered artifact refresh, still outstanding. Claude-Session: https://claude.ai/code/session_01RwwvfNHkF7ohUnbpZ75oZu
Two ways this screen reported a state the system was not in. The refresh failure wrote to `switchError` under the refresh lane, so an old refresh could overwrite the notice about an activation the user had just performed - losing the message they needed in favour of a stale one - and a later SUCCESSFUL refresh never cleared it, because the success path did not touch that state. The screen went on saying your models could not be read after a retry had read them perfectly well. It has its own state and its own strip now, cleared on a successful refresh that still owns its lane. Deliberately gated on the LANE rather than publication: whether a newer intent wins the projection has no bearing on whether this particular inventory read worked. The refused cancel was worse, because the first fix traded one false claim for another. It had optimistically erased the row before the cancel was accepted - so a refused cancel left the user believing they had stopped a download that was still running. Marking the row `failed` instead fixed the erasure and introduced a different lie: the COMMAND failed, the transfer did not, and the bytes were still arriving under a row that said otherwise. Now the transfer's own status, failure kind, percent and byte counts are left alone, and the cancel command's failure travels in one added field on the row's existing shape - no second store, no second source of download state. The row keeps painting what the download is actually doing, with the command's refusal beside it, and it is removed only when a cancel is genuinely accepted. Gates: focused eslint 0 errors with the warning profile identical to baseline - 44 prettier and 6 structural, none in changed lines · ZERO production diagnostics in typecheck:node and typecheck:web · verify-model-architecture 0 · git diff --check 0. Code-gated only: nothing here has been rendered. Mounting this screen remains the first live step, because a typecheck-green render crash shipped from this file once already and no gate I can run would catch a second. `installed_not_active` and `projector_installed_not_ready` are still unexercised - both need main-process conditions unreachable from source. Claude-Session: https://claude.ai/code/session_01RwwvfNHkF7ohUnbpZ75oZu
… rest is fine The strip told users things that were not true, in three ways. It printed the raw internal domain identifier, so someone whose semantic search had degraded read "rag is unavailable." A codeword, in the one place the app explains itself when something is wrong. It read only the FIRST degraded entry, so a second was silently dropped - and because startup stages publish theirs during boot while runtime degradations arrive later, the entry a user could actually act on was the one most likely hidden. Every degraded domain is now named in plain words, appended after any stage message rather than replacing it, and de-duplicated by label because the list is keyed by REPORTER, so two reporters of one domain would otherwise read "Search and Search". And it said "unavailable" for a state the contract defines as "up but not whole". That is provably wrong rather than a matter of taste: a `rag` degradation is published when the SEMANTIC half of search cannot run, and the keyword half still returns real results - so a user told search was unavailable could search and get hits. LIMITED is true for every domain by that same definition, and it is deliberately uniform: the snapshot says a domain is reduced, never which capability of it, so no per-domain fallback claim is invented for `sync`, `speech`, `automation` or `use`. "Everything else works." is dropped rather than softened. The strip is handed a list of what IS degraded; it is never told that nothing else is, so any version of that sentence is a claim about the whole application made from a list of its problems. Deliberately NOT rendering each entry's `reason`: that field is reporter-authored free text, so showing it raw would reintroduce the codeword defect this fixes. A curated mapping would need an audit of every `reportDegraded` call site. Two decisions left as decisions rather than guessed: a `late` stage still leads the sentence ahead of a live outage, and `rag` reads as "Search" here while `STAGE_LABELS` calls the same domain "Opening your knowledge base". Both recorded; neither encoded as a hierarchy nobody asked for. Gates: focused eslint 0 errors and 0 warnings · zero production diagnostics in typecheck:node and typecheck:web. Code-gated only: no degradation has been rendered. Two simultaneous entries in particular need a failing embedding model on top of a startup degradation, which was not staged. Claude-Session: https://claude.ai/code/session_01RwwvfNHkF7ohUnbpZ75oZu
`setSettings` mutated `performanceMode`, the runtime fields and `userExplicit` before persisting, so a failed write left the rejected value in memory and `getSettings` reported it as current. The user was told the save failed and the app went on behaving as though it had succeeded. That contradiction was already visible from the other end: the setup panel treats a refusal as a value and reports "resource-mode persistence" failure, with its own comment stating that a refusal must not resolve as a success that stored nothing. The caller was honest; the owner was not. Now the raw mutable snapshot covers the patch fields, the derived public settings are preserved, pins are restored on failure, and nothing is emitted until the write lands - no sync, no restart on a rejected save, so nothing downstream acts on a value that was never stored. The file itself is replaced through a unique exclusive temp and a rename, so a failed write leaves the previous JSON intact rather than a truncated one. Gates: focused eslint 0 · zero production diagnostics in typecheck:node and typecheck:web. Code-gated only: no rejected save has been produced, and the rollback path is argued from source rather than observed. Claude-Session: https://claude.ai/code/session_01RwwvfNHkF7ohUnbpZ75oZu
…e saved, not when a code arrives The pending registration was resolved on ARRIVAL OF THE AUTHORIZATION CODE, while `finishAuth` still had to exchange it and save the tokens. Everything between those two moments ran against a registration the app already considered finished - so a user who removed or reconnected the connector in that window could have a token save land against the registration they had just torn down. Stale credentials, written after the user asked for them to be gone. The lifetime now ends where it should: cancellation runs through commit and through a failure `finally`, so the registration is released once the exchange has actually settled either way, and a removal during the exchange is honoured rather than raced. Preserved deliberately: the PKCE `invalid_client` retries, and background refresh. Both are legitimate behaviour that a narrower "cancel everything on removal" would have broken, and neither is what this fixes. Gates: focused eslint 0 · zero production diagnostics in typecheck:node and typecheck:web. Code-gated only. The race has NOT been reproduced - it needs a removal timed inside the exchange window - so the ordering is argued from the lifetime boundary, not observed. The entitled paths remain unexercised: the isolated profile has no Pro licence and no account has been connected. Claude-Session: https://claude.ai/code/session_01RwwvfNHkF7ohUnbpZ75oZu
…s on Pairs with the Shared provenance change. The gate now hands the verifier the exact set of contract packages Desktop requires, derived from the same contract table it already checks symbols against - one source of truth for both questions, so the set of names verified for exports cannot drift from the set verified for provenance. Clipboard stays Desktop-local by intent and is not in that set. Gates: `node --check` · focused eslint 0 · gate exit 0 against the refreshed Shared artifacts. Claude-Session: https://claude.ai/code/session_01RwwvfNHkF7ohUnbpZ75oZu
…until the user retests Three confirmed defects with owners, recorded before the fixes so the record shows what the user saw rather than what the fix claims. They compound: duplicate legacy and canonical manifest rows create two models where there is one, the storage refresh paints live progress onto every row sharing an id so the stale one reads as running, and an identity mismatch hides a present model from the text list while the vision list shows it. Recorded with the standing rule that none of it is reproduced or verified by touching the user's downloads, registry or model files. Claude-Session: https://claude.ai/code/session_01RwwvfNHkF7ohUnbpZ75oZu
…st housekeeping note A missing device cap is reported as a failed registration when nothing was registered - a refusal to start wearing a downstream failure's label, in the same function that documents the identical mistake six lines later. Found by a test, not by manual testing; recorded rather than fixed because it is production source outside the test phase. The dist note is deliberately narrow: current chunks carry the new contract, an old chunk lingers, and the ambiguity is worth removing rather than the contract being stale. Claude-Session: https://claude.ai/code/session_01RwwvfNHkF7ohUnbpZ75oZu
Register-without-unregister leaves a stopped application behind the singleton proxy, which every lifecycle test papers over by registering the next one; and the CRM channel name lives in two production files, one of which a renderer test cannot import without pulling electron into jsdom. Both found while repairing tests, both recorded rather than fixed in the test phase. Claude-Session: https://claude.ai/code/session_01RwwvfNHkF7ohUnbpZ75oZu
…ement gate tests must respect The CRM projection hook publishes after unmount two ways - a listener with no cancelled-guard, which the test catches, and an uncancelled catch-up read reachable through the real transport, which it does not. The correct pattern is one file away. Recorded as production with the assertion left red. The recorder note exists so nobody stubs their way past a second gate and calls it green. Claude-Session: https://claude.ai/code/session_01RwwvfNHkF7ohUnbpZ75oZu
…ves in production `loadProFeaturesMain` dynamically imports `@offgrid/pro/main`. The production build resolves that to the real `pro/main/index.ts` when the submodule is present and to the stub when it is not (`electron.vite.config.ts:19`). The test config aliased `@offgrid/pro` and `@offgrid/pro/renderer` to the stub and left `/main` unresolved - so under test the import threw ERR_MODULE_NOT_FOUND, which `loadProFeaturesMainNow` correctly reports as a Pro activation failure whenever Pro is enabled. Every entitlement-gain test therefore logged an activation failure that no production build can produce - 111 lines in one run of two files, and two tests blocked outright. The alias now mirrors production exactly. A real Off Grid module when we have it, per the testing doctrine; the stub only where production would also have the stub. What it did, measured: the two blocked files' activation-failure noise fell 111 -> 43, and the module now LOADS - which surfaces the real residual those tests were hiding behind the resolution error: the real `pro/main` graph imports `BrowserWindow` from `electron`, and under `ELECTRON_RUN_AS_NODE` the per-file electron mocks in those two tests do not provide it. That is a mock-surface repair in the test files, owned by the test worker, and it is the same root cause as the eleven CRM suites whose mocks predate `ipcMain` being reached at application construction. Verified not to regress core: the one core file that also fails (`diagnostics-log.integration.test.ts`, 3 tests) fails identically with the alias removed - pre-existing, recorded separately. Gates: focused eslint 0. No production semantics changed, nothing masked, no threshold or exclusion touched. Nothing under shared/ touched. Claude-Session: https://claude.ai/code/session_01RwwvfNHkF7ohUnbpZ75oZu
fix(sync): consume canonical Desktop status
Route remote screen work to the selected server
fix: complete session reliability and sync updates
fix: include specialist selection projection
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueThanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
alichherawalla
force-pushed
the
main
branch
from
September 17, 2026 21:35
be69623 to
46b0c83
Compare
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.



Interim snapshot preserved before the pre-migration recovery branch.