Repository navigation
fix(server): share one pty when clients attach the same pane concurrently - #2
Merged
Merged
Conversation
…ntly ensureAttachment checked the attachment map, then awaited the terminal lookup before storing the new record. Two attaches to the same pane in that window each spawned their own `herdr terminal attach`. The later record replaced the earlier one in the map, but the orphaned pty's onData still looked the pane up by id, so it streamed into the surviving attachment: one client went silent, the other got output from two ptys, and the orphan's exit closed the surviving attachment. A concurrent attach now joins the in-flight creation through a pending map that is cleared on success and failure alike. A new contract test races two attaches on a fresh pane and checks a resize reaches both clients; with the fix reverted it fails 3/3. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
devswha
added a commit
to nahwan-kim/herdr-web-ui
that referenced
this pull request
Sep 29, 2026
Review fixes (CodeRabbit, and an adversarial review with Codex): - gh auth status asks only github.com, so enabling usage no longer contacts Enterprise hosts with their credentials or waits on them. - A GH_TOKEN/GITHUB_TOKEN sign-in is read with the plain token lookup; the per-user lookup has no stored token for it. - Copilot tokens of no known login are separate sign-ins, so one that answers first no longer hides the others. - A place that now holds another account's failing sign-in no longer shows the previous account's name, plan and numbers. - A 429 waits per account (or per private token fingerprint), so a sign-in moved or copied elsewhere does not ask again early. - Sign-ins of one provider that name no account are numbered (devswha#1, devswha#2), and a fallback key hashes where the sign-in was found instead of sending its path to the browser. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
devswha
added a commit
that referenced
this pull request
Oct 22, 2026
….*.ts (#1674) Second PR of the i18n split (#2 in the split plan, after the Settings pilot #1673). The strings that only the terminal's files use move out of the three base dictionaries into `src/lib/i18n/terminal.{ko,ja,zh}.ts`. `t()`, the English keys, every translation, placeholders, the English fallback and all call sites stay the same. ## What changes - **Moved:** 113 keys in each language, byte for byte, in the order each language's file had them. A key moves only when every file that uses it is a terminal file: `components/PaneTerminal.tsx`, `components/PaneCanvas.tsx`, `components/KeyBar.tsx` (with its `ARROWS` and `KEY_SPOKEN_NAMES` label maps), `components/FindBar.tsx`, `components/TerminalViewportTools.tsx`, `components/SplitDropPreview.tsx`, `components/TabStrip.tsx`, `lib/terminalLabel.ts` and `lib/tabName.ts`. Keys these files share with another feature stay in the base files. - **Composition:** each base file gains one import and one `KO_PARTS` / `JA_PARTS` / `ZH_PARTS` entry (`terminal: TERMINAL_KO`, …). Nothing else in the base files changes. - No test, docs or CONTRIBUTING change: the pilot's tests already cover every part, and CONTRIBUTING already says where a feature's strings go. ## Key list I regenerated the list with the split plan's ownership script on this branch's base, `a717621f`. That script collects keys the same way as `i18n.test.ts`: `t()`/`tt()` literals plus the 13 label maps, recorded per file. It finds 113 terminal keys, identical to the plan's list from `8b5aaba2`, with no key added or removed. Since then main gained only #1672 (no i18n change) and the pilot #1673 (which moved only Settings-only keys). ## Checks - **Same dictionaries:** the composed `KO`, `JA` and `ZH` (every key and value) dumped as sorted JSON on `a717621f` and on this branch are byte-identical (`EQUIVALENT`). - **`--color-moved` summary:** `git diff --color-moved=plain origin/main...HEAD` over the base and part files shows all 339 removed entry lines (113 × 3) as moved. The only non-moved lines are 33 added lines, 11 per language: - the import; - the parts entry; - the part file's 7-line header, its `export const` line and its closing `};`. No line is removed other than moved entries. - **Same keys moved:** the keys of `TERMINAL_KO`, `TERMINAL_JA` and `TERMINAL_ZH` each equal the 113-key list (`SAME-KEYS` for ko, ja and zh). The scan still finds all 1,389 keys, the same count as main. - **Part sizes:** each part file is 122 lines, under the 400-line cap. `i18n.ko.ts` goes from 1,180 to 1,069 lines, `i18n.ja.ts` from 1,182 to 1,071 and `i18n.zh.ts` from 1,184 to 1,073. - `HERDR_TEST_MODE=unit bun test src/lib/i18n.test.ts`: 21 pass, 0 fail. This covers missing, unused, placeholder and untranslated entries, a key in two parts, and different placement across languages. - `bun run typecheck`: passes. - `bun run check fast`: passes (unit 6127 pass, 0 fail, 12 skip; workflow syntax, generated types/themes, typecheck and build ok). This PR contains only moved lines, one import and one parts entry per language, and the part file header. It should qualify for the move-only merge path if the parts entry and header are counted as part of the move. ## Note After this move the `// ---- key bar ----` section comment in each base file has no entries left: its keys went to Settings (#1673) and here. The comment is left in place to keep this diff move-only; the last split PR can drop empty section comments. No changelog fragment: users see no change.
This was referenced Oct 27, 2026
devswha
added a commit
that referenced
this pull request
Oct 28, 2026
…efaults (#2276) ## Change Default native TypeScript 7 checks to `GOGC=400`, retaining garbage collection while spending modestly more RAM to reduce CPU. Typecheck's Go 1.26.4 `tsc` was the #2 CPU consumer on the shared P620 CI host. The original proposal was `GOGC=off GOMEMLIMIT=6GiB`. The maintainer's concurrency steer requires sizing for 24 simultaneous typechecks with approximately 40 GB free; the measurements below rule out a GC-off limit with the requested 1.5x headroom, so this PR uses the explicitly requested `GOGC=400` fallback instead. - `package.json` routes `typecheck` through a small Bun wrapper. Both CI's `check fast`/`check concurrent` and the remote-bundle jobs already invoke `bun run typecheck`; neither `scripts/check.ts` nor any workflow needs editing. - The wrapper retains the existing native compiler launcher, both tsconfig invocations, sequential execution, inherited stdio, and failure exit codes. - Caller `GOGC` wins, including an explicitly empty value. `GOMEMLIMIT` is never assigned or removed by the wrapper, so caller limits remain in force. - The wrapper passes a child environment and argument array, with no POSIX environment-assignment syntax or hard-coded Linux compiler path. This preserves TypeScript's platform package selection on Linux, macOS, and Windows. - Seven subprocess regression cases cover both compiler environments, default and independent/combined/empty overrides, argument forwarding, inherited unrelated environment, and stopping on either compiler's failure. - The explicit tools tsconfig now includes the wrapper and its tests so the normal typecheck validates both new files. - Includes an English maintenance changelog fragment. No workflow or dependency changes. ## Concurrency sizing Direct GNU `/usr/bin/time -v` measurements of `node_modules/@typescript/typescript-linux-x64/lib/tsc`, **not Bun's RSS**. Three runs per project and setting, each through `dori heavy ram-gogc-pr -- ...`; the table reports the maximum of those three runs. Both projects run sequentially. Baseline and unlimited-off measurements explicitly remove `GOMEMLIMIT`. | Native compiler setting | Main peak RSS MiB | Tools peak RSS MiB | | --- | ---: | ---: | | `GOGC=100`, no limit | 1205.29 | 606.29 | | `GOGC=off`, no limit | 1400.33 | 695.16 | | `GOGC=off GOMEMLIMIT=6GiB` | 1399.34 | 698.23 | | **Chosen: `GOGC=400`, no limit** | **1381.34** | **684.29** | Separate baseline `GODEBUG=gctrace=1` traces show largest post-GC live heaps (`X->Y->Z MB`, the `Z`) of **752 MiB main / 394 MiB tools**. These are observed live heaps, not a universal upper bound. The no-GC main peak times 1.5 is 2100.50 MiB, rounded up to **2112MiB**. That limit alone times 24 is **49.5 GiB**, before RSS overhead, exceeding both the approximately 30 GB target and approximately 40 GB free. Even observed unlimited-off RSS times 24 is 32.82 GiB / 35.24 decimal GB without growth headroom. There is no qualifying GC-off limit under the requested sizing rule. Baseline direct RSS times 24 is **28.25 GiB / 30.33 decimal GB**. The chosen direct peak times 24 is **32.38 GiB / 34.76 decimal GB**. The larger observed full-command high-water RSS gives 32.49 GiB / 34.89 decimal GB. The decision-relevant increment for these direct peaks is **176.05 MiB per compiler**, or **4.13 GiB / 4.43 decimal GB at 24**: the existing baseline memory is already consumed without this change. In the prior seven-round CI-image experiment below, the median increment is **157.00 MiB per compiler**, or **3.68 GiB / 3.95 decimal GB at 24**. The fallback is below approximately 40 GB in this measurement but **does not meet the preferred approximately 30 GB envelope**; it leaves about 5 GB for other memory. `GOGC=400` retains collection, not an RSS cap or a guarantee that 24 runners plus their overhead fit. `GOMEMLIMIT` remains available to the caller. Twenty-four simultaneous typechecks are a conservative planning case, not the usual workload: `CHECK_HOST_MAX_RUNS=4` bounds the herdr-backed lanes, whereas typecheck runs in Fast checks on up to 20 runners plus any applicable lanes. That lane bound is not a global semaphore for every Fast-check compiler. ## Performance evidence ### Prior CI-image experiment: seven rotating rounds Source: `/home/devswha/.cache/ram-offload-tmp/gogc/results.md`, snapshot `adfdef6e49767ec09323b9b2e1f0f151c6b84394`, Bun 1.4.2, TypeScript 7.0.2 / Go 1.26.4, `herdr-ci-runner:local` on the 128-CPU P620. Medians: | Setting | CPU user+sys s | Wall s | Max RSS MiB | | --- | ---: | ---: | ---: | | Default | 52.31 | 6.47 | 1188.54 | | **Chosen fallback: `GOGC=400`** | **38.74 (-25.94%)** | **6.14 (-5.10%)** | **1345.54 (+157.00)** | | Rejected: `GOGC=off GOMEMLIMIT=6GiB` | 23.49 (-55.09%) | 4.76 (-26.43%) | 1390.78 (+202.25) | Every one of the seven GOGC=400 runs used less CPU than the baseline median; its worst was 47.16 CPU-s versus 52.31 CPU-s. ### This worktree: five alternating baseline/chosen comparisons Rebased onto `792dbb98be8a2cd88b1123930a6aae8b131ebe23`; Bun 1.4.3, native TypeScript 7.0.2 / Go 1.26.4. GNU time wraps the real `bun run typecheck`. Baseline supplies `GOGC=100`; chosen leaves it unset and exercises the wrapper default. Both remove `GOMEMLIMIT`. Odd rounds run baseline then chosen, even rounds reverse that order. All runs use `dori heavy ram-gogc-pr -- ...`. | Round | Baseline CPU s | Chosen CPU s | Baseline wall s | Chosen wall s | Baseline RSS MiB | Chosen RSS MiB | | --- | ---: | ---: | ---: | ---: | ---: | ---: | | 1 | 59.58 | 43.57 | 6.34 | 7.37 | 1191.29 | 1310.29 | | 2 | 48.88 | 36.36 | 8.39 | 7.83 | 1174.24 | 1382.29 | | 3 | 66.91 | 42.98 | 7.38 | 6.25 | 1132.29 | 1386.23 | | 4 | 58.33 | 41.24 | 6.30 | 6.23 | 1132.29 | 1195.29 | | 5 | 64.05 | 61.80 | 9.07 | 10.97 | 1194.29 | 1191.29 | | **Median** | **59.58** | **42.98 (-27.86%)** | **7.38** | **7.37 (-0.14%)** | **1174.24** | **1310.29 (+136.05)** | Every chosen run beats its paired baseline on CPU; four of five also beat the baseline median. Round 5's 61.80 CPU-s exceeds the 59.58 CPU-s baseline median and is retained. Wall-time improvement is negligible on this busy shared host. Full-command RSS is GNU time's process-tree high-water statistic, not a sum of simultaneously resident processes. For the originally requested chronological three-run before/after comparison: the unchanged script was measured before tracked edits, so no stash was needed. Before CPU was 35.10 / 36.17 / 28.64 s, wall 4.32 / 4.95 / 5.94 s, and RSS 1202.23 / 1206.29 / 1207.29 MiB. The first three after runs are rounds 1-3 above. Their medians are **35.10 -> 42.98 CPU-s, 4.95 -> 7.37 wall-s, 1206.29 -> 1382.29 MiB**. That chronology-only comparison is slower; shared-host conditions differed between blocks. The alternating comparison above is the contemporaneous evidence for the win. No result was discarded. Full-command `GODEBUG=gctrace=1` counts: **24 before, 6 after** (3 per compiler). The 0-2 expectation was for GC off; the selected fallback deliberately retains GC. Raw logs, scripts, and the complete local report are retained at `/home/devswha/.cache/ram-offload-tmp/pr-tmp/` (`sizing-results.md`, `baseline-*.time`, `size-*.time`, `compare-*.time`, and `*.trace`). The table above uses `final/compare-*.time` and `final/size-chosen-*.time`. An earlier five-round comparison before adding the module marker and tools typechecking coverage is retained too: medians 55.60 -> 39.74 CPU-s (-28.53%), 7.03 -> 6.74 wall-s, 1193.21 -> 1250.16 MiB. The final tree was remeasured rather than substituting those earlier results. ## Validation - `dori heavy ram-gogc-pr -- bun test ./scripts/typecheck.test.ts`: 7 pass, 0 fail. - Native TS7 checks the new wrapper and tests through `tsconfig.tools.json`. - Required local `dori heavy ram-gogc-pr -- bun run check fast`: **exit 0**; **8202 pass, 0 fail, 33 skip in 530 files**. Workflow syntax, both generated-file freshness checks, both TypeScript projects, build, and unit tests all passed. Report `e39ee9c2-d32d-4758-89a6-7432c7db8945` has `status=success`, `verification.status=verified`, `source.unchanged=true`, and no errors; log: `check-fast-final.log`. - Final diff reviewed locally. An independent gate reviewer could not be started because this task is already at the harness's maximum subagent depth. - Native macOS and Windows runs were not executed locally; portability follows the existing launcher and Bun child-environment API, without shell assignments. - No live runner service/containers, panes, other worktrees, sysctl, swap, or sudo operations. Temporary evidence uses the requested `TMPDIR`; no shared cleanup. Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
This was referenced Nov 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
ensureAttachmentchecked the attachment map, then awaited the terminal lookup (session.snapshot) before storing the new record. When two attaches to the same pane landed in that window, each one spawned its ownherdr terminal attach. That can happen when a PC and a phone reconnect together after a server restart.onDatastill looked the pane up by id, so its output went into the surviving attachment.pty-data, nopane-geometry), and the other got output from two ptys.closeAttachment(paneId)closed the surviving attachment too.stop()never killed the orphan.Fix
pendingAttachmentsmap. The entry is cleared infinally, so a failed lookup (pane_not_found) doesn't leave the pane stuck.spawnAttachmentunchanged.attachments.setstill runs synchronously right beforenew PtySession(...).pane-geometry.Test
WebSocket concurrent attachruns on its ownherdr-web-ui-test-raceworkspace, so it doesn't depend on another test's pane. It sends two attaches in the same tick, waits forpty-dataon both sockets, then checks that a resize from one reaches both.server/index.tsfix revertedsecond hears resize×2,first pty-data×1)bun test server/api.contract.test.ts×4bun testbun run typecheck/bun run buildherdr terminal attach/pty-host.mjsprocesses, noherdr-web-ui-test*workspacesThe README test count (63) matches the suite again. The coverage sentence now lists concurrent attaches.
🤖 Generated with Claude Code