Skip to content

Agent Dashboard: selected sidebar entry, pop-out settings menu - #2

Merged
nplez1 merged 11 commits into
nplez1/mainfrom
nplez1/Agent-Dashboard
Sep 18, 2026
Merged

nplez1 merged 11 commits into
nplez1/mainfrom
nplez1/Agent-Dashboard

Conversation

@nplez1

@nplez1 nplez1 commented Sep 18, 2026 •

Copy link
Copy Markdown
Owner
Files Added Deleted Net
Test 15 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​428 $\color{#cf222e}{\Huge{\mathbf{−}}}$​30 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​398
Prod 27 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​308 $\color{#cf222e}{\Huge{\mathbf{−}}}$​54 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​254

Two Agent Dashboard fixes, plus a fix for this fork's changed-code gate baseline.

ELI5

The sidebar button for the Agent Dashboard never looked "on", even while the board was on screen — and the board's settings were only reachable from the in-window version, so once you switched to the pop-out window there was no way back.

What Changed

The sidebar entry shows and toggles its state. It now reads "showing" from whichever surface is actually on screen: the in-window drawer's open state, or the pop-out window's (read once on mount, then followed through popoutOnOpenChanged). While showing, it renders the sidebar's active-row treatment plus aria-pressed / data-current; clicking it hides the board instead of only re-focusing it. Hiding the pop-out needed a new dashboardPopout:close channel, restricted to the trusted main renderer and gated on the experiment flag.

The pop-out has the settings menu. DashboardPopoutRoot now passes the settings menu as headerActions. The pop-out renderer already runs its own settings-synced store, so the menu is reused unchanged; its host-specific onSwitchToPopout prop became the host-agnostic onModeChange. Main owns the mode flip because it is the only place that knows whether the pop-out window exists, so switching modes hands the board over instead of losing it.

Changed-code gate baseline. The gate's local fallback hardcoded origin/main. In this fork the trunk is nplez1/main and origin/main is a stale mirror 47 commits behind it, so a two-commit branch reported 40 findings across 512 files. The fallback now honours origin/HEAD — the remote's real default branch, which resolves to origin/main on an ordinary checkout. CI passes the pull-request base SHA explicitly and is unaffected.

Why

The selected state had no single source of truth: in-window it came from the store, and the pop-out had no signal at all. Deriving it from the surface that is showing keeps both modes consistent, and the drawer-close check keys off the transition into pop-out mode rather than the mode itself — the reverse handoff opens the drawer from the main process a tick before the new mode reaches the store, so a level check would close the board the user just asked for.

Reusing the existing settings menu instead of writing a pop-out-specific one keeps one control for one setting; the only change needed was making the host-specific handoff a callback.

Testing

  • pnpm tc, pnpm lint, and pnpm run check:code-quality:changed (0 findings across the branch's 21 files, baselined on nplez1/main).
  • Unit tests: sidebar entry (both modes, both toggle directions), sidebar host (including that ordering case), settings menu, pop-out root, main-process close/handoff authority, IPC lifecycle.
  • Rendered check: tests/e2e/agent-dashboard-sidebar-toggle.spec.ts, driven over CDP with ORCA_BACKGROUND_LAUNCH=1, so no window ever reaches the screen. It walks the whole flow — in-window toggle both ways, switch to pop-out from the drawer menu, pop-out settings button present, switch back to in-window — and asserts the selected state is painted (the computed background changes), not merely announced to assistive tech.
  • One regression test covers the message-ordering case above: it fails against a level-based check and passes with the transition-based one.

The sidebar entry looked identical whether the Agent Dashboard was on
screen or not, and clicking it while the pop-out was open only re-focused
that window — there was no way to hide the board from the sidebar.

The entry now derives "showing" from the surface actually on screen: the
in-window drawer's open state, or the pop-out window's (read once on mount
via getPopoutOpen, then followed through onPopoutOpenChanged). Showing
renders the sidebar's documented active-row treatment plus aria-pressed /
data-current; clicking it hides the board again, which needed a new
dashboardPopout:close channel restricted to the trusted main renderer.

A mode change made in the experiment pane also closes a drawer it leaves
behind, so the entry can never read unselected while the board is visible.
That check keys off the transition into pop-out mode rather than the mode
itself: the reverse handoff opens the drawer from the main process a tick
before the new mode reaches the store, and a level check would close the
board the user just asked for.
The pop-out window rendered the board with no header actions, so it had no
settings button and no way to switch back to the in-window board — the
mode control only existed in the in-window drawer, which the pop-out
replaces.

The pop-out renderer already runs its own settings-synced store (popout.tsx
seeds appearance from the main process and tracks settings:changed), so the
existing menu works there unchanged: DashboardPopoutRoot now passes
AgentDashboardSettingsMenu as headerActions, and the menu's
onSwitchToPopout prop became the host-agnostic onModeChange so the in-window
host can narrow it to the pop-out direction.

Main owns the handoff because it is the only place that knows whether the
pop-out window exists: when the mode stops being "popout" while the window
is open, it closes the window and tells the trusted main renderer to take
the board over in the drawer, so switching modes never loses the board. The
same listener covers the experiment pane, which previously could leave the
pop-out open while the setting said in-window.

Verified end to end with a rendered Playwright spec driven over CDP with
ORCA_BACKGROUND_LAUNCH=1 (no window ever reaches the screen): the entry's
selected state in both modes, the pop-out's settings button, and the
in-window handoff.
The gate's local fallback hardcoded origin/main. In this fork the trunk is
nplez1/main — origin/main is a stale mirror 47 commits behind it — so every
local run diffed the fork's own trunk and reported unrelated findings
(40 findings across 512 files on a two-commit branch).

origin/HEAD is the remote's actual default branch and resolves to origin/main
on an ordinary checkout, so the fallback now honours it. CI passes the
pull-request base SHA explicitly and is unaffected.
@nplez1

nplez1 commented Sep 18, 2026

Copy link
Copy Markdown
Owner Author

Diagnosis of the failing checks. None of them come from this branch — every one reproduces on nplez1/main itself with these commits absent. Nothing here touches the Agent Dashboard, the preload dashboard/review bridges, the sidebar, or the changed-code gate.

Proven pre-existing on the trunk (run at 6a0b02fafd in a throwaway worktree, no branch changes present)

Failure Cause
test 2/8 — 4 × agent-hook-listener-grok-completion (child-session Stop/StopFailure/StopCancelled/SessionEnd) The listener diverges from upstream here (+681 lines across 9 files: descendant-events.ts, descendant-pane-state.ts, the descendant-pane-lifecycle merge). The child-session guard no longer suppresses descendant events, so the pane is reported working where the test expects no observation.
test 7/8 — 6 × worktree-rm-host-qualification local(worktrees) added deleteRemoteBranch to the worktree.rm payload; the upstream test asserts the exact option list, so it fails on the extra key (+ "deleteRemoteBranch": false).
static analysis — 6 anti-slop findings session-search-list-latency.test.ts uses shape in symbol names (×5) and AgentSessionSearchSection.test.ts uses Reflect.apply. Both files come from the cherry-picked session-search work.

Reproduction: same counts (10 failed / 6 findings) at the trunk, so these are not regressions from this PR.

Environmental, also unrelated

  • package (linux) — pr.yml verifies the root-package marker at a hardcoded ./opt/Orca/resources/package-type, but this fork sets productName: 'Orca NP', so the deb/rpm install dir is /opt/Orca NP. Upstream's own resources/linux/packaging/after-install.sh says the directory name varies by productName — the CI assertion is the one place that still assumes Orca.
  • package (windows) — windows-hook-upgrade.test.ts hardcodes ~/.orca/agent-hooks/codex-hook.cmd while local(identity) moved the hook home to ~/.orca-np. The test is describe.skipIf(process.platform !== 'win32'), so it can only ever fail in this job.
  • track-community-pr — needs secrets.BUFO_BOT_PRIVATE_KEY (StablyAI-internal, actions/create-github-app-token for owner stablyai), which does not exist in a fork.

Passing here

typecheck, e2e / changed e2e specs (this branch's new agent-dashboard-sidebar-toggle.spec.ts runs and passes), e2e / build e2e app, root directory guard, test vs non-test LoC, detect code-relevant changes.

Suggested follow-up (separate from this PR)

  1. Marker check: derive the /opt/<dir> from productName instead of hardcoding Orca.
  2. windows-hook-upgrade.test.ts: take the script path from the installer's own resolver rather than a literal .orca, so the next identity rename cannot miss it.
  3. track-community-prs.yaml: guard the job with github.repository == 'stablyai/orca'.
  4. Anti-slop findings: rename the shape symbols to their domain role and drop Reflect.apply.
  5. The 10 genuine test failures: update worktree-rm-host-qualification for the new option, and fix the child-session suppression in the descendant listener (that one looks like a real bug, not a stale test).

None of these come from the dashboard change; each reproduces on nplez1/main
with those commits absent.

A child's END with no lead state to gate on now publishes nothing. The
descendant layer derives the pane state from the lead, and falls back to
`working` when no lead record exists — correct for a child that STARTS (the
lead spawned it), but for a child that ENDS it relabels a pane nobody has
described and no later child event can settle. The grok child-session
contract is that a child's terminal events are ignored; the fork's own
descendant tests always start a lead turn first, so they were never
exercising this case.

worktree.rm host qualification: the local deleteRemoteBranch option was
added to the forwarded payload without updating the helper that lists them.

windows-hook-upgrade.test.ts asserted the managed command against the
PRE-upgrade script path, so the identity move to ~/.orca-np made it expect a
command the installer no longer writes. It now takes the managed path from
the installer (getManagedScriptPath) while keeping the old path as the
fixture, so it covers the cross-identity upgrade and cannot drift again.
Windows-only, so it can only fail in that job.

pr.yml verified the root-package marker at a hardcoded /opt/Orca, which is
/opt/<productName>; discover it instead, as after-install.sh already does.

track-community-prs.yaml needs the StablyAI bufo-bot key to file the PR in a
Stably project, so it cannot run on a fork; guard it to the upstream repo.
The PTY smoke step spawned dist/win-unpacked/Orca.exe, but the unpacked exe
is named after the Windows executableName, which this fork sets to "Orca NP".
Same rename fallout as the /opt marker check: the job only reached this step
once the hook test ahead of it was fixed, so the failure was masked.

The step reads executableName from electron-builder.config.cjs and quotes the
path, so a space in the product name cannot split the argument.
The local(identity) rename is a rename of every name an official install
owns, and the packaging checks were the places that still spelled the old
ones. Each of these was masked by the failure before it in the same job.

- The unpacked Windows exe is named after win.executableName, so the PTY
  smoke now reads that from the build config instead of hardcoding Orca.exe.
- smoke-packaged-cli.mjs looks the launcher up in the package's own
  resources/bin (orca-np.exe beside orca-np.cmd here) rather than naming it.
- The headless-serve oracle asserts the registered command, which is
  CLI_COMMAND_NAME (orca-np), while the symlink target stays the bundle's
  launcher name (orca-ide). The runner now passes the command name in, read
  from cli-install-constants.ts.
- The workflow test that pins that step follows the new derivation.

Two stale expectations that were only selected once the change set widened,
both failing at the fork's trunk: worktree.rm now forwards
deleteRemoteBranch (second file with that helper), and this build's single
release stream means a dev-channel tag resolves to nplez1/orca rather than
upstream's dedicated hourly repo.
The static-analysis job failed on six anti-slop findings from the
cherry-picked session-search work: five symbols named `shape` (they label the
query under test, so `queryLabel`) and a Reflect.apply in the settings test's
handler invoker (now a named HandlerLike guard, so the call is typed).

The design-system report also flags every fill in status-bar/icons.tsx (22 of
them, including the Fireworks mark's #6720FF). Those are vendor logo colors —
a theme token or currentColor renders the mark wrong — so the rule is turned
off for that file in the design-system config, which is this repo's pattern
for a file-scoped exemption (an inline directive naming a rule the root scan
does not load always reads back as an unused directive there). The icons
module is byte-identical; only the config changed.
The Windows CLI launcher resolved the app as Path.Combine(appDirectory,
"Orca.exe"). The app is named after productName, which this fork sets to
"Orca NP", so the packaged launcher exited with "Unable to locate Orca.exe"
and the orca-np CLI was dead on Windows — not only in CI: the same launcher
ships in release builds.

It now mirrors the Linux launcher: try the names a build can produce, then
fall back to the install root's own executable, skipping Chromium's crashpad
helper and the NSIS uninstaller. The error message no longer names a file the
build might not produce.

The launcher is compiled from this source during the build
(native/windows-cli-launcher/.build/orca.exe -> resources/bin/orca-np.exe),
so the fix takes effect on the next Windows package; the launcher's own
launch test runs on Windows in CI.
all-host-repo-catalog-actions.ts imported ./repo-catalog-merge and
./repo-catalog-fencing twice each — a type-only import and a value import — so
audit:code-quality:native failed on the denied import/no-duplicates warning.
Pre-existing on the trunk; it only became visible once the anti-slop errors
ahead of it were fixed.

Folded both type imports into their value imports, which is also what the
inline `type` specifiers already used in those same blocks.
The localization gate flagged a new unlocalized renderer string: the sentence
teardown returns when a host answered without a cleanup. It was in the wrong
layer as well as unlocalized — RemoteBranchCleanup.message is documented as
Git's own text, and the toast already translates the title around it.

The store now reports only `status: 'failed'` for that case and the toast
supplies the sentence (message ?? localized fallback), with the string added
to the catalog under the toast's existing node. No behavior change: Git's own
text still shows when there is one.
smoke-managed-hook-runtime-node18.mjs asserted the installed hook commands
under ~/.orca and probed ~/.orca/agent-hooks/*.sh. The home directory is an
identity this fork renames (.orca-np), so the smoke failed the moment the
job's path filter selected it.

It now reads HOME_DIRECTORY_NAME from src/shared/app-directory-names.ts, the
same module the installers under test resolve it from.
@nplez1
nplez1 merged this pull request into nplez1/main Sep 18, 2026
68 of 70 checks passed
nplez1 pushed a commit that referenced this pull request Sep 25, 2026
… manifest (OTA phase C follow-up) (stablyai#22376)

* perf(mobile): keep four bundle chunk reads in flight across the whole manifest

The fetch ran one worker per asset and paged inside an asset sequentially, so
the largest script's 71 chunks were 71 serial round trips while the other
readers idled. One window of four chunk reads now covers every (asset, offset)
on the host's chunk grid, largest asset first. A read_limited refusal narrows
the window and retries the read; eof is still read from the reply.

Synthetic manifest (one 71-chunk asset, five small): 72 round trips -> 19.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): re-record the bundle-fetch family under pipelined reads

Baseline moves to a1ee317, the pipelined fetch.
781 goldens change only their `baseline` header line. Six bodies move:
mobile-web-bundle-fetch-paged, mobile-web-bundle-build-changed, and the four
matrix-mobileweb.bundle-fetch-* goldens.

The two bundle-fetch scenarios now bind requests in pipelined order, largest
asset first, with every chunk sent before any reply: index.html@0 (#1),
index.html@16 (#2), assets/app.js@0 (#3).
- fetch-paged: the request set is identical, only reordered. The chunk
  sender names/ordinals and the scenarioSha256 moved; the replies and the
  fetched bytes did not.
- build-changed: the same reorder, plus one request that is new because
  pipelining puts it in flight before the refusal lands (index.html@16).
  The refusal and the checkpoint are unchanged.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* docs(mobile): the bundle chunk comment no longer says a reply picks the next offset

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): re-pin the recording corpus after the bundle comment fix

Baseline moves from a1ee317 to e94bde3,
the comment-only commit on a fenced path. All 787 goldens and the scenarios file
change only their `baseline` line. No golden body moved.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): pool four workers over planned bundle chunks, report progress per chunk

Design-review fix round, sketch C: four workers take reads from one planned
chunk queue, largest asset first. They replace the central pump and the
read_limited narrowing. A host frees its read slot before it replies, so a lone
fetch capped at four cannot trip the limit. A refusal now fails the fetch, as
it did on base, and stops the other reads.

- Each asset's buffer is allocated when the plan is built. That removes the
  nullable buffer and its guard. The per-asset byte count is gone, and the
  hash is the oracle (S1, S2).
- The caller's signal is checked before each read and once after the pool
  drains, so an abort during the final window rejects with fetch-stopped
  (S3). The stopped check now covers only the caller's abort. The internal
  stop only makes late replies skip checks, hashing and progress (S4).
- Progress is reported per accepted chunk. completedAssets still counts on
  completion (S6).
- Renames: MAX_CONCURRENT_CHUNK_READS, and `reply` for the RPC reply (N1).
- The slot check states exact-slot acceptance once, then classifies the
  refusal (N3).
- Stale test titles and comments are renamed (N4).

The synthetic manifest still takes 19 round trips.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* docs(mobile): say why bundle reads settle at on-settle under pipelined chunks

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): re-record the bundle-fetch family under per-chunk progress

Baseline moves to 34fda6f. 782 goldens and
the scenarios file change only their `baseline` line. Five bodies move:
mobile-web-bundle-fetch-paged and the four matrix-mobileweb.bundle-fetch-*
goldens. The only change is bundle-progress effects. One report now lands
after the first accepted chunk of index.html (0 assets, 16 bytes), and the
later progress ordinals shift by one. Requests, replies and fetched bytes
are identical. mobile-web-bundle-build-changed keeps its body, because its
one accepted chunk is the whole of assets/app.js.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): drain the fake host after the fetch settles so the sibling-stop bounds can fail

The wave host stopped releasing replies once the fetch settled. Reads that
should have been stopped were never answered, so the read_limited bound (7)
and the chunk-failure bound (5) held even with no sibling stop at all. It now
drains until nothing waits. With the worker's stopped.abort() removed, both
bounds fail at 76 requests. assertChunkDescribesAsset's parameter is renamed
to `reply`.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): re-pin the recording corpus after the sibling-stop test fix

Baseline moves from 34fda6f to 97b13ec.
All 787 goldens and the scenarios file change only their `baseline` line. No
golden body moved.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
nplez1 pushed a commit that referenced this pull request Sep 25, 2026
…otiated mobileWeb.bundle.range (OTA phase C follow-up) (stablyai#22381)

* feat(mobile-web): serve gzipped 384 KiB bundle ranges behind a capability

Adds mobileWeb.bundle.range with its own strict params and result, so
shipped chunk readers see no reply change. The host gzips each range at
level 6 and sends identity when gzip does not shrink it, sharing the chunk
method's read-slot budget and per-asset verification. status.get
advertises mobileWeb.bundle.range.v1 beside mobileWeb.bundle.v1, and the
method is allowlisted for paired phones.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* feat(mobile): read the bundle range capability and range replies

Adds the range reply reader and operation, and picks range or chunk from
the status.get capabilities the connection already proved, so an older
desktop keeps being paged in chunks with no probe round trip.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* perf(mobile): keep four bundle chunk reads in flight across the whole manifest

The fetch ran one worker per asset and paged inside an asset sequentially, so
the largest script's 71 chunks were 71 serial round trips while the other
readers idled. One window of four chunk reads now covers every (asset, offset)
on the host's chunk grid, largest asset first. A read_limited refusal narrows
the window and retries the read; eof is still read from the reply.

Synthetic manifest (one 71-chunk asset, five small): 72 round trips -> 19.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* chore(mobile): add fflate 0.8.2 for gzip bundle ranges

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* feat(mobile): decode gzip bundle ranges into a bounded buffer

Inflates each range into a buffer one byte past its window, so a gzip
bomb costs at most that allocation and an overlong body is visible. A
corrupt, truncated or unknown-encoding body refuses as range-undecodable;
a body of the wrong decoded length refuses as range-length-mismatch.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* feat(mobile): pass the bundle read method from the session to the fetch

The download reads the capabilities of the gates the reducer decided
under and hands the fetch range or chunk. The fetch does not act on it
yet; the range read lands on the pipelined window.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): re-record the bundle-fetch family under pipelined reads

Baseline moves to a1ee317, the pipelined fetch.
781 goldens change only their `baseline` header line. Six bodies move:
mobile-web-bundle-fetch-paged, mobile-web-bundle-build-changed, and the four
matrix-mobileweb.bundle-fetch-* goldens.

The two bundle-fetch scenarios now bind requests in pipelined order, largest
asset first, with every chunk sent before any reply: index.html@0 (#1),
index.html@16 (#2), assets/app.js@0 (#3).
- fetch-paged: the request set is identical, only reordered. The chunk
  sender names/ordinals and the scenarioSha256 moved; the replies and the
  fetched bytes did not.
- build-changed: the same reorder, plus one request that is new because
  pipelining puts it in flight before the refusal lands (index.html@16).
  The refusal and the checkpoint are unchanged.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* docs(mobile): the bundle chunk comment no longer says a reply picks the next offset

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* feat(mobile): read gzipped bundle ranges on the pipelined window

A host that advertised mobileWeb.bundle.range.v1 is paged in 384 KiB
ranges on the range grid, through the same four-read window and queue as
chunks; any other host keeps the chunk grid. Each range is decoded to its
exact window length before the fill checks and the asset hash.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): re-pin the recording corpus after the bundle comment fix

Baseline moves from a1ee317 to e94bde3,
the comment-only commit on a fenced path. All 787 goldens and the scenarios file
change only their `baseline` line. No golden body moved.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): re-record the corpus at the range-read pin

Repins baseline to the range-read commit and re-records every golden.
Only the baseline and lockfileSha256 headers move: the lockfile gained
fflate, and the bundle-fetch adapter pages the chunk path, whose
requests and replies are unchanged, so no golden body moves.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* docs(mobile): state the on-settle reason that holds for pipelined bundle reads

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): re-pin the recording corpus after the on-settle comment fix

Baseline moves from 252c592 to fe41226,
the comment-only commit on a fenced path. Re-recorded: all 787 goldens and
the scenarios file change only their baseline line. No golden body moved.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* chore(mobile): keep the lockfile's patch block in main's form

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): re-pin the recording corpus after the lockfile patch-block restore

Baseline moves from fe41226 to 84d6fca,
which restores main's patchedDependencies form in mobile/pnpm-lock.yaml.
Re-recorded: all 787 goldens change only baseline and lockfileSha256, and
the scenarios file only baseline. No golden body moved.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): pool four workers over planned bundle chunks, report progress per chunk

Design-review fix round, sketch C: four workers take reads from one planned
chunk queue, largest asset first. They replace the central pump and the
read_limited narrowing. A host frees its read slot before it replies, so a lone
fetch capped at four cannot trip the limit. A refusal now fails the fetch, as
it did on base, and stops the other reads.

- Each asset's buffer is allocated when the plan is built. That removes the
  nullable buffer and its guard. The per-asset byte count is gone, and the
  hash is the oracle (S1, S2).
- The caller's signal is checked before each read and once after the pool
  drains, so an abort during the final window rejects with fetch-stopped
  (S3). The stopped check now covers only the caller's abort. The internal
  stop only makes late replies skip checks, hashing and progress (S4).
- Progress is reported per accepted chunk. completedAssets still counts on
  completion (S6).
- Renames: MAX_CONCURRENT_CHUNK_READS, and `reply` for the RPC reply (N1).
- The slot check states exact-slot acceptance once, then classifies the
  refusal (N3).
- Stale test titles and comments are renamed (N4).

The synthetic manifest still takes 19 round trips.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* docs(mobile): say why bundle reads settle at on-settle under pipelined chunks

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile-web): announce bundle ranges on the manifest reply

The manifest reply now names the range grid in an optional rangeBytes,
beside chunkBytes, and the status capability is gone. The range method
takes exactly the chunk params on that grid instead of a caller length.
Both methods share one verified read that returns the six-field header,
and the range handler checks the connection again before deflating.
Range schemas move into the bundle RPC contract; SHA256_PATTERN is shared
from the manifest contract. Shared refusals are tested once over both
methods.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): drop the range capability read and its session threading

The phone will read rangeBytes off the loose manifest reply instead, so
the read-method module goes and the session effects and hook return to
the pipeline branch's version. Range imports move to the bundle RPC
contract, the reply reader reuses the shared SHA256_PATTERN, and a new
test pins that node's level-6 gzip from the host encoder inflates with
fflate to the same bytes. The fetch and window-read modules still import
the deleted names until part 2.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): re-record the bundle-fetch family under per-chunk progress

Baseline moves to 34fda6f. 782 goldens and
the scenarios file change only their `baseline` line. Five bodies move:
mobile-web-bundle-fetch-paged and the four matrix-mobileweb.bundle-fetch-*
goldens. The only change is bundle-progress effects. One report now lands
after the first accepted chunk of index.html (0 assets, 16 bytes), and the
later progress ordinals shift by one. Requests, replies and fetched bytes
are identical. mobile-web-bundle-build-changed keeps its body, because its
one accepted chunk is the whole of assets/app.js.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* feat(mobile): page bundle ranges through one window reader chosen by the manifest

The fetch keeps the pipeline's four-worker pool and builds one window
reader from the manifest reply: ranges on rangeBytes when the host names
it, chunks on chunkBytes otherwise. The reader returns the six-field
header and a lazy bytes() so the stop and misroute checks run before any
decode. A range that inflates to the wrong length now falls to the slot
checks, with the one-byte-over buffer as the memory bound, so
range-length-mismatch is gone. A rangeBytes this build cannot page reads
as absent. Fetch names say window, not chunk.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): drain the fake host after the fetch settles so the sibling-stop bounds can fail

The wave host stopped releasing replies once the fetch settled. Reads that
should have been stopped were never answered, so the read_limited bound (7)
and the chunk-failure bound (5) held even with no sibling stop at all. It now
drains until nothing waits. With the worker's stopped.abort() removed, both
bounds fail at 76 requests. assertChunkDescribesAsset's parameter is renamed
to `reply`.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): re-pin the recording corpus at the window-reader commit

Baseline moves to a25a355. Re-recorded:
every golden and the scenarios file move only baseline, and the five
bundle-fetch goldens also move lockfileSha256 to this branch's lockfile.
Every golden body is byte-identical to the pipeline branch's. The
recording adapter's scripted host names no rangeBytes, so the bundle
family still records the chunk path.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): re-pin the recording corpus after the sibling-stop test fix

Baseline moves from 34fda6f to 97b13ec.
All 787 goldens and the scenarios file change only their `baseline` line. No
golden body moved.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): re-pin the recording corpus at the second pipeline merge

Baseline moves from a25a355 to 4d2cab3,
the merge of the pipeline's sibling-stop test fix. Re-recorded: every golden
and the scenarios file move only baseline. Against the pipeline branch, only
baseline and lockfileSha256 differ; every golden body is identical.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): read the bomb inflation without depending on call order

With the fetch's sibling stop removed, a read left over from the previous
test inflated into the bomb test's record first, and indexOf(601) picked
it. The test now asserts some inflation stopped at 601 and none exceeded
its buffer, whatever else ran.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): re-pin the recording corpus at the bomb-test fix

Baseline moves from 4d2cab3 to 73fde15,
the test-only commit on a fenced path. All 787 goldens and the scenarios
file change only their baseline line. No golden body moved.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile-web): tighten the bundle window contract and pin the range sibling stop

The range bomb test reads only its own host's inflations, keyed by the
gzip bodies that host sent, and plans twenty reads so a missing sibling
stop is visible: with stopped.abort() removed it sends all twenty.
The range params are an alias of the chunk params, and the chunk data
bound is the exact base64 length of a full chunk. The phone's chunk and
range replies share one header shape. The window reader closes over the
client and bytes() takes the slot length the fetch computes. The host's
positional read is readMobileWebBundleAssetWindow.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): re-pin the recording corpus at the window-contract commit

Baseline moves from 73fde15 to 7346e00.
All 787 goldens and the scenarios file change only their baseline line.
No golden body moved.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): re-pin the recording corpus at the main merge

Baseline moves from 7346e00 to 9c0fe1a,
the merge of main at 98a6a53. Recorded with --record: all 787 goldens
and the scenarios file change only their baseline line. No golden body
moved.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile-web): drop a test cast and shape-named field maps

The bomb test's inflation log is typed by its hoisted factory's return
instead of an assertion, and the zod field maps shared by the bundle
window schemas are windowParamsFields and windowHeaderFields.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): re-pin the recording corpus at the lint fix

Baseline moves from 9c0fe1a to f4f0915.
Recorded with --record: all 787 goldens and the scenarios file change only
their baseline line. No golden body moved.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): page the range fetch fixture on a small advertised grid

The fake host names a 4 KiB range grid and a 1 KiB chunk grid on its
manifest reply, which the phone pages as it would the real ones, so the
fixtures shrink to a few KiB with the same shapes and each asset is
hashed once. The file runs in about 360 ms instead of 3.5 s, which a
loaded CI runner pushed past the 5 s test timeout. The desktop range
suite still pins that the real host names MOBILE_WEB_BUNDLE_RANGE_BYTES.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): re-pin the recording corpus at the range fixture fix

Baseline moves from f4f0915 to d4d6aad.
Recorded with --record: all 787 goldens and the scenarios file change only
their baseline line. No golden body moved.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
nplez1 pushed a commit that referenced this pull request Oct 19, 2026
…nce off the deprecated owner resolver (stablyai#30484)

The eight mirroring callers (#23-#30) read the owner through
runtimeEnvironmentIdWithLegacyFallbackUnlessTwin, so a twin is still not mirrored. Sort-order
persistence (#2) leaves out ids that two hosts publish, and ids whose only server proof is a repo
row (isUnprovenServerSshRoute), instead of writing them to this desktop's or a guessed server's
meta. Deprecated importers 40 -> 31.
nplez1 pushed a commit that referenced this pull request Oct 21, 2026
…win projections stay on their host (review-5 #2 #9 #11) (stablyai#30890)

* fix(renderer): host writes route only through the strict owner, and twin projections stay on their host (review-5 #2 #9 #11)

A selected ssh: host that two paired servers proxy now resolves ambiguous at the one
selected-host resolver, so no caller can fall back to this app's own SSH connection.
File Explorer and SSH mutation expectations derive the full {endpoint, place} pair from
resolveOwner, so a server folder's nested SSH target stays the write's host. A local
lineage write projects onto its own host's rows only. The id-only store-write ratchet
now also covers persistence repo-row lookups by id alone (review-5 #10's shape).

* fix(ratchet): the id-only lookup scan also sees optional chaining and destructured ids

* fix(renderer): an unstamped folder keeps reading its host from its repos; editor focus fallback left for a follow-up

* docs(renderer): the File Explorer owner comment names its folder fallback
@nplez1
nplez1 deleted the nplez1/Agent-Dashboard branch October 27, 2026 13:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant