Skip to content

fix(web): client-only Connections hides managed-server controls - #1867

Merged
donjor merged 1 commit into
hyprwsfrom
fix/1773-client-only-connections
Oct 26, 2026
Merged

donjor merged 1 commit into
hyprwsfrom
fix/1773-client-only-connections

Conversation

@donjor-agent

@donjor-agent donjor-agent Bot commented Oct 26, 2026

Copy link
Copy Markdown

🤖 claude/anthropic claude-opus-5-5 (high)

Problem

In client-only mode, Connections shows controls for a server this desktop does not manage: Local environment, Network access, and Tailscale HTTPS.
The upstream gate keys on environment:maintain, which a standard pairing grants, and the local-environment flag defaults to on.

Change

New gate canManageLocalBackendFork requires all three:

  • environment:maintain scope
  • not client-only
  • local environment on

Callers:

  • Connections rows
  • Settings search
  • LocalEnvironmentSetting: off in client-only

Rename: one fork file holds both client-only gates.

apps/web/src/components/settings/
└── connectionsSessionScopes.fork.ts → connectionsClientOnly.fork.ts

Upstream edits are one-line fork hooks; the managed path is unchanged.

Scope and approval

Closes #1773; the human chose a fork fix.

  • Hidden: all three rows in client-only
  • Kept: managed desktop still shows them

Also hidden in client-only: the Version row, behind the same gate.

Known gap, left as is: the local-environment search item still lists in client-only, then opens Connections without the row.
Tagging it localBackendManagementOnly would also hide the way back when a managed desktop turns its local environment off.

Verification

Dev desktop on the worktree .t3; client-only attached a separate vp run dev:server on :14289.

Client-only, before (pre-fix sources): all three rows shown

client-only-before.png

Client-only, after: rows gone

client-only-after.png

Managed, after: rows kept

managed-kept.png

Checks:

  • Gate tests: 8/8 pass
  • vp run check:changed → exit 1
    • two failing tests, outside this diff
    • GitManager.test.ts: CLAUDE.md case, fails alone
    • markdownPipeline.browser.test.ts: passes alone
  • vp run fork:ci → exit 0

Claude Opus 5.5, Claude Code in T3 Code.

Fork trailers

Fork-Domain: backend-attach
Fork-Tier: bugfix
Fork-Upstreamable: no

Fork-Domain: backend-attach
Fork-Tier: bugfix
Fork-Upstreamable: no
@rsi-software-agent

rsi-software-agent Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

🧩 Effect Service Conventions 6b786be ✅ clear

No findings.

🕐 26 Oct 08:09 UTC ⏱️ 2 min 🤖 gpt-6.1-sol 🪙 67K

⚠️ Not verified: Tests and runtime UI behavior were not verified.

Details
Head Result Time Check
6b786be ✅ clear 2 min run

@rsi-software-agent

rsi-software-agent Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

🧩 UI Consistency 6b786be ✅ clear

No findings.

🕐 26 Oct 08:13 UTC ⏱️ 4 min 🤖 gpt-6.1-sol 🪙 70.3K

Details
Head Result Time Check
6b786be ✅ clear 4 min run

@donjor
donjor merged commit eec8a1e into hyprws Oct 26, 2026
13 checks passed
@donjor
donjor deleted the fix/1773-client-only-connections branch October 26, 2026 08:30
donjor added a commit that referenced this pull request Oct 27, 2026
## Problem

In client-only mode, Connections shows controls for a server this desktop does not manage: Local environment, Network access, and Tailscale HTTPS.
The upstream gate keys on `environment:maintain`, which a standard pairing grants, and the local-environment flag defaults to on.

## Change

New gate `canManageLocalBackendFork` requires all three:

- `environment:maintain` scope
- not client-only
- local environment on

**Callers:**

- Connections rows
- Settings search
- `LocalEnvironmentSetting`: off in client-only

**Rename:** one fork file holds both client-only gates.

```text
apps/web/src/components/settings/
└── connectionsSessionScopes.fork.ts → connectionsClientOnly.fork.ts
```

Upstream edits are one-line fork hooks; the managed path is unchanged.

## Scope and approval

Closes #1773; the human chose a fork fix.

- [ ] **Hidden:** all three rows in client-only
- [ ] **Kept:** managed desktop still shows them

**Also hidden in client-only:** the Version row, behind the same gate.

**Known gap, left as is:** the `local-environment` search item still lists in client-only, then opens Connections without the row.
Tagging it `localBackendManagementOnly` would also hide the way back when a managed desktop turns its local environment off.

## Verification

Dev desktop on the worktree `.t3`; client-only attached a separate `vp run dev:server` on `:14289`.

**Client-only, before** (pre-fix sources): all three rows shown

![client-only-before.png](https://github.com/user-attachments/assets/f98a7157-03c0-43a8-ad75-3ff40681a42d)

**Client-only, after:** rows gone

![client-only-after.png](https://github.com/user-attachments/assets/60e82d4c-6618-40d1-b45d-dd6c04e28e37)

**Managed, after:** rows kept

![managed-kept.png](https://github.com/user-attachments/assets/80841cb4-66d9-4536-a569-bb6047a8ffa9)

**Checks:**

- Gate tests: 8/8 pass
- `vp run check:changed` → exit 1
  - two failing tests, outside this diff
  - `GitManager.test.ts`: CLAUDE.md case, fails alone
  - `markdownPipeline.browser.test.ts`: passes alone
- `vp run fork:ci` → exit 0

Claude Opus 5.5, Claude Code in T3 Code.

## Fork trailers

Fork-Domain: backend-attach
Fork-Tier: bugfix
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
donjor added a commit that referenced this pull request Oct 27, 2026
## Problem

In client-only mode, Connections shows controls for a server this desktop does not manage: Local environment, Network access, and Tailscale HTTPS.
The upstream gate keys on `environment:maintain`, which a standard pairing grants, and the local-environment flag defaults to on.

## Change

New gate `canManageLocalBackendFork` requires all three:

- `environment:maintain` scope
- not client-only
- local environment on

**Callers:**

- Connections rows
- Settings search
- `LocalEnvironmentSetting`: off in client-only

**Rename:** one fork file holds both client-only gates.

```text
apps/web/src/components/settings/
└── connectionsSessionScopes.fork.ts → connectionsClientOnly.fork.ts
```

Upstream edits are one-line fork hooks; the managed path is unchanged.

## Scope and approval

Closes #1773; the human chose a fork fix.

- [ ] **Hidden:** all three rows in client-only
- [ ] **Kept:** managed desktop still shows them

**Also hidden in client-only:** the Version row, behind the same gate.

**Known gap, left as is:** the `local-environment` search item still lists in client-only, then opens Connections without the row.
Tagging it `localBackendManagementOnly` would also hide the way back when a managed desktop turns its local environment off.

## Verification

Dev desktop on the worktree `.t3`; client-only attached a separate `vp run dev:server` on `:14289`.

**Client-only, before** (pre-fix sources): all three rows shown

![client-only-before.png](https://github.com/user-attachments/assets/f98a7157-03c0-43a8-ad75-3ff40681a42d)

**Client-only, after:** rows gone

![client-only-after.png](https://github.com/user-attachments/assets/60e82d4c-6618-40d1-b45d-dd6c04e28e37)

**Managed, after:** rows kept

![managed-kept.png](https://github.com/user-attachments/assets/80841cb4-66d9-4536-a569-bb6047a8ffa9)

**Checks:**

- Gate tests: 8/8 pass
- `vp run check:changed` → exit 1
  - two failing tests, outside this diff
  - `GitManager.test.ts`: CLAUDE.md case, fails alone
  - `markdownPipeline.browser.test.ts`: passes alone
- `vp run fork:ci` → exit 0

Claude Opus 5.5, Claude Code in T3 Code.

## Fork trailers

Fork-Domain: backend-attach
Fork-Tier: bugfix
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
rsi-software-agent Bot pushed a commit that referenced this pull request Oct 27, 2026
## Problem

In client-only mode, Connections shows controls for a server this desktop does not manage: Local environment, Network access, and Tailscale HTTPS.
The upstream gate keys on `environment:maintain`, which a standard pairing grants, and the local-environment flag defaults to on.

## Change

New gate `canManageLocalBackendFork` requires all three:

- `environment:maintain` scope
- not client-only
- local environment on

**Callers:**

- Connections rows
- Settings search
- `LocalEnvironmentSetting`: off in client-only

**Rename:** one fork file holds both client-only gates.

```text
apps/web/src/components/settings/
└── connectionsSessionScopes.fork.ts → connectionsClientOnly.fork.ts
```

Upstream edits are one-line fork hooks; the managed path is unchanged.

## Scope and approval

Closes #1773; the human chose a fork fix.

- [ ] **Hidden:** all three rows in client-only
- [ ] **Kept:** managed desktop still shows them

**Also hidden in client-only:** the Version row, behind the same gate.

**Known gap, left as is:** the `local-environment` search item still lists in client-only, then opens Connections without the row.
Tagging it `localBackendManagementOnly` would also hide the way back when a managed desktop turns its local environment off.

## Verification

Dev desktop on the worktree `.t3`; client-only attached a separate `vp run dev:server` on `:14289`.

**Client-only, before** (pre-fix sources): all three rows shown

![client-only-before.png](https://github.com/user-attachments/assets/f98a7157-03c0-43a8-ad75-3ff40681a42d)

**Client-only, after:** rows gone

![client-only-after.png](https://github.com/user-attachments/assets/60e82d4c-6618-40d1-b45d-dd6c04e28e37)

**Managed, after:** rows kept

![managed-kept.png](https://github.com/user-attachments/assets/80841cb4-66d9-4536-a569-bb6047a8ffa9)

**Checks:**

- Gate tests: 8/8 pass
- `vp run check:changed` → exit 1
  - two failing tests, outside this diff
  - `GitManager.test.ts`: CLAUDE.md case, fails alone
  - `markdownPipeline.browser.test.ts`: passes alone
- `vp run fork:ci` → exit 0

Claude Opus 5.5, Claude Code in T3 Code.

## Fork trailers

Fork-Domain: backend-attach
Fork-Tier: bugfix
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
rsi-software-agent Bot pushed a commit that referenced this pull request Oct 28, 2026
pick 50aba4d feat(server): thread groups live on the server with agent tools (#1840)
fixup ef3cc72 fixup! feat(server): thread groups live on the server with agent tools (#1840)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/50aba4d3af96141607e26e2d0a908ce3a2c76573.msg)"
pick ae3460e feat(fork-sync): resolutions persist as git data between runs (#1849)
pick 68b1f2d feat(fork-sync): a pushed resolution reruns the sync from the trunk (#1850)
pick fb702ad feat(fork-sync): run the fork-sync skill through rsi-pr-agent (#1848)
pick 8443684 feat(fork-sync): a blocked sync labels its attention issue rsi:fork-sync (#1847)
pick 742ee72 fix(fork-sync): job mode adds the upstream remote before the dry run (#1852)
pick 29b9574 fix(fork-meta): give the fork-sync job 45 minutes (#1853)
pick d4df433 fix(fork-sync): the job always decides and runs from an rsi-pr-agent checkout (#1854)
pick a7dda3f fix(fork-sync): a red candidate starts the fork-sync job (#1855)
pick a25ae74 fix(fork-sync): carry a retired delete/modify through sync/resolution (#1856)
pick 9fa4f41 docs(backend-attach): describe the attached primary and its seams (#1861)
pick a7c4da6 fix(fork-meta): install pins gh's default repository to the fork (#1862)
pick a3977e8 test(desktop): the cross-arch probe test ignores a host ELECTRON_RUN_AS_NODE (#1859)
pick 179484c fix(github-issues): a V2 unlink of a V1 issue link survives compaction (#1858)
pick 1b3ec59 fix(fork-meta): fold apply keeps cancelling folds and never consults rerere (#1863)
pick 83320f1 fix(fork): refresh legacy setup scripts in project settings overrides (#1866)
pick affc863 fix(web): client-only Connections hides managed-server controls (#1867)
pick 49c280f fix(server): fork notice follows a first /compact and native replacement (#1864)
pick 9095e30 fix(web): group titles name the shared subject, anchored on the parent (#1868)
pick c1aa7ba fix(fork-sync): export partial progress before the job limit (#1872)
pick 147c6ee test(server): pin core.excludesFile in the GitManager fixture (#1874)
pick 3dbca16 fix(server): issue MCP tools target a thread like the pull request tools (#1860)
pick e6c42e2 fix(server): a stop during the migration backup no longer kills the update (#1873)
pick 5c2f552 fix(terminal): close T3 viewers of a settled lane's removed zmux session (#1876)
pick 2e66346 fix(fork-sync): time the partial export by the job's clock (#1879)
pick b318703 feat(web): group title becomes a tab on its parent thread (#1878)
pick 6bf77c1 fix(fork-sync): keep a job to one checkout, no proof clones (#1880)
pick 40f0718 fix(web): file saves no longer overwrite newer disk changes (#1869)
fixup 716cede fixup! fix(web): file saves no longer overwrite newer disk changes (#1869)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/40f071855f5af126fa11c2521ddf5672839ecb40.msg)"
pick 89aa35a fix(voice-input): one base URL derives dictation upload and live paths (#1881)
pick 8b43824 feat(voice-input): send per-project speech terms to the dictation service (#1882)
pick a46b608 fix(fork-sync): a job pushes its rebased tip and the driver lands it without replay (#1887)
pick 3bea8bc refactor(upstream-fixes): retire the terminal focus-request gate; upstream focusTerminal owns focus
pick 5ad62c1 fix(fork-hooks): the marker scanner skips regex literals (#1888)
pick 99232a5 fix(fork-sync): a no-tag rerun keeps the kept worktree's tag (#1889)
pick 4ed891c fix(fork-sync): a session runs fork:ci on its tip before the hand-off (#1890)
pick 8d57556 fix(fork-sync): a sync refuses a tip that lost fork hook lines (#1891)
pick 6714a7b fix(fork-sync): a landed hook retirement exempts no later sync (#1892)
pick 16d6089 fix(fork-ci): fork:ci runs the merge-tree gate, so a session hands off no tip CI refuses (#1893)
pick ab8529e fix(fork-ci): fork:ci runs knip and typecheck, so a session hands off no tip the Check job refuses (#1894)
pick eb62154 refactor(upstream-fixes): retire the conversation-fork title guard; upstream titles forks from inherited history
pick fa1a4e6 fix(fork-sync): the land run waits out candidate CI, so the report names every red job (#1895)
pick 694072c fix(upstream-fixes): storage cleanup treats a broken gitfile as gone on newer git
pick 12e0108 fix(fork-sync): the driver adds the upstream remote, and the skill allows its kept worktree (#1897)
pick 5d77480 fix(fork-sync): a job session leaves no background process before it replies (#1898)
pick 858eba8 refactor(markdown-editing): retire the flush hook supplied by upstream ; empty
pick b73c2a6 chore(fork-meta): account for existing context without changing the upstream budget
pick 2f2ed8c docs(fork-meta): track the resolved sync seams in domain scans

Fork-Sync-Tag: v0.1.4-nightly.20261027.3796
Fork-Sync-Lease: 29b011d
rsi-software-agent Bot pushed a commit that referenced this pull request Oct 28, 2026
## Problem

In client-only mode, Connections shows controls for a server this desktop does not manage: Local environment, Network access, and Tailscale HTTPS.
The upstream gate keys on `environment:maintain`, which a standard pairing grants, and the local-environment flag defaults to on.

## Change

New gate `canManageLocalBackendFork` requires all three:

- `environment:maintain` scope
- not client-only
- local environment on

**Callers:**

- Connections rows
- Settings search
- `LocalEnvironmentSetting`: off in client-only

**Rename:** one fork file holds both client-only gates.

```text
apps/web/src/components/settings/
└── connectionsSessionScopes.fork.ts → connectionsClientOnly.fork.ts
```

Upstream edits are one-line fork hooks; the managed path is unchanged.

## Scope and approval

Closes #1773; the human chose a fork fix.

- [ ] **Hidden:** all three rows in client-only
- [ ] **Kept:** managed desktop still shows them

**Also hidden in client-only:** the Version row, behind the same gate.

**Known gap, left as is:** the `local-environment` search item still lists in client-only, then opens Connections without the row.
Tagging it `localBackendManagementOnly` would also hide the way back when a managed desktop turns its local environment off.

## Verification

Dev desktop on the worktree `.t3`; client-only attached a separate `vp run dev:server` on `:14289`.

**Client-only, before** (pre-fix sources): all three rows shown

![client-only-before.png](https://github.com/user-attachments/assets/f98a7157-03c0-43a8-ad75-3ff40681a42d)

**Client-only, after:** rows gone

![client-only-after.png](https://github.com/user-attachments/assets/60e82d4c-6618-40d1-b45d-dd6c04e28e37)

**Managed, after:** rows kept

![managed-kept.png](https://github.com/user-attachments/assets/80841cb4-66d9-4536-a569-bb6047a8ffa9)

**Checks:**

- Gate tests: 8/8 pass
- `vp run check:changed` → exit 1
  - two failing tests, outside this diff
  - `GitManager.test.ts`: CLAUDE.md case, fails alone
  - `markdownPipeline.browser.test.ts`: passes alone
- `vp run fork:ci` → exit 0

Claude Opus 5.5, Claude Code in T3 Code.

## Fork trailers

Fork-Domain: backend-attach
Fork-Tier: bugfix
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
rsi-software-agent Bot pushed a commit that referenced this pull request Oct 28, 2026
pick 85cd0f8 fix(upstream-fixes): mobile checkout moves keep upstream's task-permission gate
pick 4f18a87 fix(ci): provision browser and fold replay prerequisites (#1666)
pick adf496d fix(server): conversation forks keep their title, links, and children isolated (#1667)
pick aa22745 fix(server): settle checkout-owned zmux sessions (#1665)
pick 5d52d3b feat(web): add all-except project filtering (#1663)
fixup 292c7e6 fixup! feat(web): add all-except project filtering (#1663)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/5d52d3b37617cacd6abe68efef3b114b308cc0ce.msg)"
pick 35d7d7e feat(web): simplify the window project chooser (#1669)
pick 49cad64 fix(server): deliver terminal exits to the owning viewer (#1676)
pick 7c996b4 feat(terminal): open a plain shell alongside managed zmux (#1677)
pick e6eb930 fix(web): preserve terminal selection across output updates (#1679)
pick 92d6058 feat(server): scaffold external voice dictation (#1672)
pick 24529cb feat(server): transcribe recordings with Meta Muse (#1673)
pick 6257c11 feat(desktop): record dictation into editable chat drafts (#1675)
fixup 89bc6e7 fixup! feat(desktop): record dictation into editable chat drafts (#1675)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/6257c1192810ad42c6e149f9cbd9a20ef60b7206.msg)"
pick 7c8db8b docs(agents): tighten fork development priorities (#1683)
pick 6caa4d8 fix(desktop): keep dictation transcribing across thread switches (#1684)
fixup ead68e7 fixup! fix(desktop): keep dictation transcribing across thread switches (#1684)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/6caa4d8811115e55390a8c336bcc76234e9faa23.msg)"
pick 800ed30 feat(desktop): stream live Meta dictation into the composer (#1686)
fixup 740bd49 fixup! feat(desktop): stream live Meta dictation into the composer (#1686)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/800ed307c0d26f464320892b0941babe07baf08b.msg)"
pick 3983853 docs(fork-meta): allow isolated browser verification (#1689)
pick a47c10c docs(fork): showcase recent features with existing PR media (#1691)
pick 67e6894 feat(desktop): duck selected speakers during dictation (#1687)
fixup 36850d5 fixup! feat(desktop): duck selected speakers during dictation (#1687)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/67e689405e382b3c59fa1b52f98e237df38ed698.msg)"
pick c40227a fix(fork-meta): inherit assigned Hypr workspace in desktop runner (#1690)
pick b4451af fix(server): tear down zmux sessions on thread settle and delete (#1688)
fixup 2af598f fixup! fix(server): tear down zmux sessions on thread settle and delete (#1688)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/b4451afea83a8018f4914e64e0a7a9c44b6203af.msg)"
pick 1e056b4 feat(dictation): add configurable cleanup and formatting (#1685)
fixup 06f209e fixup! feat(dictation): add configurable cleanup and formatting (#1685)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/1e056b479845c3e771897d9d340a94e5697aeda6.msg)"
pick fb52655 fix(upstream-fixes): use extracted provider and replay services
pick 80c87b6 fix(upstream-fixes): adapt fork services to extracted source-control and host process APIs
pick 287c2d7 fix(upstream-fixes): align fork focus rings with inset indicators
pick de9176b fix(fork-sync): the trunk moves only to a sha hyprws CI passed on sync/candidate (#1701)
pick 2f74ffb feat(terminal): choose zmux or plain per terminal and restore sessions on unsettle (#1699)
pick f43fd48 fix(upstream-fixes): provide fork Git workflow services in managed folder tests
pick e6ad2ff fix(voice-input): keep Meta realtime dictation on HTTP/1.1 WebSockets (#1710)
pick 08a2f80 fix(ci): run hyprws CI on stacked pull requests (#1715)
pick a1cfdb3 fix(server): skip the update backup when no migrations are pending (#1711)
pick ac34343 fix(fork): keep the client usable while the attached server restarts (#1712)
pick 8df49c9 fix(web): show server update progress through reconnect (#1713)
fixup 1c56ae7 fixup! fix(web): show server update progress through reconnect (#1713)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/8df49c97f6925ae381ffb7ecd53d3e457e28cc24.msg)"
pick afe2734 fix(server): log memory high-water marks and snapshot the heap at 4 GiB (#1718)
pick e905bde fix(server): record main-thread stacks when memory bursts past 4 GiB (#1719)
pick ba8335a fix(desktop): keep the attached server through a stall (#1721)
pick cb27f62 fix(server): stop syncing the SQLite WAL on every commit (#1722)
pick 3ee6cba fix(server): purge imported v1 rows the v2 runtime never reads (#1723)
pick 021b837 fix(ci): force upstream tag fetches so a moved tag cannot block releases (#1724)
pick a473b09 fix(server): return thread waits when input is needed (#1736)
fixup 7f92cc8 fixup! fix(server): return thread waits when input is needed (#1736)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/a473b09e495cad1421bb0e4cb2a9049f2c24889a.msg)"
pick a958a4e fix(checkout): preserve turns and shared sources during moves (#1733)
pick 7ef7b86 fix(server): retire live work when provider sessions detach (#1734)
pick 0313964 fix(terminal): keep terminals through checkout moves (#1738)
fixup 7b70026 fixup! fix(terminal): keep terminals through checkout moves (#1738)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/0313964c3c2d131d675637a9da738891d0eb0b32.msg)"
pick f622f4a fix(web): pair on same-tab token hash navigation (#1740)
pick 33743d6 fix(zmux): handle workspace basename collisions (#1741)
pick 44b7dbc fix(mcp): hide tools refused by read-only grants (#1742)
fixup 2585ace fixup! fix(mcp): hide tools refused by read-only grants (#1742)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/44b7dbc751a12a7b9fbf585ca10e6daffcf233c1.msg)"
pick cdd21bc fix(web): preserve formatter-clean rich Markdown markers (#1743)
pick 7c7faf7 fix(issues): persist controls, show parents, and search all issues (#1744)
pick 8cd1cd8 fix(upstream-fixes): fork code calls the Effect 4.0.4 randomUUIDv4 function
pick 49e5314 refactor(upstream-fixes): sampled planner statistics defer to upstream ; empty
pick 5b8b460 fix(fork): hook guard matches replayed lines across re-indentation
pick 7d1a2aa fix(web): update everything updates the attached service in client-only mode (#1751)
pick f053d33 refactor(upstream-fixes): fork GitHub issue and attachment paths use upstream's request policy after `pingdotgg#19224`
pick 40e475e refactor(upstream-fixes): fork WS RPCs get their own handler layer under TypeScript's instantiation limit
pick bb196b3 test(upstream-fixes): fork ingestor tests provide upstream's GitVcsDriver (`pingdotgg#20079`) and CommandOutputHub (`pingdotgg#15094`)
pick 953db7a fix(git): name the thread in checkout move refusals (#1762)
pick 284943c fix(web): retarget hidden terminal splits when shown (#1763)
pick 20b4aa4 fix(voice): show redacted provider detail when cleanup fails (#1765)
pick 22e1a1e fix(release): build only AppImages for fork Linux releases (#1768)
pick 6421c4d fix(web): align group title generation budgets (#1769)
pick 4b03981 fix(web): autosave rich Markdown and save files with Ctrl+S (#1770)
fixup c6f0754 fixup! fix(web): autosave rich Markdown and save files with Ctrl+S (#1770)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/4b03981264d1eb75c70dc2f8d0eec2abd9ba58cc.msg)"
pick 545c871 fix(pr): stop turn refreshes from rereading review activity (#1771)
pick 3bb83d3 fix(server): strip desktop launcher environment before providers start (#1766)
pick 10ce76a fix(desktop): ignore installed dependencies in pack watchers (#1767)
pick 7bec9b3 feat(voice-input): live dictation preview for any OpenAI Realtime endpoint (#1775)
fixup 59d45ec fixup! feat(voice-input): live dictation preview for any OpenAI Realtime endpoint (#1775)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/7bec9b3103dee6f3e8b96cc3d6db7ad4fd7c6897.msg)"
pick 10f7a3d fix(upstream-fixes): fork scripts drop section dividers upstream's no-low-value-comments lint refuses
pick eb97ac5 refactor(upstream-fixes): retire the fork's activities purge now upstream drops the table
pick eeef974 fix(ci): install Chromium for release web browser tests (#1778)
pick 74f1379 fix(fork-sync): a file's second hooked commit re-applies; hooks must stay pure insertions (#1780)
pick 179b030 refactor(desktop): retire the fork second-instance buffer for upstream's
pick 53c1d1a fix(fork-sync): hook guard, hook re-apply, and fold check each drop a manual repair (#1782)
pick 260d02a chore(fork-meta): run Macroscope check-run agents as rsi-pr-agent workflows (#1786)
pick 850335e chore(fork-meta): run rsi-pr-agent workflows on the merged pr-change trigger (#1788)
pick 02b0311 fix(web): terminal OSC 52 copies reach the client clipboard (#1790)
pick 448da8c fix(web): use shared custom agent menu heading (#1796)
pick 012cb09 fix(web): compose thread group section headers (#1797)
pick 5238881 fix(web): compose bookmark rows from shared primitives (#1798)
pick 874ffea fix(web): use shared primitives for GitHub issue controls (#1799)
pick 7f24727 fix(voice-input): use InlineButton for processing error dismissal (#1800)
pick b7af052 fix(ci): release web tests pin the upstream release repository (#1802)
pick e1fad55 fix(web): dictation fills AskUserQuestion custom answers (#1803)
pick f27d098 fix(fork-sync): an exact upstream backport may drop its PR link (#1805)
pick fe75d4c fix(fork-meta): browser test siblings keep the .browser infix (#1806)
pick 323a48d test(upstream-fixes): retire fork resume-routing tests superseded by upstream `pingdotgg#21615`
pick ee8c9fe refactor(multi-window): route-navigation seams become marked insertions
pick d2003ea refactor(multi-window): createWindow seams become marked insertions (#1811)
pick 6f5494e fix(fork-meta): fork-fold folds a sync's trailerless fixup into its owner (#1814)
pick 9a87a2c perf(fork-meta): fork scan reads blobs through one git cat-file, not one git show per path (#1815)
pick bcc5966 refactor(server): retire duplicate fork SQLite WAL sync policy (#1823)
pick a3d23d5 refactor(upstream-fixes): retire legacy V1 event purge (#1824)
pick 9b254b3 refactor(desktop): extract remaining window helper seams (#1820)
pick bd63a47 refactor(fork): share one sync attention issue (#1822)
pick a9230b5 refactor(fork-meta): isolate scan git reading (#1819)
pick 5d5f3eb refactor(fork-meta): fork scan refuses every finding; the replay baseline is gone (#1826)
pick 96ccee7 fix(upstream-fixes): agents no longer inherit the live server's port settings (#1828)
pick de66979 refactor(fork-meta): drop the dev-app launcher; agents run upstream dev and place windows with donjor hypr (#1827)
pick ffe001b feat(web): thread groups span projects and can mark a parent thread (#1829)
fixup 0a387aa fixup! feat(web): thread groups span projects and can mark a parent thread (#1829)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/ffe001b8efcae27f417731f0eb795bc49ab8f665.msg)"
pick b7b6592 fix(server): in-thread MCP thread reads show background work (#1831)
fixup 35433a7 fixup! fix(server): in-thread MCP thread reads show background work (#1831)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/b7b6592383dc1952310f272f62c42df013b23313.msg)"
pick 80b6e0f refactor(upstream-fixes): retire the fork's separate WS handler layer for upstream's split table
pick 2cb66a3 fix(fork-sync): reruns continue the rebase, fork:ci gates the candidate, tip commits survive a recreate (#1833)
pick 3f3567c fix(ci): release CLI executable builds on the engines.node pin (#1834)
pick 978268a fix(fork-sync): wait for setup, follow the trunk, root at the main checkout (#1835)
pick 1498f21 fix(desktop): wait for the service update handoff before startup discovery (#1838)
pick 94bb436 fix(server): t3 update waits for a pending remote update (#1837)
pick 41187c6 chore(fork-meta): teach rsi-pr-agent workflows the fork hook seam (#1839)
pick 01547bb feat(server): thread groups live on the server with agent tools (#1840)
fixup a4b48f8 fixup! feat(server): thread groups live on the server with agent tools (#1840)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/01547bbc3c864174352e9cb4d32ec0fa8d6ce372.msg)"
pick 6f33ca0 feat(fork-sync): resolutions persist as git data between runs (#1849)
pick ec5756e feat(fork-sync): a pushed resolution reruns the sync from the trunk (#1850)
pick 21a1043 feat(fork-sync): run the fork-sync skill through rsi-pr-agent (#1848)
pick 434880c feat(fork-sync): a blocked sync labels its attention issue rsi:fork-sync (#1847)
pick 8eadb9c fix(fork-sync): job mode adds the upstream remote before the dry run (#1852)
pick 7c7dc13 fix(fork-meta): give the fork-sync job 45 minutes (#1853)
pick dd1b537 fix(fork-sync): the job always decides and runs from an rsi-pr-agent checkout (#1854)
pick 4c97819 fix(fork-sync): a red candidate starts the fork-sync job (#1855)
pick ecc87d3 fix(fork-sync): carry a retired delete/modify through sync/resolution (#1856)
pick dbbd60d docs(backend-attach): describe the attached primary and its seams (#1861)
pick b069e64 fix(fork-meta): install pins gh's default repository to the fork (#1862)
pick cdd794e test(desktop): the cross-arch probe test ignores a host ELECTRON_RUN_AS_NODE (#1859)
pick fb406ff fix(github-issues): a V2 unlink of a V1 issue link survives compaction (#1858)
pick 7cd6ea1 fix(fork-meta): fold apply keeps cancelling folds and never consults rerere (#1863)
pick 9585bef fix(fork): refresh legacy setup scripts in project settings overrides (#1866)
pick c20d6ef fix(web): client-only Connections hides managed-server controls (#1867)
pick 49b3642 fix(server): fork notice follows a first /compact and native replacement (#1864)
fixup 59a7b46 fixup! fix(server): fork notice follows a first /compact and native replacement (#1864)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/49b3642171ae7e7e1418d76a98b1d60e89fece08.msg)"
pick d4cc6a0 fix(web): group titles name the shared subject, anchored on the parent (#1868)
pick 19b9067 fix(fork-sync): export partial progress before the job limit (#1872)
pick 013ccfc test(server): pin core.excludesFile in the GitManager fixture (#1874)
pick 8bd2820 fix(server): issue MCP tools target a thread like the pull request tools (#1860)
pick cf2e183 fix(server): a stop during the migration backup no longer kills the update (#1873)
pick 7f4c625 fix(terminal): close T3 viewers of a settled lane's removed zmux session (#1876)
pick 6a2844f fix(fork-sync): time the partial export by the job's clock (#1879)
pick e012572 feat(web): group title becomes a tab on its parent thread (#1878)
pick e6cfd23 fix(fork-sync): keep a job to one checkout, no proof clones (#1880)
pick 1231787 fix(web): file saves no longer overwrite newer disk changes (#1869)
pick b6dd169 fix(voice-input): one base URL derives dictation upload and live paths (#1881)
pick f1e284e feat(voice-input): send per-project speech terms to the dictation service (#1882)
pick a8a3005 fix(fork-sync): a job pushes its rebased tip and the driver lands it without replay (#1887)
pick 6424f8a refactor(upstream-fixes): retire the terminal focus-request gate; upstream focusTerminal owns focus
fixup 205c723 fixup! refactor(upstream-fixes): retire the terminal focus-request gate; upstream focusTerminal owns focus
pick d48be70 fix(fork-hooks): the marker scanner skips regex literals (#1888)
pick c7d647c fix(fork-sync): a no-tag rerun keeps the kept worktree's tag (#1889)
pick 998088f fix(fork-sync): a session runs fork:ci on its tip before the hand-off (#1890)
pick 7f8dad0 fix(fork-sync): a sync refuses a tip that lost fork hook lines (#1891)
pick a1c4ebc fix(fork-sync): a landed hook retirement exempts no later sync (#1892)
pick 5498116 fix(fork-ci): fork:ci runs the merge-tree gate, so a session hands off no tip CI refuses (#1893)
pick d30a4fb fix(fork-ci): fork:ci runs knip and typecheck, so a session hands off no tip the Check job refuses (#1894)
pick 664abd2 refactor(upstream-fixes): retire the conversation-fork title guard; upstream titles forks from inherited history
pick e3e4a4e fix(fork-sync): the land run waits out candidate CI, so the report names every red job (#1895)
pick 9701f0c fix(upstream-fixes): storage cleanup treats a broken gitfile as gone on newer git
pick 0d463ba fix(fork-sync): the driver adds the upstream remote, and the skill allows its kept worktree (#1897)
pick 1a03480 fix(fork-sync): a job session leaves no background process before it replies (#1898)
pick a1ea33c refactor(markdown-editing): retire the flush hook supplied by upstream ; empty
pick 1320745 chore(fork-meta): account for existing context without changing the upstream budget
pick 534ee07 docs(fork-meta): track the resolved sync seams in domain scans
pick 66deb1a fix(fork-ci): fork:ci runs every app and package fork test, so a session hands off no tip they fail (#1899)
pick 314965b fix(fork-sync): a job out of budget hands off a red finished marker, so its replay survives the pod (#1901)
pick d3e402c fix(fork-sync): a sync session folds the stack before its hand-off, so the driver stays the only trunk writer (#1902)
pick 8392dad feat(web): every thread group has a parent that carries it to Working (#1904)
pick 9c32ad7 fix(fork-ci): the agent context budget compares against the PR base, not upstream main (#1905)

Fork-Sync-Tag: v0.1.4-nightly.20261028.3809
Fork-Sync-Lease: 9c32ad7
rsi-software-agent Bot pushed a commit that referenced this pull request Oct 28, 2026
## Problem

In client-only mode, Connections shows controls for a server this desktop does not manage: Local environment, Network access, and Tailscale HTTPS.
The upstream gate keys on `environment:maintain`, which a standard pairing grants, and the local-environment flag defaults to on.

## Change

New gate `canManageLocalBackendFork` requires all three:

- `environment:maintain` scope
- not client-only
- local environment on

**Callers:**

- Connections rows
- Settings search
- `LocalEnvironmentSetting`: off in client-only

**Rename:** one fork file holds both client-only gates.

```text
apps/web/src/components/settings/
└── connectionsSessionScopes.fork.ts → connectionsClientOnly.fork.ts
```

Upstream edits are one-line fork hooks; the managed path is unchanged.

## Scope and approval

Closes #1773; the human chose a fork fix.

- [ ] **Hidden:** all three rows in client-only
- [ ] **Kept:** managed desktop still shows them

**Also hidden in client-only:** the Version row, behind the same gate.

**Known gap, left as is:** the `local-environment` search item still lists in client-only, then opens Connections without the row.
Tagging it `localBackendManagementOnly` would also hide the way back when a managed desktop turns its local environment off.

## Verification

Dev desktop on the worktree `.t3`; client-only attached a separate `vp run dev:server` on `:14289`.

**Client-only, before** (pre-fix sources): all three rows shown

![client-only-before.png](https://github.com/user-attachments/assets/f98a7157-03c0-43a8-ad75-3ff40681a42d)

**Client-only, after:** rows gone

![client-only-after.png](https://github.com/user-attachments/assets/60e82d4c-6618-40d1-b45d-dd6c04e28e37)

**Managed, after:** rows kept

![managed-kept.png](https://github.com/user-attachments/assets/80841cb4-66d9-4536-a569-bb6047a8ffa9)

**Checks:**

- Gate tests: 8/8 pass
- `vp run check:changed` → exit 1
  - two failing tests, outside this diff
  - `GitManager.test.ts`: CLAUDE.md case, fails alone
  - `markdownPipeline.browser.test.ts`: passes alone
- `vp run fork:ci` → exit 0

Claude Opus 5.5, Claude Code in T3 Code.

## Fork trailers

Fork-Domain: backend-attach
Fork-Tier: bugfix
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
rsi-software-agent Bot pushed a commit that referenced this pull request Oct 29, 2026
pick 7e1fc40 feat(web): sidebar thread groups use the section header idiom
fixup 30fc9d6 fixup! feat(web): sidebar thread groups use the section header idiom
pick c8ec899 fix(fork-sync): carry the kept sync worktree across reruns
pick 0a3bad0 fix: recover threads whose worktree was removed
pick 4e476bd fix: threads keep working after their worktree is removed
pick 813d3eb fix(web): draw sprite glyphs with Ghostty's own rasterizer
pick 98de14d ci(fork): pooled package tests get the same widened per-test budget as web
pick 55c1f33 refactor(upstream-fixes): usage keybinding order defers to upstream ; empty
pick c8fba62 fix(server): an interrupt failure is recorded before its session stops ; empty
pick b5561df feat(desktop): fork AppImages show T3 Code (hyprws) in launchers
pick 071594c feat(web): fork a thread from the thread menu (#1330)
pick cfa2968 feat(fork): register the multi-window and workspaces fork domains
pick f2151b6 fix(desktop): client-only boots drain restore and launch intent
pick d3c875f feat(web): scope unsent composer drafts per window
pick 958560c feat(desktop): attach one local server as primary in client-only mode
fixup 8b447fd fixup! feat(desktop): attach one local server as primary in client-only mode
pick dbc4ebd feat(desktop): key desktop windows by an opaque WindowId
pick 2e5c1a7 feat(web): each desktop window keeps its own sidebar project scope
fixup 355f8d2 fixup! feat(web): each desktop window keeps its own sidebar project scope
pick e95355d feat(desktop): window requests target the sender or the most recent window
pick 25ca799 feat(desktop): New Window and deep links follow one dispatch table
pick 76fc8a6 feat(desktop): update relaunch restores every window from manifest v2
pick 8eefa56 feat(web): each window filters several projects
fixup 3f67a71 fixup! feat(web): each window filters several projects
pick 8bac3c2 feat(web): the project chooser selects several projects
pick 2cd52eb refactor(web): project routes retire behind a link decoder
pick c7ded9f feat(web): the window filter scopes lists without hiding running work
fixup 661cb87 fixup! feat(web): the window filter scopes lists without hiding running work
pick b55929c docs(fork): any window is the T3 instance, filtered per window
pick ac05889 fix(desktop): a placed window maps on its workspace before it maximizes
pick 687446c chore(fork): retire the legacy project-windows domain
pick 3c2bfa3 fix(fork-sync): one standing blocked and failed issue, rewritten per run
pick 9cdab26 fix(fork): scan a fixup! landing as the commit it names (#1386)
pick 46aa064 docs(fork): README and docs describe multiple windows (#1387)
pick fed0ac0 fix(web): narrow issue panels keep the title and number readable
pick 7043061 fix(web): an open file re-reads on reopen and window focus
pick 8e4e677 fix(web): long context menus fit the window instead of scrolling (#1419)
pick 8b12b0a feat(web): github issues surface reuses the pull request primitives (#1420)
pick 3b011c0 feat(web): issues page answers the pull request page's keyboard (#1421)
fixup b049acb fixup! feat(web): issues page answers the pull request page's keyboard (#1421)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/3b011c08adc7c2a3913477f39e58d93203b3588c.msg)"
pick aa584b2 feat(web): issue list narrows in place while GitHub answers (#1422)
pick ca5bf0c fix(fork-sync): the check battery runs knip and the desktop build before a trunk push (#1439)
pick 45cadb1 feat(web): issue detail gains the pull request panel's actions menu (#1423)
pick 8261376 feat(server): threads persist linked GitHub issues (#1440)
pick 517317c feat(web): close and reopen an issue from its detail panel (#1424)
pick aedf0ca fix(web): issue controls wrap as one group on a narrow pane (#1425) ; empty
pick 5706ac9 feat(server): agents link GitHub issues to their thread over MCP (#1441)
pick 5b01459 feat(web): link GitHub issues to threads from the issue panel, thread, and palette (#1442)
pick eaf2f16 feat(server): read linked issue state on link, panel open, and refresh (#1445)
fixup 5216d82 fixup! feat(server): read linked issue state on link, panel open, and refresh (#1445)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/eaf2f161e1d69c35254613999322d5cbf086aeec.msg)"
pick a75bcda test(github-issues): prove the upstream app still reads a fork server (#1452)
pick a743556 fix(web): issues and links closed as not planned no longer show the merged or completed purple
fixup dd6ba81 fixup! fix(web): issues and links closed as not planned no longer show the merged or completed purple
pick 08bb859 fix(web): a failed issue list refresh reads as stale and keeps its rows
pick 49aa471 fix(web): a failed group title stays on screen with a retry (#1468)
pick 0de4eac fix(client-runtime): an attached primary no longer reconnects on every platform poll (#1477)
pick eb1bf01 fix(web): batch thread delete no longer asks to delete Scratch folders (#1481)
pick 739aad2 feat(server): owner-approved device authorization for native clients (#1483)
pick f0edb69 fix(fork-sync): the check battery runs every hyprws CI test job before a trunk push (#1499)
pick dc4178b docs(fork-sync): adapt in one Fork-Repair commit at the tip (#1496)
pick b452743 docs(fork): document hook marker span; guard names the comment fix (#1497)
pick aafd1e1 fix(fork): folds never drop a PR link, and a plan line can attach one (#1498)
pick 61f197e fix(fork-sync): a conflict on a generated file reruns its generator (#1500)
pick 985a836 fix(fork-sync): a stopped sync workspace builds and its report keeps every stop (#1501)
pick 4bcb299 fix(fork): fold and sync rebases run without auto maintenance racing their stops (#1502)
pick 9fa7d12 fix(fork-sync): a green dry-run keeps its tip for the real sync to publish (#1503)
pick 2299d18 chore(fork): fork tooling sheds its dead flags, outputs, and stale docs (#1504)
pick 5f769e3 fix(fork): scan skips restored upstream test openers; folds may split a cited mover (#1506)
pick cb6c874 fix(fork-sync): a squash-landed fixup! pull request folds at the next sync (#1509)
pick d76c5cc fix(fork-sync): the sync step keeps GH_REPO out of the check battery (#1510)
pick ca00d62 fix(fork): forkSupersedes names wrapped and each-table upstream titles (#1532)
pick 3831e40 fix(fork): folds list squash-landed fixups as members (#1591)
pick 697789a fix(fork): a fold keeps a split piece's repair line when its owner ends in (#N) (#1598)
pick d68a7a5 refactor(server): child-work custom-agent seams sit behind fork-owned files
pick 180745f fix(server): a server-started turn recovers its removed worktree after the root goes idle (#1601)
pick bb85595 fix(fork): folds from a split head cite the old trunk shas (#1600)
pick 14b6cd1 fix(web): retire the fork's durable sidebar Done and run age (#1608)
pick f722f9e fix(desktop): an explicit basic password store keeps saved environments (#1531)
pick 4738679 fix(fork-sync): the license merge keys an unnamed entry on its repositoryUrl (#1616)
pick fe0a298 fix(web): the palette's Link GitHub issue item matches its own title (#1617)
pick 28b0f7a fix(ci): the sync run summary keeps the log tail under GitHub's limit (#1618)
pick ae15e93 fix(fork): a sync fold keeps the squash-landed fixup's PR link (#1620)
pick 26889fd feat(fork): refuse a fixup! commit that does not fold into its owner (#1619)
pick 90ee696 feat(desktop): publish each window's project scope for external tools (#1621)
fixup e63fcb8 fixup! feat(desktop): publish each window's project scope for external tools (#1621)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/90ee696e14eb766451d595d86925fd69756f360c.msg)"
pick 1f6543e refactor(upstream-fixes): PR watch single read defers to upstream ; empty
pick 4e1f681 fix(server): an ended PR watch cancels its queued wakes (#1628)
pick 79c9fbe docs(fork): README showcase clips for each fork domain (#1637)
pick d13e32b feat(server): owner-approved external MCP clients over DPoP device grants (#1636)
pick 71e0148 fix(server): a client server update repoints the install.sh t3 launcher (#1641)
pick 633b107 refactor(fork): sync adaptations land as owner fixups, not Fork-Repair (#1645)
pick 43dfaa3 fix(server): external MCP threads inherit the scoped default model and options (#1646)
pick 681bf67 fix(server): external MCP reads show background work and native subagent turns (#1648)
pick 1a5ce45 fix(mcp): expose reversible external thread settlement (#1649)
pick b216a01 feat(server): delegated tasks continue across rounds (#1653)
pick 8ddc8a4 fix(mcp): external clients list and answer pending questions (#1654)
pick 7ce794d fix(server): resumed native subagents keep recording to their child thread (#1651)
pick d7b77d4 fix(upstream-fixes): replayed fork hooks follow upstream renames, moves, and permission splits
pick de7bc48 refactor(upstream-fixes): fork modules import Effect 4.0.1's stable paths
pick 8641c18 refactor(multi-window): per-window preview runs on upstream's environment-server browser
pick 5151ad4 refactor(upstream-fixes): fork GitHub paths use upstream's GitHubApi and RPC middleware
pick 1af6333 fix(upstream-fixes): fork controls respect upstream's split session permissions
pick c61de5f refactor(upstream-fixes): fork code follows upstream's layer names, Crypto, and module moves
fixup db5cb6a fixup! refactor(upstream-fixes): fork code follows upstream's layer names, Crypto, and module moves
pick 8db30cb fix(upstream-fixes): fork issue MCP tools declare access through upstream's McpToolAccess
pick f7f3b66 fix(upstream-fixes): external MCP registers and declares access through upstream's McpToolAccess
fixup 3f74e5b fixup! fix(upstream-fixes): external MCP registers and declares access through upstream's McpToolAccess
pick 259246d fix(upstream-fixes): fork tests and setup marker follow upstream's latest web and auth changes
pick 85cd0f8 fix(upstream-fixes): mobile checkout moves keep upstream's task-permission gate
pick 4f18a87 fix(ci): provision browser and fold replay prerequisites (#1666)
pick adf496d fix(server): conversation forks keep their title, links, and children isolated (#1667)
pick aa22745 fix(server): settle checkout-owned zmux sessions (#1665)
pick 5d52d3b feat(web): add all-except project filtering (#1663)
fixup 292c7e6 fixup! feat(web): add all-except project filtering (#1663)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/5d52d3b37617cacd6abe68efef3b114b308cc0ce.msg)"
pick 35d7d7e feat(web): simplify the window project chooser (#1669)
pick 49cad64 fix(server): deliver terminal exits to the owning viewer (#1676)
pick 7c996b4 feat(terminal): open a plain shell alongside managed zmux (#1677)
pick e6eb930 fix(web): preserve terminal selection across output updates (#1679)
pick 92d6058 feat(server): scaffold external voice dictation (#1672)
pick 24529cb feat(server): transcribe recordings with Meta Muse (#1673)
pick 6257c11 feat(desktop): record dictation into editable chat drafts (#1675)
fixup 89bc6e7 fixup! feat(desktop): record dictation into editable chat drafts (#1675)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/6257c1192810ad42c6e149f9cbd9a20ef60b7206.msg)"
pick 7c8db8b docs(agents): tighten fork development priorities (#1683)
pick 6caa4d8 fix(desktop): keep dictation transcribing across thread switches (#1684)
fixup ead68e7 fixup! fix(desktop): keep dictation transcribing across thread switches (#1684)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/6caa4d8811115e55390a8c336bcc76234e9faa23.msg)"
pick 800ed30 feat(desktop): stream live Meta dictation into the composer (#1686)
fixup 740bd49 fixup! feat(desktop): stream live Meta dictation into the composer (#1686)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/800ed307c0d26f464320892b0941babe07baf08b.msg)"
pick 3983853 docs(fork-meta): allow isolated browser verification (#1689)
pick a47c10c docs(fork): showcase recent features with existing PR media (#1691)
pick 67e6894 feat(desktop): duck selected speakers during dictation (#1687)
fixup 36850d5 fixup! feat(desktop): duck selected speakers during dictation (#1687)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/67e689405e382b3c59fa1b52f98e237df38ed698.msg)"
pick c40227a fix(fork-meta): inherit assigned Hypr workspace in desktop runner (#1690)
pick b4451af fix(server): tear down zmux sessions on thread settle and delete (#1688)
fixup 2af598f fixup! fix(server): tear down zmux sessions on thread settle and delete (#1688)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/b4451afea83a8018f4914e64e0a7a9c44b6203af.msg)"
pick 1e056b4 feat(dictation): add configurable cleanup and formatting (#1685)
fixup 06f209e fixup! feat(dictation): add configurable cleanup and formatting (#1685)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/1e056b479845c3e771897d9d340a94e5697aeda6.msg)"
pick fb52655 fix(upstream-fixes): use extracted provider and replay services
pick 80c87b6 fix(upstream-fixes): adapt fork services to extracted source-control and host process APIs
pick 287c2d7 fix(upstream-fixes): align fork focus rings with inset indicators
pick de9176b fix(fork-sync): the trunk moves only to a sha hyprws CI passed on sync/candidate (#1701)
pick 2f74ffb feat(terminal): choose zmux or plain per terminal and restore sessions on unsettle (#1699)
pick f43fd48 fix(upstream-fixes): provide fork Git workflow services in managed folder tests
pick e6ad2ff fix(voice-input): keep Meta realtime dictation on HTTP/1.1 WebSockets (#1710)
pick 08a2f80 fix(ci): run hyprws CI on stacked pull requests (#1715)
pick a1cfdb3 fix(server): skip the update backup when no migrations are pending (#1711)
pick ac34343 fix(fork): keep the client usable while the attached server restarts (#1712)
pick 8df49c9 fix(web): show server update progress through reconnect (#1713)
fixup 1c56ae7 fixup! fix(web): show server update progress through reconnect (#1713)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/8df49c97f6925ae381ffb7ecd53d3e457e28cc24.msg)"
pick afe2734 fix(server): log memory high-water marks and snapshot the heap at 4 GiB (#1718)
pick e905bde fix(server): record main-thread stacks when memory bursts past 4 GiB (#1719)
pick ba8335a fix(desktop): keep the attached server through a stall (#1721)
pick cb27f62 fix(server): stop syncing the SQLite WAL on every commit (#1722)
pick 3ee6cba fix(server): purge imported v1 rows the v2 runtime never reads (#1723)
pick 021b837 fix(ci): force upstream tag fetches so a moved tag cannot block releases (#1724)
pick a473b09 fix(server): return thread waits when input is needed (#1736)
fixup 7f92cc8 fixup! fix(server): return thread waits when input is needed (#1736)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/a473b09e495cad1421bb0e4cb2a9049f2c24889a.msg)"
pick a958a4e fix(checkout): preserve turns and shared sources during moves (#1733)
pick 7ef7b86 fix(server): retire live work when provider sessions detach (#1734)
pick 0313964 fix(terminal): keep terminals through checkout moves (#1738)
fixup 7b70026 fixup! fix(terminal): keep terminals through checkout moves (#1738)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/0313964c3c2d131d675637a9da738891d0eb0b32.msg)"
pick f622f4a fix(web): pair on same-tab token hash navigation (#1740)
pick 33743d6 fix(zmux): handle workspace basename collisions (#1741)
pick 44b7dbc fix(mcp): hide tools refused by read-only grants (#1742)
fixup 2585ace fixup! fix(mcp): hide tools refused by read-only grants (#1742)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/44b7dbc751a12a7b9fbf585ca10e6daffcf233c1.msg)"
pick cdd21bc fix(web): preserve formatter-clean rich Markdown markers (#1743)
pick 7c7faf7 fix(issues): persist controls, show parents, and search all issues (#1744)
pick 8cd1cd8 fix(upstream-fixes): fork code calls the Effect 4.0.4 randomUUIDv4 function
pick 49e5314 refactor(upstream-fixes): sampled planner statistics defer to upstream ; empty
pick 5b8b460 fix(fork): hook guard matches replayed lines across re-indentation
pick 7d1a2aa fix(web): update everything updates the attached service in client-only mode (#1751)
pick f053d33 refactor(upstream-fixes): fork GitHub issue and attachment paths use upstream's request policy after `pingdotgg#19224`
pick 40e475e refactor(upstream-fixes): fork WS RPCs get their own handler layer under TypeScript's instantiation limit
pick bb196b3 test(upstream-fixes): fork ingestor tests provide upstream's GitVcsDriver (`pingdotgg#20079`) and CommandOutputHub (`pingdotgg#15094`)
pick 953db7a fix(git): name the thread in checkout move refusals (#1762)
pick 284943c fix(web): retarget hidden terminal splits when shown (#1763)
pick 20b4aa4 fix(voice): show redacted provider detail when cleanup fails (#1765)
pick 22e1a1e fix(release): build only AppImages for fork Linux releases (#1768)
pick 6421c4d fix(web): align group title generation budgets (#1769)
pick 4b03981 fix(web): autosave rich Markdown and save files with Ctrl+S (#1770)
fixup c6f0754 fixup! fix(web): autosave rich Markdown and save files with Ctrl+S (#1770)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/4b03981264d1eb75c70dc2f8d0eec2abd9ba58cc.msg)"
pick 545c871 fix(pr): stop turn refreshes from rereading review activity (#1771)
pick 3bb83d3 fix(server): strip desktop launcher environment before providers start (#1766)
pick 10ce76a fix(desktop): ignore installed dependencies in pack watchers (#1767)
pick 7bec9b3 feat(voice-input): live dictation preview for any OpenAI Realtime endpoint (#1775)
fixup 59d45ec fixup! feat(voice-input): live dictation preview for any OpenAI Realtime endpoint (#1775)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/7bec9b3103dee6f3e8b96cc3d6db7ad4fd7c6897.msg)"
pick 10f7a3d fix(upstream-fixes): fork scripts drop section dividers upstream's no-low-value-comments lint refuses
pick eb97ac5 refactor(upstream-fixes): retire the fork's activities purge now upstream drops the table
pick eeef974 fix(ci): install Chromium for release web browser tests (#1778)
pick 74f1379 fix(fork-sync): a file's second hooked commit re-applies; hooks must stay pure insertions (#1780)
pick 179b030 refactor(desktop): retire the fork second-instance buffer for upstream's
pick 53c1d1a fix(fork-sync): hook guard, hook re-apply, and fold check each drop a manual repair (#1782)
pick 260d02a chore(fork-meta): run Macroscope check-run agents as rsi-pr-agent workflows (#1786)
pick 850335e chore(fork-meta): run rsi-pr-agent workflows on the merged pr-change trigger (#1788)
pick 02b0311 fix(web): terminal OSC 52 copies reach the client clipboard (#1790)
pick 448da8c fix(web): use shared custom agent menu heading (#1796)
pick 012cb09 fix(web): compose thread group section headers (#1797)
pick 5238881 fix(web): compose bookmark rows from shared primitives (#1798)
pick 874ffea fix(web): use shared primitives for GitHub issue controls (#1799)
pick 7f24727 fix(voice-input): use InlineButton for processing error dismissal (#1800)
pick b7af052 fix(ci): release web tests pin the upstream release repository (#1802)
pick e1fad55 fix(web): dictation fills AskUserQuestion custom answers (#1803)
pick f27d098 fix(fork-sync): an exact upstream backport may drop its PR link (#1805)
pick fe75d4c fix(fork-meta): browser test siblings keep the .browser infix (#1806)
pick 323a48d test(upstream-fixes): retire fork resume-routing tests superseded by upstream `pingdotgg#21615`
pick ee8c9fe refactor(multi-window): route-navigation seams become marked insertions
pick d2003ea refactor(multi-window): createWindow seams become marked insertions (#1811)
pick 6f5494e fix(fork-meta): fork-fold folds a sync's trailerless fixup into its owner (#1814)
pick 9a87a2c perf(fork-meta): fork scan reads blobs through one git cat-file, not one git show per path (#1815)
pick bcc5966 refactor(server): retire duplicate fork SQLite WAL sync policy (#1823)
pick a3d23d5 refactor(upstream-fixes): retire legacy V1 event purge (#1824)
pick 9b254b3 refactor(desktop): extract remaining window helper seams (#1820)
pick bd63a47 refactor(fork): share one sync attention issue (#1822)
pick a9230b5 refactor(fork-meta): isolate scan git reading (#1819)
pick 5d5f3eb refactor(fork-meta): fork scan refuses every finding; the replay baseline is gone (#1826)
pick 96ccee7 fix(upstream-fixes): agents no longer inherit the live server's port settings (#1828)
pick de66979 refactor(fork-meta): drop the dev-app launcher; agents run upstream dev and place windows with donjor hypr (#1827)
pick ffe001b feat(web): thread groups span projects and can mark a parent thread (#1829)
fixup 0a387aa fixup! feat(web): thread groups span projects and can mark a parent thread (#1829)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/ffe001b8efcae27f417731f0eb795bc49ab8f665.msg)"
pick b7b6592 fix(server): in-thread MCP thread reads show background work (#1831)
fixup 35433a7 fixup! fix(server): in-thread MCP thread reads show background work (#1831)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/b7b6592383dc1952310f272f62c42df013b23313.msg)"
pick 80b6e0f refactor(upstream-fixes): retire the fork's separate WS handler layer for upstream's split table
pick 2cb66a3 fix(fork-sync): reruns continue the rebase, fork:ci gates the candidate, tip commits survive a recreate (#1833)
pick 3f3567c fix(ci): release CLI executable builds on the engines.node pin (#1834)
pick 978268a fix(fork-sync): wait for setup, follow the trunk, root at the main checkout (#1835)
pick 1498f21 fix(desktop): wait for the service update handoff before startup discovery (#1838)
pick 94bb436 fix(server): t3 update waits for a pending remote update (#1837)
pick 41187c6 chore(fork-meta): teach rsi-pr-agent workflows the fork hook seam (#1839)
pick 01547bb feat(server): thread groups live on the server with agent tools (#1840)
fixup a4b48f8 fixup! feat(server): thread groups live on the server with agent tools (#1840)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/01547bbc3c864174352e9cb4d32ec0fa8d6ce372.msg)"
pick 6f33ca0 feat(fork-sync): resolutions persist as git data between runs (#1849)
pick ec5756e feat(fork-sync): a pushed resolution reruns the sync from the trunk (#1850)
pick 21a1043 feat(fork-sync): run the fork-sync skill through rsi-pr-agent (#1848)
pick 434880c feat(fork-sync): a blocked sync labels its attention issue rsi:fork-sync (#1847)
pick 8eadb9c fix(fork-sync): job mode adds the upstream remote before the dry run (#1852)
pick 7c7dc13 fix(fork-meta): give the fork-sync job 45 minutes (#1853)
pick dd1b537 fix(fork-sync): the job always decides and runs from an rsi-pr-agent checkout (#1854)
pick 4c97819 fix(fork-sync): a red candidate starts the fork-sync job (#1855)
pick ecc87d3 fix(fork-sync): carry a retired delete/modify through sync/resolution (#1856)
pick dbbd60d docs(backend-attach): describe the attached primary and its seams (#1861)
pick b069e64 fix(fork-meta): install pins gh's default repository to the fork (#1862)
pick cdd794e test(desktop): the cross-arch probe test ignores a host ELECTRON_RUN_AS_NODE (#1859)
pick fb406ff fix(github-issues): a V2 unlink of a V1 issue link survives compaction (#1858)
pick 7cd6ea1 fix(fork-meta): fold apply keeps cancelling folds and never consults rerere (#1863)
pick 9585bef fix(fork): refresh legacy setup scripts in project settings overrides (#1866)
pick c20d6ef fix(web): client-only Connections hides managed-server controls (#1867)
pick 49b3642 fix(server): fork notice follows a first /compact and native replacement (#1864)
fixup 59a7b46 fixup! fix(server): fork notice follows a first /compact and native replacement (#1864)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/49b3642171ae7e7e1418d76a98b1d60e89fece08.msg)"
pick d4cc6a0 fix(web): group titles name the shared subject, anchored on the parent (#1868)
pick 19b9067 fix(fork-sync): export partial progress before the job limit (#1872)
pick 013ccfc test(server): pin core.excludesFile in the GitManager fixture (#1874)
pick 8bd2820 fix(server): issue MCP tools target a thread like the pull request tools (#1860)
pick cf2e183 fix(server): a stop during the migration backup no longer kills the update (#1873)
pick 7f4c625 fix(terminal): close T3 viewers of a settled lane's removed zmux session (#1876)
pick 6a2844f fix(fork-sync): time the partial export by the job's clock (#1879)
pick e012572 feat(web): group title becomes a tab on its parent thread (#1878)
pick e6cfd23 fix(fork-sync): keep a job to one checkout, no proof clones (#1880)
pick 1231787 fix(web): file saves no longer overwrite newer disk changes (#1869)
pick b6dd169 fix(voice-input): one base URL derives dictation upload and live paths (#1881)
pick f1e284e feat(voice-input): send per-project speech terms to the dictation service (#1882)
pick a8a3005 fix(fork-sync): a job pushes its rebased tip and the driver lands it without replay (#1887)
pick 6424f8a refactor(upstream-fixes): retire the terminal focus-request gate; upstream focusTerminal owns focus
fixup 205c723 fixup! refactor(upstream-fixes): retire the terminal focus-request gate; upstream focusTerminal owns focus
pick d48be70 fix(fork-hooks): the marker scanner skips regex literals (#1888)
pick c7d647c fix(fork-sync): a no-tag rerun keeps the kept worktree's tag (#1889)
pick 998088f fix(fork-sync): a session runs fork:ci on its tip before the hand-off (#1890)
pick 7f8dad0 fix(fork-sync): a sync refuses a tip that lost fork hook lines (#1891)
pick a1c4ebc fix(fork-sync): a landed hook retirement exempts no later sync (#1892)
pick 5498116 fix(fork-ci): fork:ci runs the merge-tree gate, so a session hands off no tip CI refuses (#1893)
pick d30a4fb fix(fork-ci): fork:ci runs knip and typecheck, so a session hands off no tip the Check job refuses (#1894)
pick 664abd2 refactor(upstream-fixes): retire the conversation-fork title guard; upstream titles forks from inherited history
pick e3e4a4e fix(fork-sync): the land run waits out candidate CI, so the report names every red job (#1895)
pick 9701f0c fix(upstream-fixes): storage cleanup treats a broken gitfile as gone on newer git
pick 0d463ba fix(fork-sync): the driver adds the upstream remote, and the skill allows its kept worktree (#1897)
pick 1a03480 fix(fork-sync): a job session leaves no background process before it replies (#1898)
pick a1ea33c refactor(markdown-editing): retire the flush hook supplied by upstream ; empty
pick 1320745 chore(fork-meta): account for existing context without changing the upstream budget
pick 534ee07 docs(fork-meta): track the resolved sync seams in domain scans
pick 66deb1a fix(fork-ci): fork:ci runs every app and package fork test, so a session hands off no tip they fail (#1899)
pick 314965b fix(fork-sync): a job out of budget hands off a red finished marker, so its replay survives the pod (#1901)
pick d3e402c fix(fork-sync): a sync session folds the stack before its hand-off, so the driver stays the only trunk writer (#1902)
pick 8392dad feat(web): every thread group has a parent that carries it to Working (#1904)
pick 9c32ad7 fix(fork-ci): the agent context budget compares against the PR base, not upstream main (#1905)
pick 3f9f73a fix(web): terminal and dictation settings render inside the settings layout (#1910)
pick 42d6a9b docs(fork): README shows parent-led thread groups, live dictation, and current terminal settings (#1912)
pick 02b9845 fix(dev): worktree dev servers keep their own port, process tree, and Electron profile (#1911)
pick c612147 fix(zmux): worktree dev servers leave the developer's zmux sessions alone (#1913)
pick ec27313 fix(fork-sync): a hook finding in a replayed commit restarts the replay instead of looping on tip fixups (#1915)
pick af1f60a fix(web): distinguish window and issue-list project scopes (#1916)

Fork-Sync-Tag: v0.1.4-nightly.20261028.3824
Fork-Sync-Lease: af1f60a
rsi-software-agent Bot pushed a commit that referenced this pull request Oct 29, 2026
pick 85cd0f8 fix(upstream-fixes): mobile checkout moves keep upstream's task-permission gate
pick 4f18a87 fix(ci): provision browser and fold replay prerequisites (#1666)
pick adf496d fix(server): conversation forks keep their title, links, and children isolated (#1667)
pick aa22745 fix(server): settle checkout-owned zmux sessions (#1665)
pick 5d52d3b feat(web): add all-except project filtering (#1663)
fixup 292c7e6 fixup! feat(web): add all-except project filtering (#1663)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/5d52d3b37617cacd6abe68efef3b114b308cc0ce.msg)"
pick 35d7d7e feat(web): simplify the window project chooser (#1669)
pick 49cad64 fix(server): deliver terminal exits to the owning viewer (#1676)
pick 7c996b4 feat(terminal): open a plain shell alongside managed zmux (#1677)
pick e6eb930 fix(web): preserve terminal selection across output updates (#1679)
pick 92d6058 feat(server): scaffold external voice dictation (#1672)
pick 24529cb feat(server): transcribe recordings with Meta Muse (#1673)
pick 6257c11 feat(desktop): record dictation into editable chat drafts (#1675)
fixup 89bc6e7 fixup! feat(desktop): record dictation into editable chat drafts (#1675)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/6257c1192810ad42c6e149f9cbd9a20ef60b7206.msg)"
pick 7c8db8b docs(agents): tighten fork development priorities (#1683)
pick 6caa4d8 fix(desktop): keep dictation transcribing across thread switches (#1684)
fixup ead68e7 fixup! fix(desktop): keep dictation transcribing across thread switches (#1684)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/6caa4d8811115e55390a8c336bcc76234e9faa23.msg)"
pick 800ed30 feat(desktop): stream live Meta dictation into the composer (#1686)
fixup 740bd49 fixup! feat(desktop): stream live Meta dictation into the composer (#1686)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/800ed307c0d26f464320892b0941babe07baf08b.msg)"
pick 3983853 docs(fork-meta): allow isolated browser verification (#1689)
pick a47c10c docs(fork): showcase recent features with existing PR media (#1691)
pick 67e6894 feat(desktop): duck selected speakers during dictation (#1687)
fixup 36850d5 fixup! feat(desktop): duck selected speakers during dictation (#1687)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/67e689405e382b3c59fa1b52f98e237df38ed698.msg)"
pick c40227a fix(fork-meta): inherit assigned Hypr workspace in desktop runner (#1690)
pick b4451af fix(server): tear down zmux sessions on thread settle and delete (#1688)
fixup 2af598f fixup! fix(server): tear down zmux sessions on thread settle and delete (#1688)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/b4451afea83a8018f4914e64e0a7a9c44b6203af.msg)"
pick 1e056b4 feat(dictation): add configurable cleanup and formatting (#1685)
fixup 06f209e fixup! feat(dictation): add configurable cleanup and formatting (#1685)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/1e056b479845c3e771897d9d340a94e5697aeda6.msg)"
pick fb52655 fix(upstream-fixes): use extracted provider and replay services
pick 80c87b6 fix(upstream-fixes): adapt fork services to extracted source-control and host process APIs
pick 287c2d7 fix(upstream-fixes): align fork focus rings with inset indicators
pick de9176b fix(fork-sync): the trunk moves only to a sha hyprws CI passed on sync/candidate (#1701)
pick 2f74ffb feat(terminal): choose zmux or plain per terminal and restore sessions on unsettle (#1699)
pick f43fd48 fix(upstream-fixes): provide fork Git workflow services in managed folder tests
pick e6ad2ff fix(voice-input): keep Meta realtime dictation on HTTP/1.1 WebSockets (#1710)
pick 08a2f80 fix(ci): run hyprws CI on stacked pull requests (#1715)
pick a1cfdb3 fix(server): skip the update backup when no migrations are pending (#1711)
pick ac34343 fix(fork): keep the client usable while the attached server restarts (#1712)
pick 8df49c9 fix(web): show server update progress through reconnect (#1713)
fixup 1c56ae7 fixup! fix(web): show server update progress through reconnect (#1713)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/8df49c97f6925ae381ffb7ecd53d3e457e28cc24.msg)"
pick afe2734 fix(server): log memory high-water marks and snapshot the heap at 4 GiB (#1718)
pick e905bde fix(server): record main-thread stacks when memory bursts past 4 GiB (#1719)
pick ba8335a fix(desktop): keep the attached server through a stall (#1721)
pick cb27f62 fix(server): stop syncing the SQLite WAL on every commit (#1722)
pick 3ee6cba fix(server): purge imported v1 rows the v2 runtime never reads (#1723)
pick 021b837 fix(ci): force upstream tag fetches so a moved tag cannot block releases (#1724)
pick a473b09 fix(server): return thread waits when input is needed (#1736)
fixup 7f92cc8 fixup! fix(server): return thread waits when input is needed (#1736)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/a473b09e495cad1421bb0e4cb2a9049f2c24889a.msg)"
pick a958a4e fix(checkout): preserve turns and shared sources during moves (#1733)
pick 7ef7b86 fix(server): retire live work when provider sessions detach (#1734)
pick 0313964 fix(terminal): keep terminals through checkout moves (#1738)
fixup 7b70026 fixup! fix(terminal): keep terminals through checkout moves (#1738)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/0313964c3c2d131d675637a9da738891d0eb0b32.msg)"
pick f622f4a fix(web): pair on same-tab token hash navigation (#1740)
pick 33743d6 fix(zmux): handle workspace basename collisions (#1741)
pick 44b7dbc fix(mcp): hide tools refused by read-only grants (#1742)
fixup 2585ace fixup! fix(mcp): hide tools refused by read-only grants (#1742)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/44b7dbc751a12a7b9fbf585ca10e6daffcf233c1.msg)"
pick cdd21bc fix(web): preserve formatter-clean rich Markdown markers (#1743)
pick 7c7faf7 fix(issues): persist controls, show parents, and search all issues (#1744)
pick 8cd1cd8 fix(upstream-fixes): fork code calls the Effect 4.0.4 randomUUIDv4 function
pick 49e5314 refactor(upstream-fixes): sampled planner statistics defer to upstream ; empty
pick 5b8b460 fix(fork): hook guard matches replayed lines across re-indentation
pick 7d1a2aa fix(web): update everything updates the attached service in client-only mode (#1751)
pick f053d33 refactor(upstream-fixes): fork GitHub issue and attachment paths use upstream's request policy after `pingdotgg#19224`
pick 40e475e refactor(upstream-fixes): fork WS RPCs get their own handler layer under TypeScript's instantiation limit
pick bb196b3 test(upstream-fixes): fork ingestor tests provide upstream's GitVcsDriver (`pingdotgg#20079`) and CommandOutputHub (`pingdotgg#15094`)
pick 953db7a fix(git): name the thread in checkout move refusals (#1762)
pick 284943c fix(web): retarget hidden terminal splits when shown (#1763)
pick 20b4aa4 fix(voice): show redacted provider detail when cleanup fails (#1765)
pick 22e1a1e fix(release): build only AppImages for fork Linux releases (#1768)
pick 6421c4d fix(web): align group title generation budgets (#1769)
pick 4b03981 fix(web): autosave rich Markdown and save files with Ctrl+S (#1770)
fixup c6f0754 fixup! fix(web): autosave rich Markdown and save files with Ctrl+S (#1770)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/4b03981264d1eb75c70dc2f8d0eec2abd9ba58cc.msg)"
pick 545c871 fix(pr): stop turn refreshes from rereading review activity (#1771)
pick 3bb83d3 fix(server): strip desktop launcher environment before providers start (#1766)
pick 10ce76a fix(desktop): ignore installed dependencies in pack watchers (#1767)
pick 7bec9b3 feat(voice-input): live dictation preview for any OpenAI Realtime endpoint (#1775)
fixup 59d45ec fixup! feat(voice-input): live dictation preview for any OpenAI Realtime endpoint (#1775)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/7bec9b3103dee6f3e8b96cc3d6db7ad4fd7c6897.msg)"
pick 10f7a3d fix(upstream-fixes): fork scripts drop section dividers upstream's no-low-value-comments lint refuses
pick eb97ac5 refactor(upstream-fixes): retire the fork's activities purge now upstream drops the table
pick eeef974 fix(ci): install Chromium for release web browser tests (#1778)
pick 74f1379 fix(fork-sync): a file's second hooked commit re-applies; hooks must stay pure insertions (#1780)
pick 179b030 refactor(desktop): retire the fork second-instance buffer for upstream's
pick 53c1d1a fix(fork-sync): hook guard, hook re-apply, and fold check each drop a manual repair (#1782)
pick 260d02a chore(fork-meta): run Macroscope check-run agents as rsi-pr-agent workflows (#1786)
pick 850335e chore(fork-meta): run rsi-pr-agent workflows on the merged pr-change trigger (#1788)
pick 02b0311 fix(web): terminal OSC 52 copies reach the client clipboard (#1790)
pick 448da8c fix(web): use shared custom agent menu heading (#1796)
pick 012cb09 fix(web): compose thread group section headers (#1797)
pick 5238881 fix(web): compose bookmark rows from shared primitives (#1798)
pick 874ffea fix(web): use shared primitives for GitHub issue controls (#1799)
pick 7f24727 fix(voice-input): use InlineButton for processing error dismissal (#1800)
pick b7af052 fix(ci): release web tests pin the upstream release repository (#1802)
pick e1fad55 fix(web): dictation fills AskUserQuestion custom answers (#1803)
pick f27d098 fix(fork-sync): an exact upstream backport may drop its PR link (#1805)
pick fe75d4c fix(fork-meta): browser test siblings keep the .browser infix (#1806)
pick 323a48d test(upstream-fixes): retire fork resume-routing tests superseded by upstream `pingdotgg#21615`
pick ee8c9fe refactor(multi-window): route-navigation seams become marked insertions
pick d2003ea refactor(multi-window): createWindow seams become marked insertions (#1811)
pick 6f5494e fix(fork-meta): fork-fold folds a sync's trailerless fixup into its owner (#1814)
pick 9a87a2c perf(fork-meta): fork scan reads blobs through one git cat-file, not one git show per path (#1815)
pick bcc5966 refactor(server): retire duplicate fork SQLite WAL sync policy (#1823)
pick a3d23d5 refactor(upstream-fixes): retire legacy V1 event purge (#1824)
pick 9b254b3 refactor(desktop): extract remaining window helper seams (#1820)
pick bd63a47 refactor(fork): share one sync attention issue (#1822)
pick a9230b5 refactor(fork-meta): isolate scan git reading (#1819)
pick 5d5f3eb refactor(fork-meta): fork scan refuses every finding; the replay baseline is gone (#1826)
pick 96ccee7 fix(upstream-fixes): agents no longer inherit the live server's port settings (#1828)
pick de66979 refactor(fork-meta): drop the dev-app launcher; agents run upstream dev and place windows with donjor hypr (#1827)
pick ffe001b feat(web): thread groups span projects and can mark a parent thread (#1829)
fixup 0a387aa fixup! feat(web): thread groups span projects and can mark a parent thread (#1829)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/ffe001b8efcae27f417731f0eb795bc49ab8f665.msg)"
pick b7b6592 fix(server): in-thread MCP thread reads show background work (#1831)
fixup 35433a7 fixup! fix(server): in-thread MCP thread reads show background work (#1831)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/b7b6592383dc1952310f272f62c42df013b23313.msg)"
pick 80b6e0f refactor(upstream-fixes): retire the fork's separate WS handler layer for upstream's split table
pick 2cb66a3 fix(fork-sync): reruns continue the rebase, fork:ci gates the candidate, tip commits survive a recreate (#1833)
pick 3f3567c fix(ci): release CLI executable builds on the engines.node pin (#1834)
pick 978268a fix(fork-sync): wait for setup, follow the trunk, root at the main checkout (#1835)
pick 1498f21 fix(desktop): wait for the service update handoff before startup discovery (#1838)
pick 94bb436 fix(server): t3 update waits for a pending remote update (#1837)
pick 41187c6 chore(fork-meta): teach rsi-pr-agent workflows the fork hook seam (#1839)
pick 01547bb feat(server): thread groups live on the server with agent tools (#1840)
fixup a4b48f8 fixup! feat(server): thread groups live on the server with agent tools (#1840)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/01547bbc3c864174352e9cb4d32ec0fa8d6ce372.msg)"
pick 6f33ca0 feat(fork-sync): resolutions persist as git data between runs (#1849)
pick ec5756e feat(fork-sync): a pushed resolution reruns the sync from the trunk (#1850)
pick 21a1043 feat(fork-sync): run the fork-sync skill through rsi-pr-agent (#1848)
pick 434880c feat(fork-sync): a blocked sync labels its attention issue rsi:fork-sync (#1847)
pick 8eadb9c fix(fork-sync): job mode adds the upstream remote before the dry run (#1852)
pick 7c7dc13 fix(fork-meta): give the fork-sync job 45 minutes (#1853)
pick dd1b537 fix(fork-sync): the job always decides and runs from an rsi-pr-agent checkout (#1854)
pick 4c97819 fix(fork-sync): a red candidate starts the fork-sync job (#1855)
pick ecc87d3 fix(fork-sync): carry a retired delete/modify through sync/resolution (#1856)
pick dbbd60d docs(backend-attach): describe the attached primary and its seams (#1861)
pick b069e64 fix(fork-meta): install pins gh's default repository to the fork (#1862)
pick cdd794e test(desktop): the cross-arch probe test ignores a host ELECTRON_RUN_AS_NODE (#1859)
pick fb406ff fix(github-issues): a V2 unlink of a V1 issue link survives compaction (#1858)
pick 7cd6ea1 fix(fork-meta): fold apply keeps cancelling folds and never consults rerere (#1863)
pick 9585bef fix(fork): refresh legacy setup scripts in project settings overrides (#1866)
pick c20d6ef fix(web): client-only Connections hides managed-server controls (#1867)
pick 49b3642 fix(server): fork notice follows a first /compact and native replacement (#1864)
fixup 59a7b46 fixup! fix(server): fork notice follows a first /compact and native replacement (#1864)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/49b3642171ae7e7e1418d76a98b1d60e89fece08.msg)"
pick d4cc6a0 fix(web): group titles name the shared subject, anchored on the parent (#1868)
pick 19b9067 fix(fork-sync): export partial progress before the job limit (#1872)
pick 013ccfc test(server): pin core.excludesFile in the GitManager fixture (#1874)
pick 8bd2820 fix(server): issue MCP tools target a thread like the pull request tools (#1860)
pick cf2e183 fix(server): a stop during the migration backup no longer kills the update (#1873)
pick 7f4c625 fix(terminal): close T3 viewers of a settled lane's removed zmux session (#1876)
pick 6a2844f fix(fork-sync): time the partial export by the job's clock (#1879)
pick e012572 feat(web): group title becomes a tab on its parent thread (#1878)
pick e6cfd23 fix(fork-sync): keep a job to one checkout, no proof clones (#1880)
pick 1231787 fix(web): file saves no longer overwrite newer disk changes (#1869)
pick b6dd169 fix(voice-input): one base URL derives dictation upload and live paths (#1881)
pick f1e284e feat(voice-input): send per-project speech terms to the dictation service (#1882)
pick a8a3005 fix(fork-sync): a job pushes its rebased tip and the driver lands it without replay (#1887)
pick 6424f8a refactor(upstream-fixes): retire the terminal focus-request gate; upstream focusTerminal owns focus
fixup 205c723 fixup! refactor(upstream-fixes): retire the terminal focus-request gate; upstream focusTerminal owns focus
pick d48be70 fix(fork-hooks): the marker scanner skips regex literals (#1888)
pick c7d647c fix(fork-sync): a no-tag rerun keeps the kept worktree's tag (#1889)
pick 998088f fix(fork-sync): a session runs fork:ci on its tip before the hand-off (#1890)
pick 7f8dad0 fix(fork-sync): a sync refuses a tip that lost fork hook lines (#1891)
pick a1c4ebc fix(fork-sync): a landed hook retirement exempts no later sync (#1892)
pick 5498116 fix(fork-ci): fork:ci runs the merge-tree gate, so a session hands off no tip CI refuses (#1893)
pick d30a4fb fix(fork-ci): fork:ci runs knip and typecheck, so a session hands off no tip the Check job refuses (#1894)
pick 664abd2 refactor(upstream-fixes): retire the conversation-fork title guard; upstream titles forks from inherited history
pick e3e4a4e fix(fork-sync): the land run waits out candidate CI, so the report names every red job (#1895)
pick 9701f0c fix(upstream-fixes): storage cleanup treats a broken gitfile as gone on newer git
pick 0d463ba fix(fork-sync): the driver adds the upstream remote, and the skill allows its kept worktree (#1897)
pick 1a03480 fix(fork-sync): a job session leaves no background process before it replies (#1898)
pick a1ea33c refactor(markdown-editing): retire the flush hook supplied by upstream ; empty
pick 1320745 chore(fork-meta): account for existing context without changing the upstream budget
pick 534ee07 docs(fork-meta): track the resolved sync seams in domain scans
pick 66deb1a fix(fork-ci): fork:ci runs every app and package fork test, so a session hands off no tip they fail (#1899)
pick 314965b fix(fork-sync): a job out of budget hands off a red finished marker, so its replay survives the pod (#1901)
pick d3e402c fix(fork-sync): a sync session folds the stack before its hand-off, so the driver stays the only trunk writer (#1902)
pick 8392dad feat(web): every thread group has a parent that carries it to Working (#1904)
pick 9c32ad7 fix(fork-ci): the agent context budget compares against the PR base, not upstream main (#1905)
pick 3f9f73a fix(web): terminal and dictation settings render inside the settings layout (#1910)
pick 42d6a9b docs(fork): README shows parent-led thread groups, live dictation, and current terminal settings (#1912)
pick 02b9845 fix(dev): worktree dev servers keep their own port, process tree, and Electron profile (#1911)
pick c612147 fix(zmux): worktree dev servers leave the developer's zmux sessions alone (#1913)
pick ec27313 fix(fork-sync): a hook finding in a replayed commit restarts the replay instead of looping on tip fixups (#1915)
pick af1f60a fix(web): distinguish window and issue-list project scopes (#1916)

Fork-Sync-Tag: v0.1.4-nightly.20261028.3824
Fork-Sync-Lease: af1f60a
rsi-software-agent Bot pushed a commit that referenced this pull request Oct 29, 2026
pick ffe001b feat(web): thread groups span projects and can mark a parent thread (#1829)
fixup 0a387aa fixup! feat(web): thread groups span projects and can mark a parent thread (#1829)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/ffe001b8efcae27f417731f0eb795bc49ab8f665.msg)"
pick b7b6592 fix(server): in-thread MCP thread reads show background work (#1831)
fixup 35433a7 fixup! fix(server): in-thread MCP thread reads show background work (#1831)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/b7b6592383dc1952310f272f62c42df013b23313.msg)"
pick 80b6e0f refactor(upstream-fixes): retire the fork's separate WS handler layer for upstream's split table
pick 2cb66a3 fix(fork-sync): reruns continue the rebase, fork:ci gates the candidate, tip commits survive a recreate (#1833)
pick 3f3567c fix(ci): release CLI executable builds on the engines.node pin (#1834)
pick 978268a fix(fork-sync): wait for setup, follow the trunk, root at the main checkout (#1835)
pick 1498f21 fix(desktop): wait for the service update handoff before startup discovery (#1838)
pick 94bb436 fix(server): t3 update waits for a pending remote update (#1837)
pick 41187c6 chore(fork-meta): teach rsi-pr-agent workflows the fork hook seam (#1839)
pick 01547bb feat(server): thread groups live on the server with agent tools (#1840)
fixup a4b48f8 fixup! feat(server): thread groups live on the server with agent tools (#1840)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/01547bbc3c864174352e9cb4d32ec0fa8d6ce372.msg)"
pick 6f33ca0 feat(fork-sync): resolutions persist as git data between runs (#1849)
pick ec5756e feat(fork-sync): a pushed resolution reruns the sync from the trunk (#1850)
pick 21a1043 feat(fork-sync): run the fork-sync skill through rsi-pr-agent (#1848)
pick 434880c feat(fork-sync): a blocked sync labels its attention issue rsi:fork-sync (#1847)
pick 8eadb9c fix(fork-sync): job mode adds the upstream remote before the dry run (#1852)
pick 7c7dc13 fix(fork-meta): give the fork-sync job 45 minutes (#1853)
pick dd1b537 fix(fork-sync): the job always decides and runs from an rsi-pr-agent checkout (#1854)
pick 4c97819 fix(fork-sync): a red candidate starts the fork-sync job (#1855)
pick ecc87d3 fix(fork-sync): carry a retired delete/modify through sync/resolution (#1856)
pick dbbd60d docs(backend-attach): describe the attached primary and its seams (#1861)
pick b069e64 fix(fork-meta): install pins gh's default repository to the fork (#1862)
pick cdd794e test(desktop): the cross-arch probe test ignores a host ELECTRON_RUN_AS_NODE (#1859)
pick fb406ff fix(github-issues): a V2 unlink of a V1 issue link survives compaction (#1858)
pick 7cd6ea1 fix(fork-meta): fold apply keeps cancelling folds and never consults rerere (#1863)
pick 9585bef fix(fork): refresh legacy setup scripts in project settings overrides (#1866)
pick c20d6ef fix(web): client-only Connections hides managed-server controls (#1867)
pick 49b3642 fix(server): fork notice follows a first /compact and native replacement (#1864)
fixup 59a7b46 fixup! fix(server): fork notice follows a first /compact and native replacement (#1864)
exec git commit --amend --no-verify -F "$(git rev-parse --git-path fork-sync-folds/49b3642171ae7e7e1418d76a98b1d60e89fece08.msg)"
pick d4cc6a0 fix(web): group titles name the shared subject, anchored on the parent (#1868)
pick 19b9067 fix(fork-sync): export partial progress before the job limit (#1872)
pick 013ccfc test(server): pin core.excludesFile in the GitManager fixture (#1874)
pick 8bd2820 fix(server): issue MCP tools target a thread like the pull request tools (#1860)
pick cf2e183 fix(server): a stop during the migration backup no longer kills the update (#1873)
pick 7f4c625 fix(terminal): close T3 viewers of a settled lane's removed zmux session (#1876)
pick 6a2844f fix(fork-sync): time the partial export by the job's clock (#1879)
pick e012572 feat(web): group title becomes a tab on its parent thread (#1878)
pick e6cfd23 fix(fork-sync): keep a job to one checkout, no proof clones (#1880)
pick 1231787 fix(web): file saves no longer overwrite newer disk changes (#1869)
pick b6dd169 fix(voice-input): one base URL derives dictation upload and live paths (#1881)
pick f1e284e feat(voice-input): send per-project speech terms to the dictation service (#1882)
pick a8a3005 fix(fork-sync): a job pushes its rebased tip and the driver lands it without replay (#1887)
pick 6424f8a refactor(upstream-fixes): retire the terminal focus-request gate; upstream focusTerminal owns focus
fixup 205c723 fixup! refactor(upstream-fixes): retire the terminal focus-request gate; upstream focusTerminal owns focus
pick d48be70 fix(fork-hooks): the marker scanner skips regex literals (#1888)
pick c7d647c fix(fork-sync): a no-tag rerun keeps the kept worktree's tag (#1889)
pick 998088f fix(fork-sync): a session runs fork:ci on its tip before the hand-off (#1890)
pick 7f8dad0 fix(fork-sync): a sync refuses a tip that lost fork hook lines (#1891)
pick a1c4ebc fix(fork-sync): a landed hook retirement exempts no later sync (#1892)
pick 5498116 fix(fork-ci): fork:ci runs the merge-tree gate, so a session hands off no tip CI refuses (#1893)
pick d30a4fb fix(fork-ci): fork:ci runs knip and typecheck, so a session hands off no tip the Check job refuses (#1894)
pick 664abd2 refactor(upstream-fixes): retire the conversation-fork title guard; upstream titles forks from inherited history
pick e3e4a4e fix(fork-sync): the land run waits out candidate CI, so the report names every red job (#1895)
pick 9701f0c fix(upstream-fixes): storage cleanup treats a broken gitfile as gone on newer git
pick 0d463ba fix(fork-sync): the driver adds the upstream remote, and the skill allows its kept worktree (#1897)
pick 1a03480 fix(fork-sync): a job session leaves no background process before it replies (#1898)
pick a1ea33c refactor(markdown-editing): retire the flush hook supplied by upstream ; empty
pick 1320745 chore(fork-meta): account for existing context without changing the upstream budget
pick 534ee07 docs(fork-meta): track the resolved sync seams in domain scans
pick 66deb1a fix(fork-ci): fork:ci runs every app and package fork test, so a session hands off no tip they fail (#1899)
pick 314965b fix(fork-sync): a job out of budget hands off a red finished marker, so its replay survives the pod (#1901)
pick d3e402c fix(fork-sync): a sync session folds the stack before its hand-off, so the driver stays the only trunk writer (#1902)
pick 8392dad feat(web): every thread group has a parent that carries it to Working (#1904)
pick 9c32ad7 fix(fork-ci): the agent context budget compares against the PR base, not upstream main (#1905)
pick 3f9f73a fix(web): terminal and dictation settings render inside the settings layout (#1910)
pick 42d6a9b docs(fork): README shows parent-led thread groups, live dictation, and current terminal settings (#1912)
pick 02b9845 fix(dev): worktree dev servers keep their own port, process tree, and Electron profile (#1911)
pick c612147 fix(zmux): worktree dev servers leave the developer's zmux sessions alone (#1913)
pick ec27313 fix(fork-sync): a hook finding in a replayed commit restarts the replay instead of looping on tip fixups (#1915)
pick af1f60a fix(web): distinguish window and issue-list project scopes (#1916)

Fork-Sync-Tag: v0.1.4-nightly.20261028.3824
Fork-Sync-Lease: af1f60a
rsi-software-agent Bot pushed a commit that referenced this pull request Oct 29, 2026
## Problem

In client-only mode, Connections shows controls for a server this desktop does not manage: Local environment, Network access, and Tailscale HTTPS.
The upstream gate keys on `environment:maintain`, which a standard pairing grants, and the local-environment flag defaults to on.

## Change

New gate `canManageLocalBackendFork` requires all three:

- `environment:maintain` scope
- not client-only
- local environment on

**Callers:**

- Connections rows
- Settings search
- `LocalEnvironmentSetting`: off in client-only

**Rename:** one fork file holds both client-only gates.

```text
apps/web/src/components/settings/
└── connectionsSessionScopes.fork.ts → connectionsClientOnly.fork.ts
```

Upstream edits are one-line fork hooks; the managed path is unchanged.

## Scope and approval

Closes #1773; the human chose a fork fix.

- [ ] **Hidden:** all three rows in client-only
- [ ] **Kept:** managed desktop still shows them

**Also hidden in client-only:** the Version row, behind the same gate.

**Known gap, left as is:** the `local-environment` search item still lists in client-only, then opens Connections without the row.
Tagging it `localBackendManagementOnly` would also hide the way back when a managed desktop turns its local environment off.

## Verification

Dev desktop on the worktree `.t3`; client-only attached a separate `vp run dev:server` on `:14289`.

**Client-only, before** (pre-fix sources): all three rows shown

![client-only-before.png](https://github.com/user-attachments/assets/f98a7157-03c0-43a8-ad75-3ff40681a42d)

**Client-only, after:** rows gone

![client-only-after.png](https://github.com/user-attachments/assets/60e82d4c-6618-40d1-b45d-dd6c04e28e37)

**Managed, after:** rows kept

![managed-kept.png](https://github.com/user-attachments/assets/80841cb4-66d9-4536-a569-bb6047a8ffa9)

**Checks:**

- Gate tests: 8/8 pass
- `vp run check:changed` → exit 1
  - two failing tests, outside this diff
  - `GitManager.test.ts`: CLAUDE.md case, fails alone
  - `markdownPipeline.browser.test.ts`: passes alone
- `vp run fork:ci` → exit 0

Claude Opus 5.5, Claude Code in T3 Code.

## Fork trailers

Fork-Domain: backend-attach
Fork-Tier: bugfix
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
rsi-software-agent Bot pushed a commit that referenced this pull request Oct 29, 2026
## Problem

In client-only mode, Connections shows controls for a server this desktop does not manage: Local environment, Network access, and Tailscale HTTPS.
The upstream gate keys on `environment:maintain`, which a standard pairing grants, and the local-environment flag defaults to on.

## Change

New gate `canManageLocalBackendFork` requires all three:

- `environment:maintain` scope
- not client-only
- local environment on

**Callers:**

- Connections rows
- Settings search
- `LocalEnvironmentSetting`: off in client-only

**Rename:** one fork file holds both client-only gates.

```text
apps/web/src/components/settings/
└── connectionsSessionScopes.fork.ts → connectionsClientOnly.fork.ts
```

Upstream edits are one-line fork hooks; the managed path is unchanged.

## Scope and approval

Closes #1773; the human chose a fork fix.

- [ ] **Hidden:** all three rows in client-only
- [ ] **Kept:** managed desktop still shows them

**Also hidden in client-only:** the Version row, behind the same gate.

**Known gap, left as is:** the `local-environment` search item still lists in client-only, then opens Connections without the row.
Tagging it `localBackendManagementOnly` would also hide the way back when a managed desktop turns its local environment off.

## Verification

Dev desktop on the worktree `.t3`; client-only attached a separate `vp run dev:server` on `:14289`.

**Client-only, before** (pre-fix sources): all three rows shown

![client-only-before.png](https://github.com/user-attachments/assets/f98a7157-03c0-43a8-ad75-3ff40681a42d)

**Client-only, after:** rows gone

![client-only-after.png](https://github.com/user-attachments/assets/60e82d4c-6618-40d1-b45d-dd6c04e28e37)

**Managed, after:** rows kept

![managed-kept.png](https://github.com/user-attachments/assets/80841cb4-66d9-4536-a569-bb6047a8ffa9)

**Checks:**

- Gate tests: 8/8 pass
- `vp run check:changed` → exit 1
  - two failing tests, outside this diff
  - `GitManager.test.ts`: CLAUDE.md case, fails alone
  - `markdownPipeline.browser.test.ts`: passes alone
- `vp run fork:ci` → exit 0

Claude Opus 5.5, Claude Code in T3 Code.

## Fork trailers

Fork-Domain: backend-attach
Fork-Tier: bugfix
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
rsi-software-agent Bot pushed a commit that referenced this pull request Oct 29, 2026
## Problem

In client-only mode, Connections shows controls for a server this desktop does not manage: Local environment, Network access, and Tailscale HTTPS.
The upstream gate keys on `environment:maintain`, which a standard pairing grants, and the local-environment flag defaults to on.

## Change

New gate `canManageLocalBackendFork` requires all three:

- `environment:maintain` scope
- not client-only
- local environment on

**Callers:**

- Connections rows
- Settings search
- `LocalEnvironmentSetting`: off in client-only

**Rename:** one fork file holds both client-only gates.

```text
apps/web/src/components/settings/
└── connectionsSessionScopes.fork.ts → connectionsClientOnly.fork.ts
```

Upstream edits are one-line fork hooks; the managed path is unchanged.

## Scope and approval

Closes #1773; the human chose a fork fix.

- [ ] **Hidden:** all three rows in client-only
- [ ] **Kept:** managed desktop still shows them

**Also hidden in client-only:** the Version row, behind the same gate.

**Known gap, left as is:** the `local-environment` search item still lists in client-only, then opens Connections without the row.
Tagging it `localBackendManagementOnly` would also hide the way back when a managed desktop turns its local environment off.

## Verification

Dev desktop on the worktree `.t3`; client-only attached a separate `vp run dev:server` on `:14289`.

**Client-only, before** (pre-fix sources): all three rows shown

![client-only-before.png](https://github.com/user-attachments/assets/f98a7157-03c0-43a8-ad75-3ff40681a42d)

**Client-only, after:** rows gone

![client-only-after.png](https://github.com/user-attachments/assets/60e82d4c-6618-40d1-b45d-dd6c04e28e37)

**Managed, after:** rows kept

![managed-kept.png](https://github.com/user-attachments/assets/80841cb4-66d9-4536-a569-bb6047a8ffa9)

**Checks:**

- Gate tests: 8/8 pass
- `vp run check:changed` → exit 1
  - two failing tests, outside this diff
  - `GitManager.test.ts`: CLAUDE.md case, fails alone
  - `markdownPipeline.browser.test.ts`: passes alone
- `vp run fork:ci` → exit 0

Claude Opus 5.5, Claude Code in T3 Code.

## Fork trailers

Fork-Domain: backend-attach
Fork-Tier: bugfix
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
rsi-software-agent Bot pushed a commit that referenced this pull request Oct 30, 2026
## Problem

In client-only mode, Connections shows controls for a server this desktop does not manage: Local environment, Network access, and Tailscale HTTPS.
The upstream gate keys on `environment:maintain`, which a standard pairing grants, and the local-environment flag defaults to on.

## Change

New gate `canManageLocalBackendFork` requires all three:

- `environment:maintain` scope
- not client-only
- local environment on

**Callers:**

- Connections rows
- Settings search
- `LocalEnvironmentSetting`: off in client-only

**Rename:** one fork file holds both client-only gates.

```text
apps/web/src/components/settings/
└── connectionsSessionScopes.fork.ts → connectionsClientOnly.fork.ts
```

Upstream edits are one-line fork hooks; the managed path is unchanged.

## Scope and approval

Closes #1773; the human chose a fork fix.

- [ ] **Hidden:** all three rows in client-only
- [ ] **Kept:** managed desktop still shows them

**Also hidden in client-only:** the Version row, behind the same gate.

**Known gap, left as is:** the `local-environment` search item still lists in client-only, then opens Connections without the row.
Tagging it `localBackendManagementOnly` would also hide the way back when a managed desktop turns its local environment off.

## Verification

Dev desktop on the worktree `.t3`; client-only attached a separate `vp run dev:server` on `:14289`.

**Client-only, before** (pre-fix sources): all three rows shown

![client-only-before.png](https://github.com/user-attachments/assets/f98a7157-03c0-43a8-ad75-3ff40681a42d)

**Client-only, after:** rows gone

![client-only-after.png](https://github.com/user-attachments/assets/60e82d4c-6618-40d1-b45d-dd6c04e28e37)

**Managed, after:** rows kept

![managed-kept.png](https://github.com/user-attachments/assets/80841cb4-66d9-4536-a569-bb6047a8ffa9)

**Checks:**

- Gate tests: 8/8 pass
- `vp run check:changed` → exit 1
  - two failing tests, outside this diff
  - `GitManager.test.ts`: CLAUDE.md case, fails alone
  - `markdownPipeline.browser.test.ts`: passes alone
- `vp run fork:ci` → exit 0

Claude Opus 5.5, Claude Code in T3 Code.

## Fork trailers

Fork-Domain: backend-attach
Fork-Tier: bugfix
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
rsi-software-agent Bot pushed a commit that referenced this pull request Oct 30, 2026
## Problem

In client-only mode, Connections shows controls for a server this desktop does not manage: Local environment, Network access, and Tailscale HTTPS.
The upstream gate keys on `environment:maintain`, which a standard pairing grants, and the local-environment flag defaults to on.

## Change

New gate `canManageLocalBackendFork` requires all three:

- `environment:maintain` scope
- not client-only
- local environment on

**Callers:**

- Connections rows
- Settings search
- `LocalEnvironmentSetting`: off in client-only

**Rename:** one fork file holds both client-only gates.

```text
apps/web/src/components/settings/
└── connectionsSessionScopes.fork.ts → connectionsClientOnly.fork.ts
```

Upstream edits are one-line fork hooks; the managed path is unchanged.

## Scope and approval

Closes #1773; the human chose a fork fix.

- [ ] **Hidden:** all three rows in client-only
- [ ] **Kept:** managed desktop still shows them

**Also hidden in client-only:** the Version row, behind the same gate.

**Known gap, left as is:** the `local-environment` search item still lists in client-only, then opens Connections without the row.
Tagging it `localBackendManagementOnly` would also hide the way back when a managed desktop turns its local environment off.

## Verification

Dev desktop on the worktree `.t3`; client-only attached a separate `vp run dev:server` on `:14289`.

**Client-only, before** (pre-fix sources): all three rows shown

![client-only-before.png](https://github.com/user-attachments/assets/f98a7157-03c0-43a8-ad75-3ff40681a42d)

**Client-only, after:** rows gone

![client-only-after.png](https://github.com/user-attachments/assets/60e82d4c-6618-40d1-b45d-dd6c04e28e37)

**Managed, after:** rows kept

![managed-kept.png](https://github.com/user-attachments/assets/80841cb4-66d9-4536-a569-bb6047a8ffa9)

**Checks:**

- Gate tests: 8/8 pass
- `vp run check:changed` → exit 1
  - two failing tests, outside this diff
  - `GitManager.test.ts`: CLAUDE.md case, fails alone
  - `markdownPipeline.browser.test.ts`: passes alone
- `vp run fork:ci` → exit 0

Claude Opus 5.5, Claude Code in T3 Code.

## Fork trailers

Fork-Domain: backend-attach
Fork-Tier: bugfix
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
rsi-software-agent Bot pushed a commit that referenced this pull request Oct 30, 2026
## Problem

In client-only mode, Connections shows controls for a server this desktop does not manage: Local environment, Network access, and Tailscale HTTPS.
The upstream gate keys on `environment:maintain`, which a standard pairing grants, and the local-environment flag defaults to on.

## Change

New gate `canManageLocalBackendFork` requires all three:

- `environment:maintain` scope
- not client-only
- local environment on

**Callers:**

- Connections rows
- Settings search
- `LocalEnvironmentSetting`: off in client-only

**Rename:** one fork file holds both client-only gates.

```text
apps/web/src/components/settings/
└── connectionsSessionScopes.fork.ts → connectionsClientOnly.fork.ts
```

Upstream edits are one-line fork hooks; the managed path is unchanged.

## Scope and approval

Closes #1773; the human chose a fork fix.

- [ ] **Hidden:** all three rows in client-only
- [ ] **Kept:** managed desktop still shows them

**Also hidden in client-only:** the Version row, behind the same gate.

**Known gap, left as is:** the `local-environment` search item still lists in client-only, then opens Connections without the row.
Tagging it `localBackendManagementOnly` would also hide the way back when a managed desktop turns its local environment off.

## Verification

Dev desktop on the worktree `.t3`; client-only attached a separate `vp run dev:server` on `:14289`.

**Client-only, before** (pre-fix sources): all three rows shown

![client-only-before.png](https://github.com/user-attachments/assets/f98a7157-03c0-43a8-ad75-3ff40681a42d)

**Client-only, after:** rows gone

![client-only-after.png](https://github.com/user-attachments/assets/60e82d4c-6618-40d1-b45d-dd6c04e28e37)

**Managed, after:** rows kept

![managed-kept.png](https://github.com/user-attachments/assets/80841cb4-66d9-4536-a569-bb6047a8ffa9)

**Checks:**

- Gate tests: 8/8 pass
- `vp run check:changed` → exit 1
  - two failing tests, outside this diff
  - `GitManager.test.ts`: CLAUDE.md case, fails alone
  - `markdownPipeline.browser.test.ts`: passes alone
- `vp run fork:ci` → exit 0

Claude Opus 5.5, Claude Code in T3 Code.

## Fork trailers

Fork-Domain: backend-attach
Fork-Tier: bugfix
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
rsi-software-agent Bot pushed a commit that referenced this pull request Oct 31, 2026
## Problem

In client-only mode, Connections shows controls for a server this desktop does not manage: Local environment, Network access, and Tailscale HTTPS.
The upstream gate keys on `environment:maintain`, which a standard pairing grants, and the local-environment flag defaults to on.

## Change

New gate `canManageLocalBackendFork` requires all three:

- `environment:maintain` scope
- not client-only
- local environment on

**Callers:**

- Connections rows
- Settings search
- `LocalEnvironmentSetting`: off in client-only

**Rename:** one fork file holds both client-only gates.

```text
apps/web/src/components/settings/
└── connectionsSessionScopes.fork.ts → connectionsClientOnly.fork.ts
```

Upstream edits are one-line fork hooks; the managed path is unchanged.

## Scope and approval

Closes #1773; the human chose a fork fix.

- [ ] **Hidden:** all three rows in client-only
- [ ] **Kept:** managed desktop still shows them

**Also hidden in client-only:** the Version row, behind the same gate.

**Known gap, left as is:** the `local-environment` search item still lists in client-only, then opens Connections without the row.
Tagging it `localBackendManagementOnly` would also hide the way back when a managed desktop turns its local environment off.

## Verification

Dev desktop on the worktree `.t3`; client-only attached a separate `vp run dev:server` on `:14289`.

**Client-only, before** (pre-fix sources): all three rows shown

![client-only-before.png](https://github.com/user-attachments/assets/f98a7157-03c0-43a8-ad75-3ff40681a42d)

**Client-only, after:** rows gone

![client-only-after.png](https://github.com/user-attachments/assets/60e82d4c-6618-40d1-b45d-dd6c04e28e37)

**Managed, after:** rows kept

![managed-kept.png](https://github.com/user-attachments/assets/80841cb4-66d9-4536-a569-bb6047a8ffa9)

**Checks:**

- Gate tests: 8/8 pass
- `vp run check:changed` → exit 1
  - two failing tests, outside this diff
  - `GitManager.test.ts`: CLAUDE.md case, fails alone
  - `markdownPipeline.browser.test.ts`: passes alone
- `vp run fork:ci` → exit 0

Claude Opus 5.5, Claude Code in T3 Code.

## Fork trailers

Fork-Domain: backend-attach
Fork-Tier: bugfix
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
donjor added a commit that referenced this pull request Nov 6, 2026
## Problem

In client-only mode, Connections shows controls for a server this desktop does not manage: Local environment, Network access, and Tailscale HTTPS.
The upstream gate keys on `environment:maintain`, which a standard pairing grants, and the local-environment flag defaults to on.

## Change

New gate `canManageLocalBackendFork` requires all three:

- `environment:maintain` scope
- not client-only
- local environment on

**Callers:**

- Connections rows
- Settings search
- `LocalEnvironmentSetting`: off in client-only

**Rename:** one fork file holds both client-only gates.

```text
apps/web/src/components/settings/
└── connectionsSessionScopes.fork.ts → connectionsClientOnly.fork.ts
```

Upstream edits are one-line fork hooks; the managed path is unchanged.

## Scope and approval

Closes #1773; the human chose a fork fix.

- [ ] **Hidden:** all three rows in client-only
- [ ] **Kept:** managed desktop still shows them

**Also hidden in client-only:** the Version row, behind the same gate.

**Known gap, left as is:** the `local-environment` search item still lists in client-only, then opens Connections without the row.
Tagging it `localBackendManagementOnly` would also hide the way back when a managed desktop turns its local environment off.

## Verification

Dev desktop on the worktree `.t3`; client-only attached a separate `vp run dev:server` on `:14289`.

**Client-only, before** (pre-fix sources): all three rows shown

![client-only-before.png](https://github.com/user-attachments/assets/f98a7157-03c0-43a8-ad75-3ff40681a42d)

**Client-only, after:** rows gone

![client-only-after.png](https://github.com/user-attachments/assets/60e82d4c-6618-40d1-b45d-dd6c04e28e37)

**Managed, after:** rows kept

![managed-kept.png](https://github.com/user-attachments/assets/80841cb4-66d9-4536-a569-bb6047a8ffa9)

**Checks:**

- Gate tests: 8/8 pass
- `vp run check:changed` → exit 1
  - two failing tests, outside this diff
  - `GitManager.test.ts`: CLAUDE.md case, fails alone
  - `markdownPipeline.browser.test.ts`: passes alone
- `vp run fork:ci` → exit 0

Claude Opus 5.5, Claude Code in T3 Code.

## Fork trailers

Fork-Domain: backend-attach
Fork-Tier: bugfix
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 6, 2026
## Problem

In client-only mode, Connections shows controls for a server this desktop does not manage: Local environment, Network access, and Tailscale HTTPS.
The upstream gate keys on `environment:maintain`, which a standard pairing grants, and the local-environment flag defaults to on.

## Change

New gate `canManageLocalBackendFork` requires all three:

- `environment:maintain` scope
- not client-only
- local environment on

**Callers:**

- Connections rows
- Settings search
- `LocalEnvironmentSetting`: off in client-only

**Rename:** one fork file holds both client-only gates.

```text
apps/web/src/components/settings/
└── connectionsSessionScopes.fork.ts → connectionsClientOnly.fork.ts
```

Upstream edits are one-line fork hooks; the managed path is unchanged.

## Scope and approval

Closes #1773; the human chose a fork fix.

- [ ] **Hidden:** all three rows in client-only
- [ ] **Kept:** managed desktop still shows them

**Also hidden in client-only:** the Version row, behind the same gate.

**Known gap, left as is:** the `local-environment` search item still lists in client-only, then opens Connections without the row.
Tagging it `localBackendManagementOnly` would also hide the way back when a managed desktop turns its local environment off.

## Verification

Dev desktop on the worktree `.t3`; client-only attached a separate `vp run dev:server` on `:14289`.

**Client-only, before** (pre-fix sources): all three rows shown

![client-only-before.png](https://github.com/user-attachments/assets/f98a7157-03c0-43a8-ad75-3ff40681a42d)

**Client-only, after:** rows gone

![client-only-after.png](https://github.com/user-attachments/assets/60e82d4c-6618-40d1-b45d-dd6c04e28e37)

**Managed, after:** rows kept

![managed-kept.png](https://github.com/user-attachments/assets/80841cb4-66d9-4536-a569-bb6047a8ffa9)

**Checks:**

- Gate tests: 8/8 pass
- `vp run check:changed` → exit 1
  - two failing tests, outside this diff
  - `GitManager.test.ts`: CLAUDE.md case, fails alone
  - `markdownPipeline.browser.test.ts`: passes alone
- `vp run fork:ci` → exit 0

Claude Opus 5.5, Claude Code in T3 Code.

## Fork trailers

Fork-Domain: backend-attach
Fork-Tier: bugfix
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 6, 2026
## Problem

In client-only mode, Connections shows controls for a server this desktop does not manage: Local environment, Network access, and Tailscale HTTPS.
The upstream gate keys on `environment:maintain`, which a standard pairing grants, and the local-environment flag defaults to on.

## Change

New gate `canManageLocalBackendFork` requires all three:

- `environment:maintain` scope
- not client-only
- local environment on

**Callers:**

- Connections rows
- Settings search
- `LocalEnvironmentSetting`: off in client-only

**Rename:** one fork file holds both client-only gates.

```text
apps/web/src/components/settings/
└── connectionsSessionScopes.fork.ts → connectionsClientOnly.fork.ts
```

Upstream edits are one-line fork hooks; the managed path is unchanged.

## Scope and approval

Closes #1773; the human chose a fork fix.

- [ ] **Hidden:** all three rows in client-only
- [ ] **Kept:** managed desktop still shows them

**Also hidden in client-only:** the Version row, behind the same gate.

**Known gap, left as is:** the `local-environment` search item still lists in client-only, then opens Connections without the row.
Tagging it `localBackendManagementOnly` would also hide the way back when a managed desktop turns its local environment off.

## Verification

Dev desktop on the worktree `.t3`; client-only attached a separate `vp run dev:server` on `:14289`.

**Client-only, before** (pre-fix sources): all three rows shown

![client-only-before.png](https://github.com/user-attachments/assets/f98a7157-03c0-43a8-ad75-3ff40681a42d)

**Client-only, after:** rows gone

![client-only-after.png](https://github.com/user-attachments/assets/60e82d4c-6618-40d1-b45d-dd6c04e28e37)

**Managed, after:** rows kept

![managed-kept.png](https://github.com/user-attachments/assets/80841cb4-66d9-4536-a569-bb6047a8ffa9)

**Checks:**

- Gate tests: 8/8 pass
- `vp run check:changed` → exit 1
  - two failing tests, outside this diff
  - `GitManager.test.ts`: CLAUDE.md case, fails alone
  - `markdownPipeline.browser.test.ts`: passes alone
- `vp run fork:ci` → exit 0

Claude Opus 5.5, Claude Code in T3 Code.

## Fork trailers

Fork-Domain: backend-attach
Fork-Tier: bugfix
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 6, 2026
## Problem

In client-only mode, Connections shows controls for a server this desktop does not manage: Local environment, Network access, and Tailscale HTTPS.
The upstream gate keys on `environment:maintain`, which a standard pairing grants, and the local-environment flag defaults to on.

## Change

New gate `canManageLocalBackendFork` requires all three:

- `environment:maintain` scope
- not client-only
- local environment on

**Callers:**

- Connections rows
- Settings search
- `LocalEnvironmentSetting`: off in client-only

**Rename:** one fork file holds both client-only gates.

```text
apps/web/src/components/settings/
└── connectionsSessionScopes.fork.ts → connectionsClientOnly.fork.ts
```

Upstream edits are one-line fork hooks; the managed path is unchanged.

## Scope and approval

Closes #1773; the human chose a fork fix.

- [ ] **Hidden:** all three rows in client-only
- [ ] **Kept:** managed desktop still shows them

**Also hidden in client-only:** the Version row, behind the same gate.

**Known gap, left as is:** the `local-environment` search item still lists in client-only, then opens Connections without the row.
Tagging it `localBackendManagementOnly` would also hide the way back when a managed desktop turns its local environment off.

## Verification

Dev desktop on the worktree `.t3`; client-only attached a separate `vp run dev:server` on `:14289`.

**Client-only, before** (pre-fix sources): all three rows shown

![client-only-before.png](https://github.com/user-attachments/assets/f98a7157-03c0-43a8-ad75-3ff40681a42d)

**Client-only, after:** rows gone

![client-only-after.png](https://github.com/user-attachments/assets/60e82d4c-6618-40d1-b45d-dd6c04e28e37)

**Managed, after:** rows kept

![managed-kept.png](https://github.com/user-attachments/assets/80841cb4-66d9-4536-a569-bb6047a8ffa9)

**Checks:**

- Gate tests: 8/8 pass
- `vp run check:changed` → exit 1
  - two failing tests, outside this diff
  - `GitManager.test.ts`: CLAUDE.md case, fails alone
  - `markdownPipeline.browser.test.ts`: passes alone
- `vp run fork:ci` → exit 0

Claude Opus 5.5, Claude Code in T3 Code.

## Fork trailers

Fork-Domain: backend-attach
Fork-Tier: bugfix
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 6, 2026
## Problem

In client-only mode, Connections shows controls for a server this desktop does not manage: Local environment, Network access, and Tailscale HTTPS.
The upstream gate keys on `environment:maintain`, which a standard pairing grants, and the local-environment flag defaults to on.

## Change

New gate `canManageLocalBackendFork` requires all three:

- `environment:maintain` scope
- not client-only
- local environment on

**Callers:**

- Connections rows
- Settings search
- `LocalEnvironmentSetting`: off in client-only

**Rename:** one fork file holds both client-only gates.

```text
apps/web/src/components/settings/
└── connectionsSessionScopes.fork.ts → connectionsClientOnly.fork.ts
```

Upstream edits are one-line fork hooks; the managed path is unchanged.

## Scope and approval

Closes #1773; the human chose a fork fix.

- [ ] **Hidden:** all three rows in client-only
- [ ] **Kept:** managed desktop still shows them

**Also hidden in client-only:** the Version row, behind the same gate.

**Known gap, left as is:** the `local-environment` search item still lists in client-only, then opens Connections without the row.
Tagging it `localBackendManagementOnly` would also hide the way back when a managed desktop turns its local environment off.

## Verification

Dev desktop on the worktree `.t3`; client-only attached a separate `vp run dev:server` on `:14289`.

**Client-only, before** (pre-fix sources): all three rows shown

![client-only-before.png](https://github.com/user-attachments/assets/f98a7157-03c0-43a8-ad75-3ff40681a42d)

**Client-only, after:** rows gone

![client-only-after.png](https://github.com/user-attachments/assets/60e82d4c-6618-40d1-b45d-dd6c04e28e37)

**Managed, after:** rows kept

![managed-kept.png](https://github.com/user-attachments/assets/80841cb4-66d9-4536-a569-bb6047a8ffa9)

**Checks:**

- Gate tests: 8/8 pass
- `vp run check:changed` → exit 1
  - two failing tests, outside this diff
  - `GitManager.test.ts`: CLAUDE.md case, fails alone
  - `markdownPipeline.browser.test.ts`: passes alone
- `vp run fork:ci` → exit 0

Claude Opus 5.5, Claude Code in T3 Code.

## Fork trailers

Fork-Domain: backend-attach
Fork-Tier: bugfix
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 6, 2026
## Problem

In client-only mode, Connections shows controls for a server this desktop does not manage: Local environment, Network access, and Tailscale HTTPS.
The upstream gate keys on `environment:maintain`, which a standard pairing grants, and the local-environment flag defaults to on.

## Change

New gate `canManageLocalBackendFork` requires all three:

- `environment:maintain` scope
- not client-only
- local environment on

**Callers:**

- Connections rows
- Settings search
- `LocalEnvironmentSetting`: off in client-only

**Rename:** one fork file holds both client-only gates.

```text
apps/web/src/components/settings/
└── connectionsSessionScopes.fork.ts → connectionsClientOnly.fork.ts
```

Upstream edits are one-line fork hooks; the managed path is unchanged.

## Scope and approval

Closes #1773; the human chose a fork fix.

- [ ] **Hidden:** all three rows in client-only
- [ ] **Kept:** managed desktop still shows them

**Also hidden in client-only:** the Version row, behind the same gate.

**Known gap, left as is:** the `local-environment` search item still lists in client-only, then opens Connections without the row.
Tagging it `localBackendManagementOnly` would also hide the way back when a managed desktop turns its local environment off.

## Verification

Dev desktop on the worktree `.t3`; client-only attached a separate `vp run dev:server` on `:14289`.

**Client-only, before** (pre-fix sources): all three rows shown

![client-only-before.png](https://github.com/user-attachments/assets/f98a7157-03c0-43a8-ad75-3ff40681a42d)

**Client-only, after:** rows gone

![client-only-after.png](https://github.com/user-attachments/assets/60e82d4c-6618-40d1-b45d-dd6c04e28e37)

**Managed, after:** rows kept

![managed-kept.png](https://github.com/user-attachments/assets/80841cb4-66d9-4536-a569-bb6047a8ffa9)

**Checks:**

- Gate tests: 8/8 pass
- `vp run check:changed` → exit 1
  - two failing tests, outside this diff
  - `GitManager.test.ts`: CLAUDE.md case, fails alone
  - `markdownPipeline.browser.test.ts`: passes alone
- `vp run fork:ci` → exit 0

Claude Opus 5.5, Claude Code in T3 Code.

## Fork trailers

Fork-Domain: backend-attach
Fork-Tier: bugfix
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
donjor added a commit that referenced this pull request Nov 9, 2026
Desktop auto-pairing always failed against a local server started with no explicit host. `pair --json` emitted `localhost` while runtime state recorded `127.0.0.1`, and the desktop compares the two origins strictly.

## Change

`apps/server/src/cli/pair.ts` resolves the machine-readable pairing URL through the recorded origin when no host is set. Human output, `resolveDirectPairingBaseUrl`, the upstream localhost fallback, and the desktop's strict comparison are untouched, so a `--host 0.0.0.0` server still records an externally reachable connection string and that stays the pairing URL.

## Checks

```text
vp test run apps/server/src/cli/pair.fork.test.ts   3 passed
vp run fork:delta --check                           ok: 220 fork commits tagged
```

## Grounding

A real `t3 serve` on the branch, started with no `--host` and its own base dir, is exactly the reported precondition: the runtime record carries no `host` and an origin of `127.0.0.1`.

```json
{"version":1,"pid":3596040,"port":7911,"origin":"http://127.0.0.1:7911", ...}
```

`--json` and the human path were then read off that one server. The human path still resolves through `resolveDirectPairingBaseUrl`, so it is the pre-fix value for the same state.

```text
pair           Pairing URL: http://localhost:7911/pair#token=…     (upstream base, untouched)
pair --json    "pairingUrl": "http://127.0.0.1:7911/pair#token=…"
               "origin":     "http://127.0.0.1:7911"
```

Applying the desktop's own check — `pairingUrl.origin === httpBaseUrl.origin`, where `httpBaseUrl` is the recorded `origin` (`DesktopRunningLocalServers.ts:83,174`):

| pairing base | pairingUrl.origin | matches recorded origin |
| --- | --- | --- |
| upstream base (pre-fix `--json`) | `http://localhost:7911` | no |
| recorded origin (this change) | `http://127.0.0.1:7911` | yes |

**A configured host still keeps the upstream base.** A second server on the same branch with `--host 0.0.0.0` records the host, so `--json` takes the untouched path and resolves to the externally reachable address rather than to loopback.

```json
state    {"host":"0.0.0.0","port":7912,"origin":"http://127.0.0.1:7912"}
--json   "pairingUrl": "http://192.168.0.60:7912/pair#token=…"
```

One observation outside this change: for that wildcard server `pairingUrl.origin` and the recorded `origin` still differ, because upstream records loopback as the origin whatever the bind address is. The desktop's local-server auto-pairing would reject it for the same reason it rejected the no-host case. That is upstream behaviour on a path this change deliberately leaves alone, and it is not what #902 reported; filing it separately if it proves to matter.

Refs #902

https://claude.ai/code/session_012duArYgsDfEoAPcp9ZUXdr

## Fork trailers

Squashes:

- bc24ddf68a5 fix(server): pair --json through the recorded origin when no host is set
- 8618b0b9460 fix(client-runtime): an attached primary no longer reconnects on every platform poll (#1477) (#1477)
- 2f240119896 fix(fork): keep the client usable while the attached server restarts (#1712) (#1712)
- 6b19887bb4b fix(desktop): keep the attached server through a stall (#1721) (#1721)
- bcb0971cfd2 fix(web): update everything updates the attached service in client-only mode (#1751) (#1751)
- 6f68726fc5f fix(desktop): wait for the service update handoff before startup discovery (#1838) (#1838)
- a67ac66cef1 docs(backend-attach): describe the attached primary and its seams (#1861) (#1861)
- ba31fdd764e fix(web): client-only Connections hides managed-server controls (#1867) (#1867)
- 8fcfc1dd625 refactor(desktop)!: retire backend-attach for upstream Local-off (#1954) (#1954)

Fork-Domain: backend-attach
Fork-Tier: core
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 9, 2026
Desktop auto-pairing always failed against a local server started with no explicit host. `pair --json` emitted `localhost` while runtime state recorded `127.0.0.1`, and the desktop compares the two origins strictly.

## Change

`apps/server/src/cli/pair.ts` resolves the machine-readable pairing URL through the recorded origin when no host is set. Human output, `resolveDirectPairingBaseUrl`, the upstream localhost fallback, and the desktop's strict comparison are untouched, so a `--host 0.0.0.0` server still records an externally reachable connection string and that stays the pairing URL.

## Checks

```text
vp test run apps/server/src/cli/pair.fork.test.ts   3 passed
vp run fork:delta --check                           ok: 220 fork commits tagged
```

## Grounding

A real `t3 serve` on the branch, started with no `--host` and its own base dir, is exactly the reported precondition: the runtime record carries no `host` and an origin of `127.0.0.1`.

```json
{"version":1,"pid":3596040,"port":7911,"origin":"http://127.0.0.1:7911", ...}
```

`--json` and the human path were then read off that one server. The human path still resolves through `resolveDirectPairingBaseUrl`, so it is the pre-fix value for the same state.

```text
pair           Pairing URL: http://localhost:7911/pair#token=…     (upstream base, untouched)
pair --json    "pairingUrl": "http://127.0.0.1:7911/pair#token=…"
               "origin":     "http://127.0.0.1:7911"
```

Applying the desktop's own check — `pairingUrl.origin === httpBaseUrl.origin`, where `httpBaseUrl` is the recorded `origin` (`DesktopRunningLocalServers.ts:83,174`):

| pairing base | pairingUrl.origin | matches recorded origin |
| --- | --- | --- |
| upstream base (pre-fix `--json`) | `http://localhost:7911` | no |
| recorded origin (this change) | `http://127.0.0.1:7911` | yes |

**A configured host still keeps the upstream base.** A second server on the same branch with `--host 0.0.0.0` records the host, so `--json` takes the untouched path and resolves to the externally reachable address rather than to loopback.

```json
state    {"host":"0.0.0.0","port":7912,"origin":"http://127.0.0.1:7912"}
--json   "pairingUrl": "http://192.168.0.60:7912/pair#token=…"
```

One observation outside this change: for that wildcard server `pairingUrl.origin` and the recorded `origin` still differ, because upstream records loopback as the origin whatever the bind address is. The desktop's local-server auto-pairing would reject it for the same reason it rejected the no-host case. That is upstream behaviour on a path this change deliberately leaves alone, and it is not what #902 reported; filing it separately if it proves to matter.

Refs #902

https://claude.ai/code/session_012duArYgsDfEoAPcp9ZUXdr

## Fork trailers

Squashes:

- bc24ddf68a5 fix(server): pair --json through the recorded origin when no host is set
- 8618b0b9460 fix(client-runtime): an attached primary no longer reconnects on every platform poll (#1477) (#1477)
- 2f240119896 fix(fork): keep the client usable while the attached server restarts (#1712) (#1712)
- 6b19887bb4b fix(desktop): keep the attached server through a stall (#1721) (#1721)
- bcb0971cfd2 fix(web): update everything updates the attached service in client-only mode (#1751) (#1751)
- 6f68726fc5f fix(desktop): wait for the service update handoff before startup discovery (#1838) (#1838)
- a67ac66cef1 docs(backend-attach): describe the attached primary and its seams (#1861) (#1861)
- ba31fdd764e fix(web): client-only Connections hides managed-server controls (#1867) (#1867)
- 8fcfc1dd625 refactor(desktop)!: retire backend-attach for upstream Local-off (#1954) (#1954)

Fork-Domain: backend-attach
Fork-Tier: core
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 9, 2026
Desktop auto-pairing always failed against a local server started with no explicit host. `pair --json` emitted `localhost` while runtime state recorded `127.0.0.1`, and the desktop compares the two origins strictly.

## Change

`apps/server/src/cli/pair.ts` resolves the machine-readable pairing URL through the recorded origin when no host is set. Human output, `resolveDirectPairingBaseUrl`, the upstream localhost fallback, and the desktop's strict comparison are untouched, so a `--host 0.0.0.0` server still records an externally reachable connection string and that stays the pairing URL.

## Checks

```text
vp test run apps/server/src/cli/pair.fork.test.ts   3 passed
vp run fork:delta --check                           ok: 220 fork commits tagged
```

## Grounding

A real `t3 serve` on the branch, started with no `--host` and its own base dir, is exactly the reported precondition: the runtime record carries no `host` and an origin of `127.0.0.1`.

```json
{"version":1,"pid":3596040,"port":7911,"origin":"http://127.0.0.1:7911", ...}
```

`--json` and the human path were then read off that one server. The human path still resolves through `resolveDirectPairingBaseUrl`, so it is the pre-fix value for the same state.

```text
pair           Pairing URL: http://localhost:7911/pair#token=…     (upstream base, untouched)
pair --json    "pairingUrl": "http://127.0.0.1:7911/pair#token=…"
               "origin":     "http://127.0.0.1:7911"
```

Applying the desktop's own check — `pairingUrl.origin === httpBaseUrl.origin`, where `httpBaseUrl` is the recorded `origin` (`DesktopRunningLocalServers.ts:83,174`):

| pairing base | pairingUrl.origin | matches recorded origin |
| --- | --- | --- |
| upstream base (pre-fix `--json`) | `http://localhost:7911` | no |
| recorded origin (this change) | `http://127.0.0.1:7911` | yes |

**A configured host still keeps the upstream base.** A second server on the same branch with `--host 0.0.0.0` records the host, so `--json` takes the untouched path and resolves to the externally reachable address rather than to loopback.

```json
state    {"host":"0.0.0.0","port":7912,"origin":"http://127.0.0.1:7912"}
--json   "pairingUrl": "http://192.168.0.60:7912/pair#token=…"
```

One observation outside this change: for that wildcard server `pairingUrl.origin` and the recorded `origin` still differ, because upstream records loopback as the origin whatever the bind address is. The desktop's local-server auto-pairing would reject it for the same reason it rejected the no-host case. That is upstream behaviour on a path this change deliberately leaves alone, and it is not what #902 reported; filing it separately if it proves to matter.

Refs #902

https://claude.ai/code/session_012duArYgsDfEoAPcp9ZUXdr

## Fork trailers

Squashes:

- bc24ddf68a5 fix(server): pair --json through the recorded origin when no host is set
- 8618b0b9460 fix(client-runtime): an attached primary no longer reconnects on every platform poll (#1477) (#1477)
- 2f240119896 fix(fork): keep the client usable while the attached server restarts (#1712) (#1712)
- 6b19887bb4b fix(desktop): keep the attached server through a stall (#1721) (#1721)
- bcb0971cfd2 fix(web): update everything updates the attached service in client-only mode (#1751) (#1751)
- 6f68726fc5f fix(desktop): wait for the service update handoff before startup discovery (#1838) (#1838)
- a67ac66cef1 docs(backend-attach): describe the attached primary and its seams (#1861) (#1861)
- ba31fdd764e fix(web): client-only Connections hides managed-server controls (#1867) (#1867)
- 8fcfc1dd625 refactor(desktop)!: retire backend-attach for upstream Local-off (#1954) (#1954)

Fork-Domain: backend-attach
Fork-Tier: core
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 9, 2026
Desktop auto-pairing always failed against a local server started with no explicit host. `pair --json` emitted `localhost` while runtime state recorded `127.0.0.1`, and the desktop compares the two origins strictly.

## Change

`apps/server/src/cli/pair.ts` resolves the machine-readable pairing URL through the recorded origin when no host is set. Human output, `resolveDirectPairingBaseUrl`, the upstream localhost fallback, and the desktop's strict comparison are untouched, so a `--host 0.0.0.0` server still records an externally reachable connection string and that stays the pairing URL.

## Checks

```text
vp test run apps/server/src/cli/pair.fork.test.ts   3 passed
vp run fork:delta --check                           ok: 220 fork commits tagged
```

## Grounding

A real `t3 serve` on the branch, started with no `--host` and its own base dir, is exactly the reported precondition: the runtime record carries no `host` and an origin of `127.0.0.1`.

```json
{"version":1,"pid":3596040,"port":7911,"origin":"http://127.0.0.1:7911", ...}
```

`--json` and the human path were then read off that one server. The human path still resolves through `resolveDirectPairingBaseUrl`, so it is the pre-fix value for the same state.

```text
pair           Pairing URL: http://localhost:7911/pair#token=…     (upstream base, untouched)
pair --json    "pairingUrl": "http://127.0.0.1:7911/pair#token=…"
               "origin":     "http://127.0.0.1:7911"
```

Applying the desktop's own check — `pairingUrl.origin === httpBaseUrl.origin`, where `httpBaseUrl` is the recorded `origin` (`DesktopRunningLocalServers.ts:83,174`):

| pairing base | pairingUrl.origin | matches recorded origin |
| --- | --- | --- |
| upstream base (pre-fix `--json`) | `http://localhost:7911` | no |
| recorded origin (this change) | `http://127.0.0.1:7911` | yes |

**A configured host still keeps the upstream base.** A second server on the same branch with `--host 0.0.0.0` records the host, so `--json` takes the untouched path and resolves to the externally reachable address rather than to loopback.

```json
state    {"host":"0.0.0.0","port":7912,"origin":"http://127.0.0.1:7912"}
--json   "pairingUrl": "http://192.168.0.60:7912/pair#token=…"
```

One observation outside this change: for that wildcard server `pairingUrl.origin` and the recorded `origin` still differ, because upstream records loopback as the origin whatever the bind address is. The desktop's local-server auto-pairing would reject it for the same reason it rejected the no-host case. That is upstream behaviour on a path this change deliberately leaves alone, and it is not what #902 reported; filing it separately if it proves to matter.

Refs #902

https://claude.ai/code/session_012duArYgsDfEoAPcp9ZUXdr

## Fork trailers

Squashes:

- bc24ddf68a5 fix(server): pair --json through the recorded origin when no host is set
- 8618b0b9460 fix(client-runtime): an attached primary no longer reconnects on every platform poll (#1477) (#1477)
- 2f240119896 fix(fork): keep the client usable while the attached server restarts (#1712) (#1712)
- 6b19887bb4b fix(desktop): keep the attached server through a stall (#1721) (#1721)
- bcb0971cfd2 fix(web): update everything updates the attached service in client-only mode (#1751) (#1751)
- 6f68726fc5f fix(desktop): wait for the service update handoff before startup discovery (#1838) (#1838)
- a67ac66cef1 docs(backend-attach): describe the attached primary and its seams (#1861) (#1861)
- ba31fdd764e fix(web): client-only Connections hides managed-server controls (#1867) (#1867)
- 8fcfc1dd625 refactor(desktop)!: retire backend-attach for upstream Local-off (#1954) (#1954)

Fork-Domain: backend-attach
Fork-Tier: core
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 9, 2026
Desktop auto-pairing always failed against a local server started with no explicit host. `pair --json` emitted `localhost` while runtime state recorded `127.0.0.1`, and the desktop compares the two origins strictly.

## Change

`apps/server/src/cli/pair.ts` resolves the machine-readable pairing URL through the recorded origin when no host is set. Human output, `resolveDirectPairingBaseUrl`, the upstream localhost fallback, and the desktop's strict comparison are untouched, so a `--host 0.0.0.0` server still records an externally reachable connection string and that stays the pairing URL.

## Checks

```text
vp test run apps/server/src/cli/pair.fork.test.ts   3 passed
vp run fork:delta --check                           ok: 220 fork commits tagged
```

## Grounding

A real `t3 serve` on the branch, started with no `--host` and its own base dir, is exactly the reported precondition: the runtime record carries no `host` and an origin of `127.0.0.1`.

```json
{"version":1,"pid":3596040,"port":7911,"origin":"http://127.0.0.1:7911", ...}
```

`--json` and the human path were then read off that one server. The human path still resolves through `resolveDirectPairingBaseUrl`, so it is the pre-fix value for the same state.

```text
pair           Pairing URL: http://localhost:7911/pair#token=…     (upstream base, untouched)
pair --json    "pairingUrl": "http://127.0.0.1:7911/pair#token=…"
               "origin":     "http://127.0.0.1:7911"
```

Applying the desktop's own check — `pairingUrl.origin === httpBaseUrl.origin`, where `httpBaseUrl` is the recorded `origin` (`DesktopRunningLocalServers.ts:83,174`):

| pairing base | pairingUrl.origin | matches recorded origin |
| --- | --- | --- |
| upstream base (pre-fix `--json`) | `http://localhost:7911` | no |
| recorded origin (this change) | `http://127.0.0.1:7911` | yes |

**A configured host still keeps the upstream base.** A second server on the same branch with `--host 0.0.0.0` records the host, so `--json` takes the untouched path and resolves to the externally reachable address rather than to loopback.

```json
state    {"host":"0.0.0.0","port":7912,"origin":"http://127.0.0.1:7912"}
--json   "pairingUrl": "http://192.168.0.60:7912/pair#token=…"
```

One observation outside this change: for that wildcard server `pairingUrl.origin` and the recorded `origin` still differ, because upstream records loopback as the origin whatever the bind address is. The desktop's local-server auto-pairing would reject it for the same reason it rejected the no-host case. That is upstream behaviour on a path this change deliberately leaves alone, and it is not what #902 reported; filing it separately if it proves to matter.

Refs #902

https://claude.ai/code/session_012duArYgsDfEoAPcp9ZUXdr

## Fork trailers

Squashes:

- bc24ddf68a5 fix(server): pair --json through the recorded origin when no host is set
- 8618b0b9460 fix(client-runtime): an attached primary no longer reconnects on every platform poll (#1477) (#1477)
- 2f240119896 fix(fork): keep the client usable while the attached server restarts (#1712) (#1712)
- 6b19887bb4b fix(desktop): keep the attached server through a stall (#1721) (#1721)
- bcb0971cfd2 fix(web): update everything updates the attached service in client-only mode (#1751) (#1751)
- 6f68726fc5f fix(desktop): wait for the service update handoff before startup discovery (#1838) (#1838)
- a67ac66cef1 docs(backend-attach): describe the attached primary and its seams (#1861) (#1861)
- ba31fdd764e fix(web): client-only Connections hides managed-server controls (#1867) (#1867)
- 8fcfc1dd625 refactor(desktop)!: retire backend-attach for upstream Local-off (#1954) (#1954)

Fork-Domain: backend-attach
Fork-Tier: core
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 10, 2026
Desktop auto-pairing always failed against a local server started with no explicit host. `pair --json` emitted `localhost` while runtime state recorded `127.0.0.1`, and the desktop compares the two origins strictly.

## Change

`apps/server/src/cli/pair.ts` resolves the machine-readable pairing URL through the recorded origin when no host is set. Human output, `resolveDirectPairingBaseUrl`, the upstream localhost fallback, and the desktop's strict comparison are untouched, so a `--host 0.0.0.0` server still records an externally reachable connection string and that stays the pairing URL.

## Checks

```text
vp test run apps/server/src/cli/pair.fork.test.ts   3 passed
vp run fork:delta --check                           ok: 220 fork commits tagged
```

## Grounding

A real `t3 serve` on the branch, started with no `--host` and its own base dir, is exactly the reported precondition: the runtime record carries no `host` and an origin of `127.0.0.1`.

```json
{"version":1,"pid":3596040,"port":7911,"origin":"http://127.0.0.1:7911", ...}
```

`--json` and the human path were then read off that one server. The human path still resolves through `resolveDirectPairingBaseUrl`, so it is the pre-fix value for the same state.

```text
pair           Pairing URL: http://localhost:7911/pair#token=…     (upstream base, untouched)
pair --json    "pairingUrl": "http://127.0.0.1:7911/pair#token=…"
               "origin":     "http://127.0.0.1:7911"
```

Applying the desktop's own check — `pairingUrl.origin === httpBaseUrl.origin`, where `httpBaseUrl` is the recorded `origin` (`DesktopRunningLocalServers.ts:83,174`):

| pairing base | pairingUrl.origin | matches recorded origin |
| --- | --- | --- |
| upstream base (pre-fix `--json`) | `http://localhost:7911` | no |
| recorded origin (this change) | `http://127.0.0.1:7911` | yes |

**A configured host still keeps the upstream base.** A second server on the same branch with `--host 0.0.0.0` records the host, so `--json` takes the untouched path and resolves to the externally reachable address rather than to loopback.

```json
state    {"host":"0.0.0.0","port":7912,"origin":"http://127.0.0.1:7912"}
--json   "pairingUrl": "http://192.168.0.60:7912/pair#token=…"
```

One observation outside this change: for that wildcard server `pairingUrl.origin` and the recorded `origin` still differ, because upstream records loopback as the origin whatever the bind address is. The desktop's local-server auto-pairing would reject it for the same reason it rejected the no-host case. That is upstream behaviour on a path this change deliberately leaves alone, and it is not what #902 reported; filing it separately if it proves to matter.

Refs #902

https://claude.ai/code/session_012duArYgsDfEoAPcp9ZUXdr

## Fork trailers

Squashes:

- bc24ddf68a5 fix(server): pair --json through the recorded origin when no host is set
- 8618b0b9460 fix(client-runtime): an attached primary no longer reconnects on every platform poll (#1477) (#1477)
- 2f240119896 fix(fork): keep the client usable while the attached server restarts (#1712) (#1712)
- 6b19887bb4b fix(desktop): keep the attached server through a stall (#1721) (#1721)
- bcb0971cfd2 fix(web): update everything updates the attached service in client-only mode (#1751) (#1751)
- 6f68726fc5f fix(desktop): wait for the service update handoff before startup discovery (#1838) (#1838)
- a67ac66cef1 docs(backend-attach): describe the attached primary and its seams (#1861) (#1861)
- ba31fdd764e fix(web): client-only Connections hides managed-server controls (#1867) (#1867)
- 8fcfc1dd625 refactor(desktop)!: retire backend-attach for upstream Local-off (#1954) (#1954)

Fork-Domain: backend-attach
Fork-Tier: core
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 10, 2026
Desktop auto-pairing always failed against a local server started with no explicit host. `pair --json` emitted `localhost` while runtime state recorded `127.0.0.1`, and the desktop compares the two origins strictly.

## Change

`apps/server/src/cli/pair.ts` resolves the machine-readable pairing URL through the recorded origin when no host is set. Human output, `resolveDirectPairingBaseUrl`, the upstream localhost fallback, and the desktop's strict comparison are untouched, so a `--host 0.0.0.0` server still records an externally reachable connection string and that stays the pairing URL.

## Checks

```text
vp test run apps/server/src/cli/pair.fork.test.ts   3 passed
vp run fork:delta --check                           ok: 220 fork commits tagged
```

## Grounding

A real `t3 serve` on the branch, started with no `--host` and its own base dir, is exactly the reported precondition: the runtime record carries no `host` and an origin of `127.0.0.1`.

```json
{"version":1,"pid":3596040,"port":7911,"origin":"http://127.0.0.1:7911", ...}
```

`--json` and the human path were then read off that one server. The human path still resolves through `resolveDirectPairingBaseUrl`, so it is the pre-fix value for the same state.

```text
pair           Pairing URL: http://localhost:7911/pair#token=…     (upstream base, untouched)
pair --json    "pairingUrl": "http://127.0.0.1:7911/pair#token=…"
               "origin":     "http://127.0.0.1:7911"
```

Applying the desktop's own check — `pairingUrl.origin === httpBaseUrl.origin`, where `httpBaseUrl` is the recorded `origin` (`DesktopRunningLocalServers.ts:83,174`):

| pairing base | pairingUrl.origin | matches recorded origin |
| --- | --- | --- |
| upstream base (pre-fix `--json`) | `http://localhost:7911` | no |
| recorded origin (this change) | `http://127.0.0.1:7911` | yes |

**A configured host still keeps the upstream base.** A second server on the same branch with `--host 0.0.0.0` records the host, so `--json` takes the untouched path and resolves to the externally reachable address rather than to loopback.

```json
state    {"host":"0.0.0.0","port":7912,"origin":"http://127.0.0.1:7912"}
--json   "pairingUrl": "http://192.168.0.60:7912/pair#token=…"
```

One observation outside this change: for that wildcard server `pairingUrl.origin` and the recorded `origin` still differ, because upstream records loopback as the origin whatever the bind address is. The desktop's local-server auto-pairing would reject it for the same reason it rejected the no-host case. That is upstream behaviour on a path this change deliberately leaves alone, and it is not what #902 reported; filing it separately if it proves to matter.

Refs #902

https://claude.ai/code/session_012duArYgsDfEoAPcp9ZUXdr

## Fork trailers

Squashes:

- bc24ddf68a5 fix(server): pair --json through the recorded origin when no host is set
- 8618b0b9460 fix(client-runtime): an attached primary no longer reconnects on every platform poll (#1477) (#1477)
- 2f240119896 fix(fork): keep the client usable while the attached server restarts (#1712) (#1712)
- 6b19887bb4b fix(desktop): keep the attached server through a stall (#1721) (#1721)
- bcb0971cfd2 fix(web): update everything updates the attached service in client-only mode (#1751) (#1751)
- 6f68726fc5f fix(desktop): wait for the service update handoff before startup discovery (#1838) (#1838)
- a67ac66cef1 docs(backend-attach): describe the attached primary and its seams (#1861) (#1861)
- ba31fdd764e fix(web): client-only Connections hides managed-server controls (#1867) (#1867)
- 8fcfc1dd625 refactor(desktop)!: retire backend-attach for upstream Local-off (#1954) (#1954)

Fork-Domain: backend-attach
Fork-Tier: core
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 10, 2026
Desktop auto-pairing always failed against a local server started with no explicit host. `pair --json` emitted `localhost` while runtime state recorded `127.0.0.1`, and the desktop compares the two origins strictly.

## Change

`apps/server/src/cli/pair.ts` resolves the machine-readable pairing URL through the recorded origin when no host is set. Human output, `resolveDirectPairingBaseUrl`, the upstream localhost fallback, and the desktop's strict comparison are untouched, so a `--host 0.0.0.0` server still records an externally reachable connection string and that stays the pairing URL.

## Checks

```text
vp test run apps/server/src/cli/pair.fork.test.ts   3 passed
vp run fork:delta --check                           ok: 220 fork commits tagged
```

## Grounding

A real `t3 serve` on the branch, started with no `--host` and its own base dir, is exactly the reported precondition: the runtime record carries no `host` and an origin of `127.0.0.1`.

```json
{"version":1,"pid":3596040,"port":7911,"origin":"http://127.0.0.1:7911", ...}
```

`--json` and the human path were then read off that one server. The human path still resolves through `resolveDirectPairingBaseUrl`, so it is the pre-fix value for the same state.

```text
pair           Pairing URL: http://localhost:7911/pair#token=…     (upstream base, untouched)
pair --json    "pairingUrl": "http://127.0.0.1:7911/pair#token=…"
               "origin":     "http://127.0.0.1:7911"
```

Applying the desktop's own check — `pairingUrl.origin === httpBaseUrl.origin`, where `httpBaseUrl` is the recorded `origin` (`DesktopRunningLocalServers.ts:83,174`):

| pairing base | pairingUrl.origin | matches recorded origin |
| --- | --- | --- |
| upstream base (pre-fix `--json`) | `http://localhost:7911` | no |
| recorded origin (this change) | `http://127.0.0.1:7911` | yes |

**A configured host still keeps the upstream base.** A second server on the same branch with `--host 0.0.0.0` records the host, so `--json` takes the untouched path and resolves to the externally reachable address rather than to loopback.

```json
state    {"host":"0.0.0.0","port":7912,"origin":"http://127.0.0.1:7912"}
--json   "pairingUrl": "http://192.168.0.60:7912/pair#token=…"
```

One observation outside this change: for that wildcard server `pairingUrl.origin` and the recorded `origin` still differ, because upstream records loopback as the origin whatever the bind address is. The desktop's local-server auto-pairing would reject it for the same reason it rejected the no-host case. That is upstream behaviour on a path this change deliberately leaves alone, and it is not what #902 reported; filing it separately if it proves to matter.

Refs #902

https://claude.ai/code/session_012duArYgsDfEoAPcp9ZUXdr

## Fork trailers

Squashes:

- bc24ddf68a5 fix(server): pair --json through the recorded origin when no host is set
- 8618b0b9460 fix(client-runtime): an attached primary no longer reconnects on every platform poll (#1477) (#1477)
- 2f240119896 fix(fork): keep the client usable while the attached server restarts (#1712) (#1712)
- 6b19887bb4b fix(desktop): keep the attached server through a stall (#1721) (#1721)
- bcb0971cfd2 fix(web): update everything updates the attached service in client-only mode (#1751) (#1751)
- 6f68726fc5f fix(desktop): wait for the service update handoff before startup discovery (#1838) (#1838)
- a67ac66cef1 docs(backend-attach): describe the attached primary and its seams (#1861) (#1861)
- ba31fdd764e fix(web): client-only Connections hides managed-server controls (#1867) (#1867)
- 8fcfc1dd625 refactor(desktop)!: retire backend-attach for upstream Local-off (#1954) (#1954)

Fork-Domain: backend-attach
Fork-Tier: core
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 10, 2026
Desktop auto-pairing always failed against a local server started with no explicit host. `pair --json` emitted `localhost` while runtime state recorded `127.0.0.1`, and the desktop compares the two origins strictly.

## Change

`apps/server/src/cli/pair.ts` resolves the machine-readable pairing URL through the recorded origin when no host is set. Human output, `resolveDirectPairingBaseUrl`, the upstream localhost fallback, and the desktop's strict comparison are untouched, so a `--host 0.0.0.0` server still records an externally reachable connection string and that stays the pairing URL.

## Checks

```text
vp test run apps/server/src/cli/pair.fork.test.ts   3 passed
vp run fork:delta --check                           ok: 220 fork commits tagged
```

## Grounding

A real `t3 serve` on the branch, started with no `--host` and its own base dir, is exactly the reported precondition: the runtime record carries no `host` and an origin of `127.0.0.1`.

```json
{"version":1,"pid":3596040,"port":7911,"origin":"http://127.0.0.1:7911", ...}
```

`--json` and the human path were then read off that one server. The human path still resolves through `resolveDirectPairingBaseUrl`, so it is the pre-fix value for the same state.

```text
pair           Pairing URL: http://localhost:7911/pair#token=…     (upstream base, untouched)
pair --json    "pairingUrl": "http://127.0.0.1:7911/pair#token=…"
               "origin":     "http://127.0.0.1:7911"
```

Applying the desktop's own check — `pairingUrl.origin === httpBaseUrl.origin`, where `httpBaseUrl` is the recorded `origin` (`DesktopRunningLocalServers.ts:83,174`):

| pairing base | pairingUrl.origin | matches recorded origin |
| --- | --- | --- |
| upstream base (pre-fix `--json`) | `http://localhost:7911` | no |
| recorded origin (this change) | `http://127.0.0.1:7911` | yes |

**A configured host still keeps the upstream base.** A second server on the same branch with `--host 0.0.0.0` records the host, so `--json` takes the untouched path and resolves to the externally reachable address rather than to loopback.

```json
state    {"host":"0.0.0.0","port":7912,"origin":"http://127.0.0.1:7912"}
--json   "pairingUrl": "http://192.168.0.60:7912/pair#token=…"
```

One observation outside this change: for that wildcard server `pairingUrl.origin` and the recorded `origin` still differ, because upstream records loopback as the origin whatever the bind address is. The desktop's local-server auto-pairing would reject it for the same reason it rejected the no-host case. That is upstream behaviour on a path this change deliberately leaves alone, and it is not what #902 reported; filing it separately if it proves to matter.

Refs #902

https://claude.ai/code/session_012duArYgsDfEoAPcp9ZUXdr

## Fork trailers

Squashes:

- bc24ddf68a5 fix(server): pair --json through the recorded origin when no host is set
- 8618b0b9460 fix(client-runtime): an attached primary no longer reconnects on every platform poll (#1477) (#1477)
- 2f240119896 fix(fork): keep the client usable while the attached server restarts (#1712) (#1712)
- 6b19887bb4b fix(desktop): keep the attached server through a stall (#1721) (#1721)
- bcb0971cfd2 fix(web): update everything updates the attached service in client-only mode (#1751) (#1751)
- 6f68726fc5f fix(desktop): wait for the service update handoff before startup discovery (#1838) (#1838)
- a67ac66cef1 docs(backend-attach): describe the attached primary and its seams (#1861) (#1861)
- ba31fdd764e fix(web): client-only Connections hides managed-server controls (#1867) (#1867)
- 8fcfc1dd625 refactor(desktop)!: retire backend-attach for upstream Local-off (#1954) (#1954)

Fork-Domain: backend-attach
Fork-Tier: core
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 10, 2026
Desktop auto-pairing always failed against a local server started with no explicit host. `pair --json` emitted `localhost` while runtime state recorded `127.0.0.1`, and the desktop compares the two origins strictly.

## Change

`apps/server/src/cli/pair.ts` resolves the machine-readable pairing URL through the recorded origin when no host is set. Human output, `resolveDirectPairingBaseUrl`, the upstream localhost fallback, and the desktop's strict comparison are untouched, so a `--host 0.0.0.0` server still records an externally reachable connection string and that stays the pairing URL.

## Checks

```text
vp test run apps/server/src/cli/pair.fork.test.ts   3 passed
vp run fork:delta --check                           ok: 220 fork commits tagged
```

## Grounding

A real `t3 serve` on the branch, started with no `--host` and its own base dir, is exactly the reported precondition: the runtime record carries no `host` and an origin of `127.0.0.1`.

```json
{"version":1,"pid":3596040,"port":7911,"origin":"http://127.0.0.1:7911", ...}
```

`--json` and the human path were then read off that one server. The human path still resolves through `resolveDirectPairingBaseUrl`, so it is the pre-fix value for the same state.

```text
pair           Pairing URL: http://localhost:7911/pair#token=…     (upstream base, untouched)
pair --json    "pairingUrl": "http://127.0.0.1:7911/pair#token=…"
               "origin":     "http://127.0.0.1:7911"
```

Applying the desktop's own check — `pairingUrl.origin === httpBaseUrl.origin`, where `httpBaseUrl` is the recorded `origin` (`DesktopRunningLocalServers.ts:83,174`):

| pairing base | pairingUrl.origin | matches recorded origin |
| --- | --- | --- |
| upstream base (pre-fix `--json`) | `http://localhost:7911` | no |
| recorded origin (this change) | `http://127.0.0.1:7911` | yes |

**A configured host still keeps the upstream base.** A second server on the same branch with `--host 0.0.0.0` records the host, so `--json` takes the untouched path and resolves to the externally reachable address rather than to loopback.

```json
state    {"host":"0.0.0.0","port":7912,"origin":"http://127.0.0.1:7912"}
--json   "pairingUrl": "http://192.168.0.60:7912/pair#token=…"
```

One observation outside this change: for that wildcard server `pairingUrl.origin` and the recorded `origin` still differ, because upstream records loopback as the origin whatever the bind address is. The desktop's local-server auto-pairing would reject it for the same reason it rejected the no-host case. That is upstream behaviour on a path this change deliberately leaves alone, and it is not what #902 reported; filing it separately if it proves to matter.

Refs #902

https://claude.ai/code/session_012duArYgsDfEoAPcp9ZUXdr

## Fork trailers

Squashes:

- bc24ddf68a5 fix(server): pair --json through the recorded origin when no host is set
- 8618b0b9460 fix(client-runtime): an attached primary no longer reconnects on every platform poll (#1477) (#1477)
- 2f240119896 fix(fork): keep the client usable while the attached server restarts (#1712) (#1712)
- 6b19887bb4b fix(desktop): keep the attached server through a stall (#1721) (#1721)
- bcb0971cfd2 fix(web): update everything updates the attached service in client-only mode (#1751) (#1751)
- 6f68726fc5f fix(desktop): wait for the service update handoff before startup discovery (#1838) (#1838)
- a67ac66cef1 docs(backend-attach): describe the attached primary and its seams (#1861) (#1861)
- ba31fdd764e fix(web): client-only Connections hides managed-server controls (#1867) (#1867)
- 8fcfc1dd625 refactor(desktop)!: retire backend-attach for upstream Local-off (#1954) (#1954)

Fork-Domain: backend-attach
Fork-Tier: core
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 11, 2026
Desktop auto-pairing always failed against a local server started with no explicit host. `pair --json` emitted `localhost` while runtime state recorded `127.0.0.1`, and the desktop compares the two origins strictly.

## Change

`apps/server/src/cli/pair.ts` resolves the machine-readable pairing URL through the recorded origin when no host is set. Human output, `resolveDirectPairingBaseUrl`, the upstream localhost fallback, and the desktop's strict comparison are untouched, so a `--host 0.0.0.0` server still records an externally reachable connection string and that stays the pairing URL.

## Checks

```text
vp test run apps/server/src/cli/pair.fork.test.ts   3 passed
vp run fork:delta --check                           ok: 220 fork commits tagged
```

## Grounding

A real `t3 serve` on the branch, started with no `--host` and its own base dir, is exactly the reported precondition: the runtime record carries no `host` and an origin of `127.0.0.1`.

```json
{"version":1,"pid":3596040,"port":7911,"origin":"http://127.0.0.1:7911", ...}
```

`--json` and the human path were then read off that one server. The human path still resolves through `resolveDirectPairingBaseUrl`, so it is the pre-fix value for the same state.

```text
pair           Pairing URL: http://localhost:7911/pair#token=…     (upstream base, untouched)
pair --json    "pairingUrl": "http://127.0.0.1:7911/pair#token=…"
               "origin":     "http://127.0.0.1:7911"
```

Applying the desktop's own check — `pairingUrl.origin === httpBaseUrl.origin`, where `httpBaseUrl` is the recorded `origin` (`DesktopRunningLocalServers.ts:83,174`):

| pairing base | pairingUrl.origin | matches recorded origin |
| --- | --- | --- |
| upstream base (pre-fix `--json`) | `http://localhost:7911` | no |
| recorded origin (this change) | `http://127.0.0.1:7911` | yes |

**A configured host still keeps the upstream base.** A second server on the same branch with `--host 0.0.0.0` records the host, so `--json` takes the untouched path and resolves to the externally reachable address rather than to loopback.

```json
state    {"host":"0.0.0.0","port":7912,"origin":"http://127.0.0.1:7912"}
--json   "pairingUrl": "http://192.168.0.60:7912/pair#token=…"
```

One observation outside this change: for that wildcard server `pairingUrl.origin` and the recorded `origin` still differ, because upstream records loopback as the origin whatever the bind address is. The desktop's local-server auto-pairing would reject it for the same reason it rejected the no-host case. That is upstream behaviour on a path this change deliberately leaves alone, and it is not what #902 reported; filing it separately if it proves to matter.

Refs #902

https://claude.ai/code/session_012duArYgsDfEoAPcp9ZUXdr

## Fork trailers

Squashes:

- bc24ddf68a5 fix(server): pair --json through the recorded origin when no host is set
- 8618b0b9460 fix(client-runtime): an attached primary no longer reconnects on every platform poll (#1477) (#1477)
- 2f240119896 fix(fork): keep the client usable while the attached server restarts (#1712) (#1712)
- 6b19887bb4b fix(desktop): keep the attached server through a stall (#1721) (#1721)
- bcb0971cfd2 fix(web): update everything updates the attached service in client-only mode (#1751) (#1751)
- 6f68726fc5f fix(desktop): wait for the service update handoff before startup discovery (#1838) (#1838)
- a67ac66cef1 docs(backend-attach): describe the attached primary and its seams (#1861) (#1861)
- ba31fdd764e fix(web): client-only Connections hides managed-server controls (#1867) (#1867)
- 8fcfc1dd625 refactor(desktop)!: retire backend-attach for upstream Local-off (#1954) (#1954)

Fork-Domain: backend-attach
Fork-Tier: core
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 11, 2026
Desktop auto-pairing always failed against a local server started with no explicit host. `pair --json` emitted `localhost` while runtime state recorded `127.0.0.1`, and the desktop compares the two origins strictly.

## Change

`apps/server/src/cli/pair.ts` resolves the machine-readable pairing URL through the recorded origin when no host is set. Human output, `resolveDirectPairingBaseUrl`, the upstream localhost fallback, and the desktop's strict comparison are untouched, so a `--host 0.0.0.0` server still records an externally reachable connection string and that stays the pairing URL.

## Checks

```text
vp test run apps/server/src/cli/pair.fork.test.ts   3 passed
vp run fork:delta --check                           ok: 220 fork commits tagged
```

## Grounding

A real `t3 serve` on the branch, started with no `--host` and its own base dir, is exactly the reported precondition: the runtime record carries no `host` and an origin of `127.0.0.1`.

```json
{"version":1,"pid":3596040,"port":7911,"origin":"http://127.0.0.1:7911", ...}
```

`--json` and the human path were then read off that one server. The human path still resolves through `resolveDirectPairingBaseUrl`, so it is the pre-fix value for the same state.

```text
pair           Pairing URL: http://localhost:7911/pair#token=…     (upstream base, untouched)
pair --json    "pairingUrl": "http://127.0.0.1:7911/pair#token=…"
               "origin":     "http://127.0.0.1:7911"
```

Applying the desktop's own check — `pairingUrl.origin === httpBaseUrl.origin`, where `httpBaseUrl` is the recorded `origin` (`DesktopRunningLocalServers.ts:83,174`):

| pairing base | pairingUrl.origin | matches recorded origin |
| --- | --- | --- |
| upstream base (pre-fix `--json`) | `http://localhost:7911` | no |
| recorded origin (this change) | `http://127.0.0.1:7911` | yes |

**A configured host still keeps the upstream base.** A second server on the same branch with `--host 0.0.0.0` records the host, so `--json` takes the untouched path and resolves to the externally reachable address rather than to loopback.

```json
state    {"host":"0.0.0.0","port":7912,"origin":"http://127.0.0.1:7912"}
--json   "pairingUrl": "http://192.168.0.60:7912/pair#token=…"
```

One observation outside this change: for that wildcard server `pairingUrl.origin` and the recorded `origin` still differ, because upstream records loopback as the origin whatever the bind address is. The desktop's local-server auto-pairing would reject it for the same reason it rejected the no-host case. That is upstream behaviour on a path this change deliberately leaves alone, and it is not what #902 reported; filing it separately if it proves to matter.

Refs #902

https://claude.ai/code/session_012duArYgsDfEoAPcp9ZUXdr

## Fork trailers

Squashes:

- bc24ddf68a5 fix(server): pair --json through the recorded origin when no host is set
- 8618b0b9460 fix(client-runtime): an attached primary no longer reconnects on every platform poll (#1477) (#1477)
- 2f240119896 fix(fork): keep the client usable while the attached server restarts (#1712) (#1712)
- 6b19887bb4b fix(desktop): keep the attached server through a stall (#1721) (#1721)
- bcb0971cfd2 fix(web): update everything updates the attached service in client-only mode (#1751) (#1751)
- 6f68726fc5f fix(desktop): wait for the service update handoff before startup discovery (#1838) (#1838)
- a67ac66cef1 docs(backend-attach): describe the attached primary and its seams (#1861) (#1861)
- ba31fdd764e fix(web): client-only Connections hides managed-server controls (#1867) (#1867)
- 8fcfc1dd625 refactor(desktop)!: retire backend-attach for upstream Local-off (#1954) (#1954)

Fork-Domain: backend-attach
Fork-Tier: core
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 11, 2026
Desktop auto-pairing always failed against a local server started with no explicit host. `pair --json` emitted `localhost` while runtime state recorded `127.0.0.1`, and the desktop compares the two origins strictly.

## Change

`apps/server/src/cli/pair.ts` resolves the machine-readable pairing URL through the recorded origin when no host is set. Human output, `resolveDirectPairingBaseUrl`, the upstream localhost fallback, and the desktop's strict comparison are untouched, so a `--host 0.0.0.0` server still records an externally reachable connection string and that stays the pairing URL.

## Checks

```text
vp test run apps/server/src/cli/pair.fork.test.ts   3 passed
vp run fork:delta --check                           ok: 220 fork commits tagged
```

## Grounding

A real `t3 serve` on the branch, started with no `--host` and its own base dir, is exactly the reported precondition: the runtime record carries no `host` and an origin of `127.0.0.1`.

```json
{"version":1,"pid":3596040,"port":7911,"origin":"http://127.0.0.1:7911", ...}
```

`--json` and the human path were then read off that one server. The human path still resolves through `resolveDirectPairingBaseUrl`, so it is the pre-fix value for the same state.

```text
pair           Pairing URL: http://localhost:7911/pair#token=…     (upstream base, untouched)
pair --json    "pairingUrl": "http://127.0.0.1:7911/pair#token=…"
               "origin":     "http://127.0.0.1:7911"
```

Applying the desktop's own check — `pairingUrl.origin === httpBaseUrl.origin`, where `httpBaseUrl` is the recorded `origin` (`DesktopRunningLocalServers.ts:83,174`):

| pairing base | pairingUrl.origin | matches recorded origin |
| --- | --- | --- |
| upstream base (pre-fix `--json`) | `http://localhost:7911` | no |
| recorded origin (this change) | `http://127.0.0.1:7911` | yes |

**A configured host still keeps the upstream base.** A second server on the same branch with `--host 0.0.0.0` records the host, so `--json` takes the untouched path and resolves to the externally reachable address rather than to loopback.

```json
state    {"host":"0.0.0.0","port":7912,"origin":"http://127.0.0.1:7912"}
--json   "pairingUrl": "http://192.168.0.60:7912/pair#token=…"
```

One observation outside this change: for that wildcard server `pairingUrl.origin` and the recorded `origin` still differ, because upstream records loopback as the origin whatever the bind address is. The desktop's local-server auto-pairing would reject it for the same reason it rejected the no-host case. That is upstream behaviour on a path this change deliberately leaves alone, and it is not what #902 reported; filing it separately if it proves to matter.

Refs #902

https://claude.ai/code/session_012duArYgsDfEoAPcp9ZUXdr

## Fork trailers

Squashes:

- bc24ddf68a5 fix(server): pair --json through the recorded origin when no host is set
- 8618b0b9460 fix(client-runtime): an attached primary no longer reconnects on every platform poll (#1477) (#1477)
- 2f240119896 fix(fork): keep the client usable while the attached server restarts (#1712) (#1712)
- 6b19887bb4b fix(desktop): keep the attached server through a stall (#1721) (#1721)
- bcb0971cfd2 fix(web): update everything updates the attached service in client-only mode (#1751) (#1751)
- 6f68726fc5f fix(desktop): wait for the service update handoff before startup discovery (#1838) (#1838)
- a67ac66cef1 docs(backend-attach): describe the attached primary and its seams (#1861) (#1861)
- ba31fdd764e fix(web): client-only Connections hides managed-server controls (#1867) (#1867)
- 8fcfc1dd625 refactor(desktop)!: retire backend-attach for upstream Local-off (#1954) (#1954)

Fork-Domain: backend-attach
Fork-Tier: core
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 11, 2026
Desktop auto-pairing always failed against a local server started with no explicit host. `pair --json` emitted `localhost` while runtime state recorded `127.0.0.1`, and the desktop compares the two origins strictly.

## Change

`apps/server/src/cli/pair.ts` resolves the machine-readable pairing URL through the recorded origin when no host is set. Human output, `resolveDirectPairingBaseUrl`, the upstream localhost fallback, and the desktop's strict comparison are untouched, so a `--host 0.0.0.0` server still records an externally reachable connection string and that stays the pairing URL.

## Checks

```text
vp test run apps/server/src/cli/pair.fork.test.ts   3 passed
vp run fork:delta --check                           ok: 220 fork commits tagged
```

## Grounding

A real `t3 serve` on the branch, started with no `--host` and its own base dir, is exactly the reported precondition: the runtime record carries no `host` and an origin of `127.0.0.1`.

```json
{"version":1,"pid":3596040,"port":7911,"origin":"http://127.0.0.1:7911", ...}
```

`--json` and the human path were then read off that one server. The human path still resolves through `resolveDirectPairingBaseUrl`, so it is the pre-fix value for the same state.

```text
pair           Pairing URL: http://localhost:7911/pair#token=…     (upstream base, untouched)
pair --json    "pairingUrl": "http://127.0.0.1:7911/pair#token=…"
               "origin":     "http://127.0.0.1:7911"
```

Applying the desktop's own check — `pairingUrl.origin === httpBaseUrl.origin`, where `httpBaseUrl` is the recorded `origin` (`DesktopRunningLocalServers.ts:83,174`):

| pairing base | pairingUrl.origin | matches recorded origin |
| --- | --- | --- |
| upstream base (pre-fix `--json`) | `http://localhost:7911` | no |
| recorded origin (this change) | `http://127.0.0.1:7911` | yes |

**A configured host still keeps the upstream base.** A second server on the same branch with `--host 0.0.0.0` records the host, so `--json` takes the untouched path and resolves to the externally reachable address rather than to loopback.

```json
state    {"host":"0.0.0.0","port":7912,"origin":"http://127.0.0.1:7912"}
--json   "pairingUrl": "http://192.168.0.60:7912/pair#token=…"
```

One observation outside this change: for that wildcard server `pairingUrl.origin` and the recorded `origin` still differ, because upstream records loopback as the origin whatever the bind address is. The desktop's local-server auto-pairing would reject it for the same reason it rejected the no-host case. That is upstream behaviour on a path this change deliberately leaves alone, and it is not what #902 reported; filing it separately if it proves to matter.

Refs #902

https://claude.ai/code/session_012duArYgsDfEoAPcp9ZUXdr

## Fork trailers

Squashes:

- bc24ddf68a5 fix(server): pair --json through the recorded origin when no host is set
- 8618b0b9460 fix(client-runtime): an attached primary no longer reconnects on every platform poll (#1477) (#1477)
- 2f240119896 fix(fork): keep the client usable while the attached server restarts (#1712) (#1712)
- 6b19887bb4b fix(desktop): keep the attached server through a stall (#1721) (#1721)
- bcb0971cfd2 fix(web): update everything updates the attached service in client-only mode (#1751) (#1751)
- 6f68726fc5f fix(desktop): wait for the service update handoff before startup discovery (#1838) (#1838)
- a67ac66cef1 docs(backend-attach): describe the attached primary and its seams (#1861) (#1861)
- ba31fdd764e fix(web): client-only Connections hides managed-server controls (#1867) (#1867)
- 8fcfc1dd625 refactor(desktop)!: retire backend-attach for upstream Local-off (#1954) (#1954)

Fork-Domain: backend-attach
Fork-Tier: core
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 11, 2026
Desktop auto-pairing always failed against a local server started with no explicit host. `pair --json` emitted `localhost` while runtime state recorded `127.0.0.1`, and the desktop compares the two origins strictly.

## Change

`apps/server/src/cli/pair.ts` resolves the machine-readable pairing URL through the recorded origin when no host is set. Human output, `resolveDirectPairingBaseUrl`, the upstream localhost fallback, and the desktop's strict comparison are untouched, so a `--host 0.0.0.0` server still records an externally reachable connection string and that stays the pairing URL.

## Checks

```text
vp test run apps/server/src/cli/pair.fork.test.ts   3 passed
vp run fork:delta --check                           ok: 220 fork commits tagged
```

## Grounding

A real `t3 serve` on the branch, started with no `--host` and its own base dir, is exactly the reported precondition: the runtime record carries no `host` and an origin of `127.0.0.1`.

```json
{"version":1,"pid":3596040,"port":7911,"origin":"http://127.0.0.1:7911", ...}
```

`--json` and the human path were then read off that one server. The human path still resolves through `resolveDirectPairingBaseUrl`, so it is the pre-fix value for the same state.

```text
pair           Pairing URL: http://localhost:7911/pair#token=…     (upstream base, untouched)
pair --json    "pairingUrl": "http://127.0.0.1:7911/pair#token=…"
               "origin":     "http://127.0.0.1:7911"
```

Applying the desktop's own check — `pairingUrl.origin === httpBaseUrl.origin`, where `httpBaseUrl` is the recorded `origin` (`DesktopRunningLocalServers.ts:83,174`):

| pairing base | pairingUrl.origin | matches recorded origin |
| --- | --- | --- |
| upstream base (pre-fix `--json`) | `http://localhost:7911` | no |
| recorded origin (this change) | `http://127.0.0.1:7911` | yes |

**A configured host still keeps the upstream base.** A second server on the same branch with `--host 0.0.0.0` records the host, so `--json` takes the untouched path and resolves to the externally reachable address rather than to loopback.

```json
state    {"host":"0.0.0.0","port":7912,"origin":"http://127.0.0.1:7912"}
--json   "pairingUrl": "http://192.168.0.60:7912/pair#token=…"
```

One observation outside this change: for that wildcard server `pairingUrl.origin` and the recorded `origin` still differ, because upstream records loopback as the origin whatever the bind address is. The desktop's local-server auto-pairing would reject it for the same reason it rejected the no-host case. That is upstream behaviour on a path this change deliberately leaves alone, and it is not what #902 reported; filing it separately if it proves to matter.

Refs #902

https://claude.ai/code/session_012duArYgsDfEoAPcp9ZUXdr

## Fork trailers

Squashes:

- bc24ddf68a5 fix(server): pair --json through the recorded origin when no host is set
- 8618b0b9460 fix(client-runtime): an attached primary no longer reconnects on every platform poll (#1477) (#1477)
- 2f240119896 fix(fork): keep the client usable while the attached server restarts (#1712) (#1712)
- 6b19887bb4b fix(desktop): keep the attached server through a stall (#1721) (#1721)
- bcb0971cfd2 fix(web): update everything updates the attached service in client-only mode (#1751) (#1751)
- 6f68726fc5f fix(desktop): wait for the service update handoff before startup discovery (#1838) (#1838)
- a67ac66cef1 docs(backend-attach): describe the attached primary and its seams (#1861) (#1861)
- ba31fdd764e fix(web): client-only Connections hides managed-server controls (#1867) (#1867)
- 8fcfc1dd625 refactor(desktop)!: retire backend-attach for upstream Local-off (#1954) (#1954)

Fork-Domain: backend-attach
Fork-Tier: core
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 11, 2026
Desktop auto-pairing always failed against a local server started with no explicit host. `pair --json` emitted `localhost` while runtime state recorded `127.0.0.1`, and the desktop compares the two origins strictly.

## Change

`apps/server/src/cli/pair.ts` resolves the machine-readable pairing URL through the recorded origin when no host is set. Human output, `resolveDirectPairingBaseUrl`, the upstream localhost fallback, and the desktop's strict comparison are untouched, so a `--host 0.0.0.0` server still records an externally reachable connection string and that stays the pairing URL.

## Checks

```text
vp test run apps/server/src/cli/pair.fork.test.ts   3 passed
vp run fork:delta --check                           ok: 220 fork commits tagged
```

## Grounding

A real `t3 serve` on the branch, started with no `--host` and its own base dir, is exactly the reported precondition: the runtime record carries no `host` and an origin of `127.0.0.1`.

```json
{"version":1,"pid":3596040,"port":7911,"origin":"http://127.0.0.1:7911", ...}
```

`--json` and the human path were then read off that one server. The human path still resolves through `resolveDirectPairingBaseUrl`, so it is the pre-fix value for the same state.

```text
pair           Pairing URL: http://localhost:7911/pair#token=…     (upstream base, untouched)
pair --json    "pairingUrl": "http://127.0.0.1:7911/pair#token=…"
               "origin":     "http://127.0.0.1:7911"
```

Applying the desktop's own check — `pairingUrl.origin === httpBaseUrl.origin`, where `httpBaseUrl` is the recorded `origin` (`DesktopRunningLocalServers.ts:83,174`):

| pairing base | pairingUrl.origin | matches recorded origin |
| --- | --- | --- |
| upstream base (pre-fix `--json`) | `http://localhost:7911` | no |
| recorded origin (this change) | `http://127.0.0.1:7911` | yes |

**A configured host still keeps the upstream base.** A second server on the same branch with `--host 0.0.0.0` records the host, so `--json` takes the untouched path and resolves to the externally reachable address rather than to loopback.

```json
state    {"host":"0.0.0.0","port":7912,"origin":"http://127.0.0.1:7912"}
--json   "pairingUrl": "http://192.168.0.60:7912/pair#token=…"
```

One observation outside this change: for that wildcard server `pairingUrl.origin` and the recorded `origin` still differ, because upstream records loopback as the origin whatever the bind address is. The desktop's local-server auto-pairing would reject it for the same reason it rejected the no-host case. That is upstream behaviour on a path this change deliberately leaves alone, and it is not what #902 reported; filing it separately if it proves to matter.

Refs #902

https://claude.ai/code/session_012duArYgsDfEoAPcp9ZUXdr

## Fork trailers

Squashes:

- bc24ddf68a5 fix(server): pair --json through the recorded origin when no host is set
- 8618b0b9460 fix(client-runtime): an attached primary no longer reconnects on every platform poll (#1477) (#1477)
- 2f240119896 fix(fork): keep the client usable while the attached server restarts (#1712) (#1712)
- 6b19887bb4b fix(desktop): keep the attached server through a stall (#1721) (#1721)
- bcb0971cfd2 fix(web): update everything updates the attached service in client-only mode (#1751) (#1751)
- 6f68726fc5f fix(desktop): wait for the service update handoff before startup discovery (#1838) (#1838)
- a67ac66cef1 docs(backend-attach): describe the attached primary and its seams (#1861) (#1861)
- ba31fdd764e fix(web): client-only Connections hides managed-server controls (#1867) (#1867)
- 8fcfc1dd625 refactor(desktop)!: retire backend-attach for upstream Local-off (#1954) (#1954)

Fork-Domain: backend-attach
Fork-Tier: core
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 12, 2026
Desktop auto-pairing always failed against a local server started with no explicit host. `pair --json` emitted `localhost` while runtime state recorded `127.0.0.1`, and the desktop compares the two origins strictly.

## Change

`apps/server/src/cli/pair.ts` resolves the machine-readable pairing URL through the recorded origin when no host is set. Human output, `resolveDirectPairingBaseUrl`, the upstream localhost fallback, and the desktop's strict comparison are untouched, so a `--host 0.0.0.0` server still records an externally reachable connection string and that stays the pairing URL.

## Checks

```text
vp test run apps/server/src/cli/pair.fork.test.ts   3 passed
vp run fork:delta --check                           ok: 220 fork commits tagged
```

## Grounding

A real `t3 serve` on the branch, started with no `--host` and its own base dir, is exactly the reported precondition: the runtime record carries no `host` and an origin of `127.0.0.1`.

```json
{"version":1,"pid":3596040,"port":7911,"origin":"http://127.0.0.1:7911", ...}
```

`--json` and the human path were then read off that one server. The human path still resolves through `resolveDirectPairingBaseUrl`, so it is the pre-fix value for the same state.

```text
pair           Pairing URL: http://localhost:7911/pair#token=…     (upstream base, untouched)
pair --json    "pairingUrl": "http://127.0.0.1:7911/pair#token=…"
               "origin":     "http://127.0.0.1:7911"
```

Applying the desktop's own check — `pairingUrl.origin === httpBaseUrl.origin`, where `httpBaseUrl` is the recorded `origin` (`DesktopRunningLocalServers.ts:83,174`):

| pairing base | pairingUrl.origin | matches recorded origin |
| --- | --- | --- |
| upstream base (pre-fix `--json`) | `http://localhost:7911` | no |
| recorded origin (this change) | `http://127.0.0.1:7911` | yes |

**A configured host still keeps the upstream base.** A second server on the same branch with `--host 0.0.0.0` records the host, so `--json` takes the untouched path and resolves to the externally reachable address rather than to loopback.

```json
state    {"host":"0.0.0.0","port":7912,"origin":"http://127.0.0.1:7912"}
--json   "pairingUrl": "http://192.168.0.60:7912/pair#token=…"
```

One observation outside this change: for that wildcard server `pairingUrl.origin` and the recorded `origin` still differ, because upstream records loopback as the origin whatever the bind address is. The desktop's local-server auto-pairing would reject it for the same reason it rejected the no-host case. That is upstream behaviour on a path this change deliberately leaves alone, and it is not what #902 reported; filing it separately if it proves to matter.

Refs #902

https://claude.ai/code/session_012duArYgsDfEoAPcp9ZUXdr

## Fork trailers

Squashes:

- bc24ddf68a5 fix(server): pair --json through the recorded origin when no host is set
- 8618b0b9460 fix(client-runtime): an attached primary no longer reconnects on every platform poll (#1477) (#1477)
- 2f240119896 fix(fork): keep the client usable while the attached server restarts (#1712) (#1712)
- 6b19887bb4b fix(desktop): keep the attached server through a stall (#1721) (#1721)
- bcb0971cfd2 fix(web): update everything updates the attached service in client-only mode (#1751) (#1751)
- 6f68726fc5f fix(desktop): wait for the service update handoff before startup discovery (#1838) (#1838)
- a67ac66cef1 docs(backend-attach): describe the attached primary and its seams (#1861) (#1861)
- ba31fdd764e fix(web): client-only Connections hides managed-server controls (#1867) (#1867)
- 8fcfc1dd625 refactor(desktop)!: retire backend-attach for upstream Local-off (#1954) (#1954)

Fork-Domain: backend-attach
Fork-Tier: core
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 12, 2026
Desktop auto-pairing always failed against a local server started with no explicit host. `pair --json` emitted `localhost` while runtime state recorded `127.0.0.1`, and the desktop compares the two origins strictly.

## Change

`apps/server/src/cli/pair.ts` resolves the machine-readable pairing URL through the recorded origin when no host is set. Human output, `resolveDirectPairingBaseUrl`, the upstream localhost fallback, and the desktop's strict comparison are untouched, so a `--host 0.0.0.0` server still records an externally reachable connection string and that stays the pairing URL.

## Checks

```text
vp test run apps/server/src/cli/pair.fork.test.ts   3 passed
vp run fork:delta --check                           ok: 220 fork commits tagged
```

## Grounding

A real `t3 serve` on the branch, started with no `--host` and its own base dir, is exactly the reported precondition: the runtime record carries no `host` and an origin of `127.0.0.1`.

```json
{"version":1,"pid":3596040,"port":7911,"origin":"http://127.0.0.1:7911", ...}
```

`--json` and the human path were then read off that one server. The human path still resolves through `resolveDirectPairingBaseUrl`, so it is the pre-fix value for the same state.

```text
pair           Pairing URL: http://localhost:7911/pair#token=…     (upstream base, untouched)
pair --json    "pairingUrl": "http://127.0.0.1:7911/pair#token=…"
               "origin":     "http://127.0.0.1:7911"
```

Applying the desktop's own check — `pairingUrl.origin === httpBaseUrl.origin`, where `httpBaseUrl` is the recorded `origin` (`DesktopRunningLocalServers.ts:83,174`):

| pairing base | pairingUrl.origin | matches recorded origin |
| --- | --- | --- |
| upstream base (pre-fix `--json`) | `http://localhost:7911` | no |
| recorded origin (this change) | `http://127.0.0.1:7911` | yes |

**A configured host still keeps the upstream base.** A second server on the same branch with `--host 0.0.0.0` records the host, so `--json` takes the untouched path and resolves to the externally reachable address rather than to loopback.

```json
state    {"host":"0.0.0.0","port":7912,"origin":"http://127.0.0.1:7912"}
--json   "pairingUrl": "http://192.168.0.60:7912/pair#token=…"
```

One observation outside this change: for that wildcard server `pairingUrl.origin` and the recorded `origin` still differ, because upstream records loopback as the origin whatever the bind address is. The desktop's local-server auto-pairing would reject it for the same reason it rejected the no-host case. That is upstream behaviour on a path this change deliberately leaves alone, and it is not what #902 reported; filing it separately if it proves to matter.

Refs #902

https://claude.ai/code/session_012duArYgsDfEoAPcp9ZUXdr

## Fork trailers

Squashes:

- bc24ddf68a5 fix(server): pair --json through the recorded origin when no host is set
- 8618b0b9460 fix(client-runtime): an attached primary no longer reconnects on every platform poll (#1477) (#1477)
- 2f240119896 fix(fork): keep the client usable while the attached server restarts (#1712) (#1712)
- 6b19887bb4b fix(desktop): keep the attached server through a stall (#1721) (#1721)
- bcb0971cfd2 fix(web): update everything updates the attached service in client-only mode (#1751) (#1751)
- 6f68726fc5f fix(desktop): wait for the service update handoff before startup discovery (#1838) (#1838)
- a67ac66cef1 docs(backend-attach): describe the attached primary and its seams (#1861) (#1861)
- ba31fdd764e fix(web): client-only Connections hides managed-server controls (#1867) (#1867)
- 8fcfc1dd625 refactor(desktop)!: retire backend-attach for upstream Local-off (#1954) (#1954)

Fork-Domain: backend-attach
Fork-Tier: core
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 12, 2026
Desktop auto-pairing always failed against a local server started with no explicit host. `pair --json` emitted `localhost` while runtime state recorded `127.0.0.1`, and the desktop compares the two origins strictly.

## Change

`apps/server/src/cli/pair.ts` resolves the machine-readable pairing URL through the recorded origin when no host is set. Human output, `resolveDirectPairingBaseUrl`, the upstream localhost fallback, and the desktop's strict comparison are untouched, so a `--host 0.0.0.0` server still records an externally reachable connection string and that stays the pairing URL.

## Checks

```text
vp test run apps/server/src/cli/pair.fork.test.ts   3 passed
vp run fork:delta --check                           ok: 220 fork commits tagged
```

## Grounding

A real `t3 serve` on the branch, started with no `--host` and its own base dir, is exactly the reported precondition: the runtime record carries no `host` and an origin of `127.0.0.1`.

```json
{"version":1,"pid":3596040,"port":7911,"origin":"http://127.0.0.1:7911", ...}
```

`--json` and the human path were then read off that one server. The human path still resolves through `resolveDirectPairingBaseUrl`, so it is the pre-fix value for the same state.

```text
pair           Pairing URL: http://localhost:7911/pair#token=…     (upstream base, untouched)
pair --json    "pairingUrl": "http://127.0.0.1:7911/pair#token=…"
               "origin":     "http://127.0.0.1:7911"
```

Applying the desktop's own check — `pairingUrl.origin === httpBaseUrl.origin`, where `httpBaseUrl` is the recorded `origin` (`DesktopRunningLocalServers.ts:83,174`):

| pairing base | pairingUrl.origin | matches recorded origin |
| --- | --- | --- |
| upstream base (pre-fix `--json`) | `http://localhost:7911` | no |
| recorded origin (this change) | `http://127.0.0.1:7911` | yes |

**A configured host still keeps the upstream base.** A second server on the same branch with `--host 0.0.0.0` records the host, so `--json` takes the untouched path and resolves to the externally reachable address rather than to loopback.

```json
state    {"host":"0.0.0.0","port":7912,"origin":"http://127.0.0.1:7912"}
--json   "pairingUrl": "http://192.168.0.60:7912/pair#token=…"
```

One observation outside this change: for that wildcard server `pairingUrl.origin` and the recorded `origin` still differ, because upstream records loopback as the origin whatever the bind address is. The desktop's local-server auto-pairing would reject it for the same reason it rejected the no-host case. That is upstream behaviour on a path this change deliberately leaves alone, and it is not what #902 reported; filing it separately if it proves to matter.

Refs #902

https://claude.ai/code/session_012duArYgsDfEoAPcp9ZUXdr

## Fork trailers

Squashes:

- bc24ddf68a5 fix(server): pair --json through the recorded origin when no host is set
- 8618b0b9460 fix(client-runtime): an attached primary no longer reconnects on every platform poll (#1477) (#1477)
- 2f240119896 fix(fork): keep the client usable while the attached server restarts (#1712) (#1712)
- 6b19887bb4b fix(desktop): keep the attached server through a stall (#1721) (#1721)
- bcb0971cfd2 fix(web): update everything updates the attached service in client-only mode (#1751) (#1751)
- 6f68726fc5f fix(desktop): wait for the service update handoff before startup discovery (#1838) (#1838)
- a67ac66cef1 docs(backend-attach): describe the attached primary and its seams (#1861) (#1861)
- ba31fdd764e fix(web): client-only Connections hides managed-server controls (#1867) (#1867)
- 8fcfc1dd625 refactor(desktop)!: retire backend-attach for upstream Local-off (#1954) (#1954)

Fork-Domain: backend-attach
Fork-Tier: core
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 12, 2026
Desktop auto-pairing always failed against a local server started with no explicit host. `pair --json` emitted `localhost` while runtime state recorded `127.0.0.1`, and the desktop compares the two origins strictly.

## Change

`apps/server/src/cli/pair.ts` resolves the machine-readable pairing URL through the recorded origin when no host is set. Human output, `resolveDirectPairingBaseUrl`, the upstream localhost fallback, and the desktop's strict comparison are untouched, so a `--host 0.0.0.0` server still records an externally reachable connection string and that stays the pairing URL.

## Checks

```text
vp test run apps/server/src/cli/pair.fork.test.ts   3 passed
vp run fork:delta --check                           ok: 220 fork commits tagged
```

## Grounding

A real `t3 serve` on the branch, started with no `--host` and its own base dir, is exactly the reported precondition: the runtime record carries no `host` and an origin of `127.0.0.1`.

```json
{"version":1,"pid":3596040,"port":7911,"origin":"http://127.0.0.1:7911", ...}
```

`--json` and the human path were then read off that one server. The human path still resolves through `resolveDirectPairingBaseUrl`, so it is the pre-fix value for the same state.

```text
pair           Pairing URL: http://localhost:7911/pair#token=…     (upstream base, untouched)
pair --json    "pairingUrl": "http://127.0.0.1:7911/pair#token=…"
               "origin":     "http://127.0.0.1:7911"
```

Applying the desktop's own check — `pairingUrl.origin === httpBaseUrl.origin`, where `httpBaseUrl` is the recorded `origin` (`DesktopRunningLocalServers.ts:83,174`):

| pairing base | pairingUrl.origin | matches recorded origin |
| --- | --- | --- |
| upstream base (pre-fix `--json`) | `http://localhost:7911` | no |
| recorded origin (this change) | `http://127.0.0.1:7911` | yes |

**A configured host still keeps the upstream base.** A second server on the same branch with `--host 0.0.0.0` records the host, so `--json` takes the untouched path and resolves to the externally reachable address rather than to loopback.

```json
state    {"host":"0.0.0.0","port":7912,"origin":"http://127.0.0.1:7912"}
--json   "pairingUrl": "http://192.168.0.60:7912/pair#token=…"
```

One observation outside this change: for that wildcard server `pairingUrl.origin` and the recorded `origin` still differ, because upstream records loopback as the origin whatever the bind address is. The desktop's local-server auto-pairing would reject it for the same reason it rejected the no-host case. That is upstream behaviour on a path this change deliberately leaves alone, and it is not what #902 reported; filing it separately if it proves to matter.

Refs #902

https://claude.ai/code/session_012duArYgsDfEoAPcp9ZUXdr

## Fork trailers

Squashes:

- bc24ddf68a5 fix(server): pair --json through the recorded origin when no host is set
- 8618b0b9460 fix(client-runtime): an attached primary no longer reconnects on every platform poll (#1477) (#1477)
- 2f240119896 fix(fork): keep the client usable while the attached server restarts (#1712) (#1712)
- 6b19887bb4b fix(desktop): keep the attached server through a stall (#1721) (#1721)
- bcb0971cfd2 fix(web): update everything updates the attached service in client-only mode (#1751) (#1751)
- 6f68726fc5f fix(desktop): wait for the service update handoff before startup discovery (#1838) (#1838)
- a67ac66cef1 docs(backend-attach): describe the attached primary and its seams (#1861) (#1861)
- ba31fdd764e fix(web): client-only Connections hides managed-server controls (#1867) (#1867)
- 8fcfc1dd625 refactor(desktop)!: retire backend-attach for upstream Local-off (#1954) (#1954)

Fork-Domain: backend-attach
Fork-Tier: core
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 13, 2026
Desktop auto-pairing always failed against a local server started with no explicit host. `pair --json` emitted `localhost` while runtime state recorded `127.0.0.1`, and the desktop compares the two origins strictly.

## Change

`apps/server/src/cli/pair.ts` resolves the machine-readable pairing URL through the recorded origin when no host is set. Human output, `resolveDirectPairingBaseUrl`, the upstream localhost fallback, and the desktop's strict comparison are untouched, so a `--host 0.0.0.0` server still records an externally reachable connection string and that stays the pairing URL.

## Checks

```text
vp test run apps/server/src/cli/pair.fork.test.ts   3 passed
vp run fork:delta --check                           ok: 220 fork commits tagged
```

## Grounding

A real `t3 serve` on the branch, started with no `--host` and its own base dir, is exactly the reported precondition: the runtime record carries no `host` and an origin of `127.0.0.1`.

```json
{"version":1,"pid":3596040,"port":7911,"origin":"http://127.0.0.1:7911", ...}
```

`--json` and the human path were then read off that one server. The human path still resolves through `resolveDirectPairingBaseUrl`, so it is the pre-fix value for the same state.

```text
pair           Pairing URL: http://localhost:7911/pair#token=…     (upstream base, untouched)
pair --json    "pairingUrl": "http://127.0.0.1:7911/pair#token=…"
               "origin":     "http://127.0.0.1:7911"
```

Applying the desktop's own check — `pairingUrl.origin === httpBaseUrl.origin`, where `httpBaseUrl` is the recorded `origin` (`DesktopRunningLocalServers.ts:83,174`):

| pairing base | pairingUrl.origin | matches recorded origin |
| --- | --- | --- |
| upstream base (pre-fix `--json`) | `http://localhost:7911` | no |
| recorded origin (this change) | `http://127.0.0.1:7911` | yes |

**A configured host still keeps the upstream base.** A second server on the same branch with `--host 0.0.0.0` records the host, so `--json` takes the untouched path and resolves to the externally reachable address rather than to loopback.

```json
state    {"host":"0.0.0.0","port":7912,"origin":"http://127.0.0.1:7912"}
--json   "pairingUrl": "http://192.168.0.60:7912/pair#token=…"
```

One observation outside this change: for that wildcard server `pairingUrl.origin` and the recorded `origin` still differ, because upstream records loopback as the origin whatever the bind address is. The desktop's local-server auto-pairing would reject it for the same reason it rejected the no-host case. That is upstream behaviour on a path this change deliberately leaves alone, and it is not what #902 reported; filing it separately if it proves to matter.

Refs #902

https://claude.ai/code/session_012duArYgsDfEoAPcp9ZUXdr

## Fork trailers

Squashes:

- bc24ddf68a5 fix(server): pair --json through the recorded origin when no host is set
- 8618b0b9460 fix(client-runtime): an attached primary no longer reconnects on every platform poll (#1477) (#1477)
- 2f240119896 fix(fork): keep the client usable while the attached server restarts (#1712) (#1712)
- 6b19887bb4b fix(desktop): keep the attached server through a stall (#1721) (#1721)
- bcb0971cfd2 fix(web): update everything updates the attached service in client-only mode (#1751) (#1751)
- 6f68726fc5f fix(desktop): wait for the service update handoff before startup discovery (#1838) (#1838)
- a67ac66cef1 docs(backend-attach): describe the attached primary and its seams (#1861) (#1861)
- ba31fdd764e fix(web): client-only Connections hides managed-server controls (#1867) (#1867)
- 8fcfc1dd625 refactor(desktop)!: retire backend-attach for upstream Local-off (#1954) (#1954)

Fork-Domain: backend-attach
Fork-Tier: core
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 13, 2026
Desktop auto-pairing always failed against a local server started with no explicit host. `pair --json` emitted `localhost` while runtime state recorded `127.0.0.1`, and the desktop compares the two origins strictly.

## Change

`apps/server/src/cli/pair.ts` resolves the machine-readable pairing URL through the recorded origin when no host is set. Human output, `resolveDirectPairingBaseUrl`, the upstream localhost fallback, and the desktop's strict comparison are untouched, so a `--host 0.0.0.0` server still records an externally reachable connection string and that stays the pairing URL.

## Checks

```text
vp test run apps/server/src/cli/pair.fork.test.ts   3 passed
vp run fork:delta --check                           ok: 220 fork commits tagged
```

## Grounding

A real `t3 serve` on the branch, started with no `--host` and its own base dir, is exactly the reported precondition: the runtime record carries no `host` and an origin of `127.0.0.1`.

```json
{"version":1,"pid":3596040,"port":7911,"origin":"http://127.0.0.1:7911", ...}
```

`--json` and the human path were then read off that one server. The human path still resolves through `resolveDirectPairingBaseUrl`, so it is the pre-fix value for the same state.

```text
pair           Pairing URL: http://localhost:7911/pair#token=…     (upstream base, untouched)
pair --json    "pairingUrl": "http://127.0.0.1:7911/pair#token=…"
               "origin":     "http://127.0.0.1:7911"
```

Applying the desktop's own check — `pairingUrl.origin === httpBaseUrl.origin`, where `httpBaseUrl` is the recorded `origin` (`DesktopRunningLocalServers.ts:83,174`):

| pairing base | pairingUrl.origin | matches recorded origin |
| --- | --- | --- |
| upstream base (pre-fix `--json`) | `http://localhost:7911` | no |
| recorded origin (this change) | `http://127.0.0.1:7911` | yes |

**A configured host still keeps the upstream base.** A second server on the same branch with `--host 0.0.0.0` records the host, so `--json` takes the untouched path and resolves to the externally reachable address rather than to loopback.

```json
state    {"host":"0.0.0.0","port":7912,"origin":"http://127.0.0.1:7912"}
--json   "pairingUrl": "http://192.168.0.60:7912/pair#token=…"
```

One observation outside this change: for that wildcard server `pairingUrl.origin` and the recorded `origin` still differ, because upstream records loopback as the origin whatever the bind address is. The desktop's local-server auto-pairing would reject it for the same reason it rejected the no-host case. That is upstream behaviour on a path this change deliberately leaves alone, and it is not what #902 reported; filing it separately if it proves to matter.

Refs #902

https://claude.ai/code/session_012duArYgsDfEoAPcp9ZUXdr

## Fork trailers

Squashes:

- bc24ddf68a5 fix(server): pair --json through the recorded origin when no host is set
- 8618b0b9460 fix(client-runtime): an attached primary no longer reconnects on every platform poll (#1477) (#1477)
- 2f240119896 fix(fork): keep the client usable while the attached server restarts (#1712) (#1712)
- 6b19887bb4b fix(desktop): keep the attached server through a stall (#1721) (#1721)
- bcb0971cfd2 fix(web): update everything updates the attached service in client-only mode (#1751) (#1751)
- 6f68726fc5f fix(desktop): wait for the service update handoff before startup discovery (#1838) (#1838)
- a67ac66cef1 docs(backend-attach): describe the attached primary and its seams (#1861) (#1861)
- ba31fdd764e fix(web): client-only Connections hides managed-server controls (#1867) (#1867)
- 8fcfc1dd625 refactor(desktop)!: retire backend-attach for upstream Local-off (#1954) (#1954)

Fork-Domain: backend-attach
Fork-Tier: core
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 13, 2026
## What Changed

The desktop probes for a live local server before it spawns one.

- `packages/shared` gains a reader for `server-runtime.json`, so both the CLI and the desktop main process resolve a running server the same way.
- `t3 pair` gains `--json`, so a credential can be minted without parsing a QR code.
- The desktop runs in one of two backend modes. `managed` spawns and owns a backend. `client-only` attaches to one it did not start, never terminates it, and reconnects with backoff when it goes away. A live `server-runtime.json` flips a `managed` launch to `client-only`; `--backend-mode managed|client-only` overrides. Discovery is skipped in development.
- The web client pairs a client-only launch with the server it attached to, so a cold launch reaches an authenticated session with no pairing URL pasted by hand.
- The connection catalog is written mode `0600`. It holds every credential the app has been given.
- One `fork-sync` repair commit formats four files the `v0.0.41-nightly.20260910.1473` walk left unformatted. `vp check` is a required step and the fork runs CI only on a pull request, so the drift first surfaced here.

## Why

`t3code-backend.service` already serves this machine on `127.0.0.1:3773` and `https://t3.donjor.net`. Opening the desktop app started a second server against the same `~/.t3`, and quitting it took the service down with it.

The pool was already multi-instance and the renderer was already a pure client, so the gap was registration and ownership, not architecture.

## Verification

Run on the packaged AppImage built from the trial tree, against the real `t3code-backend.service`. The head since moved fork test cases into fork-owned `*.fork.test.ts` siblings and raised the budget rows; no product source changed after the trial.

| Behaviour | Result |
| --- | --- |
| Launch, service up, no flag | `client-only` / `existing-server`, one environment, no second listener |
| Authenticated session | bearer profile minted from `server-runtime.json`, no pairing paste |
| Catalog file mode | `664` before the write, `600` after |
| `systemctl --user restart` under an open window | service `3870853` to `1039036`, desktop PID unchanged, reconnected |
| Quit | service active, `https://t3.donjor.net` 200 |
| Launch, service down | `managed` / `settings`, spawned child on `127.0.0.1:3773`, exits with its parent |

```text
25 attach test files, 263 passed
typecheck: desktop, contracts, web, client-runtime — exit 0
vp run --filter @t3tools/web build — exit 0
vp run fork:delta --check — ok: 200 fork commits tagged
vp run fork:scan — 0 adopted authoring guard warnings
```

## UI Changes

None. The window is identical; only which backend it talks to changes.

## Checklist

- [x] This PR is small and focused
- [x] I explained what changed and why
- [x] I included before/after screenshots for any UI changes
- [x] I included a video for animation/interaction changes

Closes #742
Closes #743
Closes #744
Refs #735

Squashes:

- ee2d47bf0f feat(desktop): attach to a running local backend instead of spawning a second one (#1575) (#1516) (#1593) (#1811)
- ea7944ca72 feat(desktop): attach one local server as primary in client-only mode
- 8b231f2f1f fix(server): pair --json through the recorded origin when no host is set (#1477) (#1712) (#1721) (#1751) (#1838) (#1861) (#1867) (#1954)

Fork-Domain: backend-attach
Fork-Tier: core
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 13, 2026
## What Changed

The desktop probes for a live local server before it spawns one.

- `packages/shared` gains a reader for `server-runtime.json`, so both the CLI and the desktop main process resolve a running server the same way.
- `t3 pair` gains `--json`, so a credential can be minted without parsing a QR code.
- The desktop runs in one of two backend modes. `managed` spawns and owns a backend. `client-only` attaches to one it did not start, never terminates it, and reconnects with backoff when it goes away. A live `server-runtime.json` flips a `managed` launch to `client-only`; `--backend-mode managed|client-only` overrides. Discovery is skipped in development.
- The web client pairs a client-only launch with the server it attached to, so a cold launch reaches an authenticated session with no pairing URL pasted by hand.
- The connection catalog is written mode `0600`. It holds every credential the app has been given.
- One `fork-sync` repair commit formats four files the `v0.0.41-nightly.20260910.1473` walk left unformatted. `vp check` is a required step and the fork runs CI only on a pull request, so the drift first surfaced here.

## Why

`t3code-backend.service` already serves this machine on `127.0.0.1:3773` and `https://t3.donjor.net`. Opening the desktop app started a second server against the same `~/.t3`, and quitting it took the service down with it.

The pool was already multi-instance and the renderer was already a pure client, so the gap was registration and ownership, not architecture.

## Verification

Run on the packaged AppImage built from the trial tree, against the real `t3code-backend.service`. The head since moved fork test cases into fork-owned `*.fork.test.ts` siblings and raised the budget rows; no product source changed after the trial.

| Behaviour | Result |
| --- | --- |
| Launch, service up, no flag | `client-only` / `existing-server`, one environment, no second listener |
| Authenticated session | bearer profile minted from `server-runtime.json`, no pairing paste |
| Catalog file mode | `664` before the write, `600` after |
| `systemctl --user restart` under an open window | service `3870853` to `1039036`, desktop PID unchanged, reconnected |
| Quit | service active, `https://t3.donjor.net` 200 |
| Launch, service down | `managed` / `settings`, spawned child on `127.0.0.1:3773`, exits with its parent |

```text
25 attach test files, 263 passed
typecheck: desktop, contracts, web, client-runtime — exit 0
vp run --filter @t3tools/web build — exit 0
vp run fork:delta --check — ok: 200 fork commits tagged
vp run fork:scan — 0 adopted authoring guard warnings
```

## UI Changes

None. The window is identical; only which backend it talks to changes.

## Checklist

- [x] This PR is small and focused
- [x] I explained what changed and why
- [x] I included before/after screenshots for any UI changes
- [x] I included a video for animation/interaction changes

Closes #742
Closes #743
Closes #744
Refs #735

Squashes:

- ee2d47bf0f feat(desktop): attach to a running local backend instead of spawning a second one (#1575) (#1516) (#1593) (#1811)
- ea7944ca72 feat(desktop): attach one local server as primary in client-only mode
- 8b231f2f1f fix(server): pair --json through the recorded origin when no host is set (#1477) (#1712) (#1721) (#1751) (#1838) (#1861) (#1867) (#1954)

Fork-Domain: backend-attach
Fork-Tier: core
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 14, 2026
## What Changed

The desktop probes for a live local server before it spawns one.

- `packages/shared` gains a reader for `server-runtime.json`, so both the CLI and the desktop main process resolve a running server the same way.
- `t3 pair` gains `--json`, so a credential can be minted without parsing a QR code.
- The desktop runs in one of two backend modes. `managed` spawns and owns a backend. `client-only` attaches to one it did not start, never terminates it, and reconnects with backoff when it goes away. A live `server-runtime.json` flips a `managed` launch to `client-only`; `--backend-mode managed|client-only` overrides. Discovery is skipped in development.
- The web client pairs a client-only launch with the server it attached to, so a cold launch reaches an authenticated session with no pairing URL pasted by hand.
- The connection catalog is written mode `0600`. It holds every credential the app has been given.
- One `fork-sync` repair commit formats four files the `v0.0.41-nightly.20260910.1473` walk left unformatted. `vp check` is a required step and the fork runs CI only on a pull request, so the drift first surfaced here.

## Why

`t3code-backend.service` already serves this machine on `127.0.0.1:3773` and `https://t3.donjor.net`. Opening the desktop app started a second server against the same `~/.t3`, and quitting it took the service down with it.

The pool was already multi-instance and the renderer was already a pure client, so the gap was registration and ownership, not architecture.

## Verification

Run on the packaged AppImage built from the trial tree, against the real `t3code-backend.service`. The head since moved fork test cases into fork-owned `*.fork.test.ts` siblings and raised the budget rows; no product source changed after the trial.

| Behaviour | Result |
| --- | --- |
| Launch, service up, no flag | `client-only` / `existing-server`, one environment, no second listener |
| Authenticated session | bearer profile minted from `server-runtime.json`, no pairing paste |
| Catalog file mode | `664` before the write, `600` after |
| `systemctl --user restart` under an open window | service `3870853` to `1039036`, desktop PID unchanged, reconnected |
| Quit | service active, `https://t3.donjor.net` 200 |
| Launch, service down | `managed` / `settings`, spawned child on `127.0.0.1:3773`, exits with its parent |

```text
25 attach test files, 263 passed
typecheck: desktop, contracts, web, client-runtime — exit 0
vp run --filter @t3tools/web build — exit 0
vp run fork:delta --check — ok: 200 fork commits tagged
vp run fork:scan — 0 adopted authoring guard warnings
```

## UI Changes

None. The window is identical; only which backend it talks to changes.

## Checklist

- [x] This PR is small and focused
- [x] I explained what changed and why
- [x] I included before/after screenshots for any UI changes
- [x] I included a video for animation/interaction changes

Closes #742
Closes #743
Closes #744
Refs #735

Squashes:

- ee2d47bf0f feat(desktop): attach to a running local backend instead of spawning a second one (#1575) (#1516) (#1593) (#1811)
- ea7944ca72 feat(desktop): attach one local server as primary in client-only mode
- 8b231f2f1f fix(server): pair --json through the recorded origin when no host is set (#1477) (#1712) (#1721) (#1751) (#1838) (#1861) (#1867) (#1954)

Fork-Domain: backend-attach
Fork-Tier: core
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 14, 2026
## What Changed

The desktop probes for a live local server before it spawns one.

- `packages/shared` gains a reader for `server-runtime.json`, so both the CLI and the desktop main process resolve a running server the same way.
- `t3 pair` gains `--json`, so a credential can be minted without parsing a QR code.
- The desktop runs in one of two backend modes. `managed` spawns and owns a backend. `client-only` attaches to one it did not start, never terminates it, and reconnects with backoff when it goes away. A live `server-runtime.json` flips a `managed` launch to `client-only`; `--backend-mode managed|client-only` overrides. Discovery is skipped in development.
- The web client pairs a client-only launch with the server it attached to, so a cold launch reaches an authenticated session with no pairing URL pasted by hand.
- The connection catalog is written mode `0600`. It holds every credential the app has been given.
- One `fork-sync` repair commit formats four files the `v0.0.41-nightly.20260910.1473` walk left unformatted. `vp check` is a required step and the fork runs CI only on a pull request, so the drift first surfaced here.

## Why

`t3code-backend.service` already serves this machine on `127.0.0.1:3773` and `https://t3.donjor.net`. Opening the desktop app started a second server against the same `~/.t3`, and quitting it took the service down with it.

The pool was already multi-instance and the renderer was already a pure client, so the gap was registration and ownership, not architecture.

## Verification

Run on the packaged AppImage built from the trial tree, against the real `t3code-backend.service`. The head since moved fork test cases into fork-owned `*.fork.test.ts` siblings and raised the budget rows; no product source changed after the trial.

| Behaviour | Result |
| --- | --- |
| Launch, service up, no flag | `client-only` / `existing-server`, one environment, no second listener |
| Authenticated session | bearer profile minted from `server-runtime.json`, no pairing paste |
| Catalog file mode | `664` before the write, `600` after |
| `systemctl --user restart` under an open window | service `3870853` to `1039036`, desktop PID unchanged, reconnected |
| Quit | service active, `https://t3.donjor.net` 200 |
| Launch, service down | `managed` / `settings`, spawned child on `127.0.0.1:3773`, exits with its parent |

```text
25 attach test files, 263 passed
typecheck: desktop, contracts, web, client-runtime — exit 0
vp run --filter @t3tools/web build — exit 0
vp run fork:delta --check — ok: 200 fork commits tagged
vp run fork:scan — 0 adopted authoring guard warnings
```

## UI Changes

None. The window is identical; only which backend it talks to changes.

## Checklist

- [x] This PR is small and focused
- [x] I explained what changed and why
- [x] I included before/after screenshots for any UI changes
- [x] I included a video for animation/interaction changes

Closes #742
Closes #743
Closes #744
Refs #735

Squashes:

- ee2d47bf0f feat(desktop): attach to a running local backend instead of spawning a second one (#1575) (#1516) (#1593) (#1811)
- ea7944ca72 feat(desktop): attach one local server as primary in client-only mode
- 8b231f2f1f fix(server): pair --json through the recorded origin when no host is set (#1477) (#1712) (#1721) (#1751) (#1838) (#1861) (#1867) (#1954)

Fork-Domain: backend-attach
Fork-Tier: core
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 14, 2026
## What Changed

The desktop probes for a live local server before it spawns one.

- `packages/shared` gains a reader for `server-runtime.json`, so both the CLI and the desktop main process resolve a running server the same way.
- `t3 pair` gains `--json`, so a credential can be minted without parsing a QR code.
- The desktop runs in one of two backend modes. `managed` spawns and owns a backend. `client-only` attaches to one it did not start, never terminates it, and reconnects with backoff when it goes away. A live `server-runtime.json` flips a `managed` launch to `client-only`; `--backend-mode managed|client-only` overrides. Discovery is skipped in development.
- The web client pairs a client-only launch with the server it attached to, so a cold launch reaches an authenticated session with no pairing URL pasted by hand.
- The connection catalog is written mode `0600`. It holds every credential the app has been given.
- One `fork-sync` repair commit formats four files the `v0.0.41-nightly.20260910.1473` walk left unformatted. `vp check` is a required step and the fork runs CI only on a pull request, so the drift first surfaced here.

## Why

`t3code-backend.service` already serves this machine on `127.0.0.1:3773` and `https://t3.donjor.net`. Opening the desktop app started a second server against the same `~/.t3`, and quitting it took the service down with it.

The pool was already multi-instance and the renderer was already a pure client, so the gap was registration and ownership, not architecture.

## Verification

Run on the packaged AppImage built from the trial tree, against the real `t3code-backend.service`. The head since moved fork test cases into fork-owned `*.fork.test.ts` siblings and raised the budget rows; no product source changed after the trial.

| Behaviour | Result |
| --- | --- |
| Launch, service up, no flag | `client-only` / `existing-server`, one environment, no second listener |
| Authenticated session | bearer profile minted from `server-runtime.json`, no pairing paste |
| Catalog file mode | `664` before the write, `600` after |
| `systemctl --user restart` under an open window | service `3870853` to `1039036`, desktop PID unchanged, reconnected |
| Quit | service active, `https://t3.donjor.net` 200 |
| Launch, service down | `managed` / `settings`, spawned child on `127.0.0.1:3773`, exits with its parent |

```text
25 attach test files, 263 passed
typecheck: desktop, contracts, web, client-runtime — exit 0
vp run --filter @t3tools/web build — exit 0
vp run fork:delta --check — ok: 200 fork commits tagged
vp run fork:scan — 0 adopted authoring guard warnings
```

## UI Changes

None. The window is identical; only which backend it talks to changes.

## Checklist

- [x] This PR is small and focused
- [x] I explained what changed and why
- [x] I included before/after screenshots for any UI changes
- [x] I included a video for animation/interaction changes

Closes #742
Closes #743
Closes #744
Refs #735

Squashes:

- ee2d47bf0f feat(desktop): attach to a running local backend instead of spawning a second one (#1575) (#1516) (#1593) (#1811)
- ea7944ca72 feat(desktop): attach one local server as primary in client-only mode
- 8b231f2f1f fix(server): pair --json through the recorded origin when no host is set (#1477) (#1712) (#1721) (#1751) (#1838) (#1861) (#1867) (#1954)

Fork-Domain: backend-attach
Fork-Tier: core
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 14, 2026
## What Changed

The desktop probes for a live local server before it spawns one.

- `packages/shared` gains a reader for `server-runtime.json`, so both the CLI and the desktop main process resolve a running server the same way.
- `t3 pair` gains `--json`, so a credential can be minted without parsing a QR code.
- The desktop runs in one of two backend modes. `managed` spawns and owns a backend. `client-only` attaches to one it did not start, never terminates it, and reconnects with backoff when it goes away. A live `server-runtime.json` flips a `managed` launch to `client-only`; `--backend-mode managed|client-only` overrides. Discovery is skipped in development.
- The web client pairs a client-only launch with the server it attached to, so a cold launch reaches an authenticated session with no pairing URL pasted by hand.
- The connection catalog is written mode `0600`. It holds every credential the app has been given.
- One `fork-sync` repair commit formats four files the `v0.0.41-nightly.20260910.1473` walk left unformatted. `vp check` is a required step and the fork runs CI only on a pull request, so the drift first surfaced here.

## Why

`t3code-backend.service` already serves this machine on `127.0.0.1:3773` and `https://t3.donjor.net`. Opening the desktop app started a second server against the same `~/.t3`, and quitting it took the service down with it.

The pool was already multi-instance and the renderer was already a pure client, so the gap was registration and ownership, not architecture.

## Verification

Run on the packaged AppImage built from the trial tree, against the real `t3code-backend.service`. The head since moved fork test cases into fork-owned `*.fork.test.ts` siblings and raised the budget rows; no product source changed after the trial.

| Behaviour | Result |
| --- | --- |
| Launch, service up, no flag | `client-only` / `existing-server`, one environment, no second listener |
| Authenticated session | bearer profile minted from `server-runtime.json`, no pairing paste |
| Catalog file mode | `664` before the write, `600` after |
| `systemctl --user restart` under an open window | service `3870853` to `1039036`, desktop PID unchanged, reconnected |
| Quit | service active, `https://t3.donjor.net` 200 |
| Launch, service down | `managed` / `settings`, spawned child on `127.0.0.1:3773`, exits with its parent |

```text
25 attach test files, 263 passed
typecheck: desktop, contracts, web, client-runtime — exit 0
vp run --filter @t3tools/web build — exit 0
vp run fork:delta --check — ok: 200 fork commits tagged
vp run fork:scan — 0 adopted authoring guard warnings
```

## UI Changes

None. The window is identical; only which backend it talks to changes.

## Checklist

- [x] This PR is small and focused
- [x] I explained what changed and why
- [x] I included before/after screenshots for any UI changes
- [x] I included a video for animation/interaction changes

Closes #742
Closes #743
Closes #744
Refs #735

Squashes:

- ee2d47bf0f feat(desktop): attach to a running local backend instead of spawning a second one (#1575) (#1516) (#1593) (#1811)
- ea7944ca72 feat(desktop): attach one local server as primary in client-only mode
- 8b231f2f1f fix(server): pair --json through the recorded origin when no host is set (#1477) (#1712) (#1721) (#1751) (#1838) (#1861) (#1867) (#1954)

Fork-Domain: backend-attach
Fork-Tier: core
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 15, 2026
## What Changed

The desktop probes for a live local server before it spawns one.

- `packages/shared` gains a reader for `server-runtime.json`, so both the CLI and the desktop main process resolve a running server the same way.
- `t3 pair` gains `--json`, so a credential can be minted without parsing a QR code.
- The desktop runs in one of two backend modes. `managed` spawns and owns a backend. `client-only` attaches to one it did not start, never terminates it, and reconnects with backoff when it goes away. A live `server-runtime.json` flips a `managed` launch to `client-only`; `--backend-mode managed|client-only` overrides. Discovery is skipped in development.
- The web client pairs a client-only launch with the server it attached to, so a cold launch reaches an authenticated session with no pairing URL pasted by hand.
- The connection catalog is written mode `0600`. It holds every credential the app has been given.
- One `fork-sync` repair commit formats four files the `v0.0.41-nightly.20260910.1473` walk left unformatted. `vp check` is a required step and the fork runs CI only on a pull request, so the drift first surfaced here.

## Why

`t3code-backend.service` already serves this machine on `127.0.0.1:3773` and `https://t3.donjor.net`. Opening the desktop app started a second server against the same `~/.t3`, and quitting it took the service down with it.

The pool was already multi-instance and the renderer was already a pure client, so the gap was registration and ownership, not architecture.

## Verification

Run on the packaged AppImage built from the trial tree, against the real `t3code-backend.service`. The head since moved fork test cases into fork-owned `*.fork.test.ts` siblings and raised the budget rows; no product source changed after the trial.

| Behaviour | Result |
| --- | --- |
| Launch, service up, no flag | `client-only` / `existing-server`, one environment, no second listener |
| Authenticated session | bearer profile minted from `server-runtime.json`, no pairing paste |
| Catalog file mode | `664` before the write, `600` after |
| `systemctl --user restart` under an open window | service `3870853` to `1039036`, desktop PID unchanged, reconnected |
| Quit | service active, `https://t3.donjor.net` 200 |
| Launch, service down | `managed` / `settings`, spawned child on `127.0.0.1:3773`, exits with its parent |

```text
25 attach test files, 263 passed
typecheck: desktop, contracts, web, client-runtime — exit 0
vp run --filter @t3tools/web build — exit 0
vp run fork:delta --check — ok: 200 fork commits tagged
vp run fork:scan — 0 adopted authoring guard warnings
```

## UI Changes

None. The window is identical; only which backend it talks to changes.

## Checklist

- [x] This PR is small and focused
- [x] I explained what changed and why
- [x] I included before/after screenshots for any UI changes
- [x] I included a video for animation/interaction changes

Closes #742
Closes #743
Closes #744
Refs #735

Squashes:

- ee2d47bf0f feat(desktop): attach to a running local backend instead of spawning a second one (#1575) (#1516) (#1593) (#1811)
- ea7944ca72 feat(desktop): attach one local server as primary in client-only mode
- 8b231f2f1f fix(server): pair --json through the recorded origin when no host is set (#1477) (#1712) (#1721) (#1751) (#1838) (#1861) (#1867) (#1954)

Fork-Domain: backend-attach
Fork-Tier: core
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 15, 2026
## What Changed

The desktop probes for a live local server before it spawns one.

- `packages/shared` gains a reader for `server-runtime.json`, so both the CLI and the desktop main process resolve a running server the same way.
- `t3 pair` gains `--json`, so a credential can be minted without parsing a QR code.
- The desktop runs in one of two backend modes. `managed` spawns and owns a backend. `client-only` attaches to one it did not start, never terminates it, and reconnects with backoff when it goes away. A live `server-runtime.json` flips a `managed` launch to `client-only`; `--backend-mode managed|client-only` overrides. Discovery is skipped in development.
- The web client pairs a client-only launch with the server it attached to, so a cold launch reaches an authenticated session with no pairing URL pasted by hand.
- The connection catalog is written mode `0600`. It holds every credential the app has been given.
- One `fork-sync` repair commit formats four files the `v0.0.41-nightly.20260910.1473` walk left unformatted. `vp check` is a required step and the fork runs CI only on a pull request, so the drift first surfaced here.

## Why

`t3code-backend.service` already serves this machine on `127.0.0.1:3773` and `https://t3.donjor.net`. Opening the desktop app started a second server against the same `~/.t3`, and quitting it took the service down with it.

The pool was already multi-instance and the renderer was already a pure client, so the gap was registration and ownership, not architecture.

## Verification

Run on the packaged AppImage built from the trial tree, against the real `t3code-backend.service`. The head since moved fork test cases into fork-owned `*.fork.test.ts` siblings and raised the budget rows; no product source changed after the trial.

| Behaviour | Result |
| --- | --- |
| Launch, service up, no flag | `client-only` / `existing-server`, one environment, no second listener |
| Authenticated session | bearer profile minted from `server-runtime.json`, no pairing paste |
| Catalog file mode | `664` before the write, `600` after |
| `systemctl --user restart` under an open window | service `3870853` to `1039036`, desktop PID unchanged, reconnected |
| Quit | service active, `https://t3.donjor.net` 200 |
| Launch, service down | `managed` / `settings`, spawned child on `127.0.0.1:3773`, exits with its parent |

```text
25 attach test files, 263 passed
typecheck: desktop, contracts, web, client-runtime — exit 0
vp run --filter @t3tools/web build — exit 0
vp run fork:delta --check — ok: 200 fork commits tagged
vp run fork:scan — 0 adopted authoring guard warnings
```

## UI Changes

None. The window is identical; only which backend it talks to changes.

## Checklist

- [x] This PR is small and focused
- [x] I explained what changed and why
- [x] I included before/after screenshots for any UI changes
- [x] I included a video for animation/interaction changes

Closes #742
Closes #743
Closes #744
Refs #735

Squashes:

- ee2d47bf0f feat(desktop): attach to a running local backend instead of spawning a second one (#1575) (#1516) (#1593) (#1811)
- ea7944ca72 feat(desktop): attach one local server as primary in client-only mode
- 8b231f2f1f fix(server): pair --json through the recorded origin when no host is set (#1477) (#1712) (#1721) (#1751) (#1838) (#1861) (#1867) (#1954)

Fork-Domain: backend-attach
Fork-Tier: core
Fork-Upstreamable: no
Co-authored-by: donjor <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

client-only Connections shows managed-server controls again [📥]

1 participant