Skip to content

Commit 2b215fd

Browse files
release: 1.38.0 — reliable session close, terminal drawing, Apply update, performance (#46)
* fix(link): close the gaps a paired or unpaired peer could walk through - removeMachine no longer resurrects the machine: closing the link fired onClose -> scheduleRetry before the entry was deleted, so it redialed, re-saved the known host and kept delivering sessions. Entries are now marked removed and every callback checks it is still current. - A notify may only run `send` methods (it was running invoke methods such as cockpit:open with no rate limit); non-input notifies share a token bucket, async rejections are caught. - Only the session channels a host is allowed to push reach the renderer; a host can no longer forge `link:sessions` for another machine (or for 'local'). - Pre-auth limits: at most 8 unauthenticated connections, 64 KiB frames until ready, TLS handshake timeout, attach/detach only from a paired device; the frame decoder no longer copies O(n^2). - Audit rows from refusals and strangers are throttled so they cannot evict the history. - A hello->ready deadline so a host that stalls after TLS cannot wedge the dial forever. - An exited session is dropped from every connection's attached set (it kept hasRemoteViewers true forever, blocking idle shutdown); attach announces only when something changed. - Port-mapping renewals stop when the server they were started for is gone. - An already-paired device sending `pair` keeps its permissions; evicting the oldest device past the cap is audited. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YTT8PjCsRNq19Rc53mSSta (cherry picked from commit f924b93970bf80bd5b29c704714ede01d234778b) * fix(main): stop double-counting usage, leaking agent processes and killing work on a crash - Usage: two scans resuming the same grown transcript folded into the CACHED digest's objects, so concurrent scans (deck + usage view, or a remote viewer) added the new lines twice, for good (200,100 -> 400,100 tokens until restart). Folds go into copies and are serialized per file; the same fix for the per-session meta reader. - Windows: closing a session now kills its process tree (taskkill /T, then node-pty's kill). Measured: node-pty's console-only kill left a hidden child (the shape of an MCP server or a background dev server) running; the tree kill ends it. - A renderer crash no longer kills every terminal: the reload re-adopts the running sessions, so only DevDeck's own login shells are reaped. Measured: both sessions survive a forced renderer crash and come back as the same two tiles. - git runs with GIT_OPTIONAL_LOCKS=0 / GIT_TERMINAL_PROMPT=0, so the 30-45 s status polls stop taking index.lock out from under the agent's own git commands. - AI summaries: no shell:true (it corrupted `--mcp-config '{"mcpServers":{}}'` on Windows), a timed-out summarizer's whole tree is killed, failures are reported. - A resume id that fails validation is reported as null instead of naming a conversation the `-c` fallback never opened. - state.json rename retries on EPERM/EBUSY/EACCES; a failed shutdown.exe returns the scheduler to disarmed instead of counting down to nothing; at most 32 project terminals at once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YTT8PjCsRNq19Rc53mSSta (cherry picked from commit d5c47a164d90ad42c62c42d07559c49124470ef6) * fix(cockpit): a closed session stays closed, everywhere; draw terminals correctly; apply agent updates Close, measured on two paired instances before and after (qa/close.mjs, 22 checks): - Closing at one machine left an "exited" tile on the OTHER one, which was saved, came back as a previous session, and was re-run by an update restart. The exit now says it was a close (and from where), and every deck showing the session follows it. - A bulk close brought all but one of the closed sessions back as dead tiles: each close makes the machine announce its sessions, and the first announcement still listed the rest, which adoption read as "started elsewhere". Closed ids are tombstoned until the machine stops listing them. - A remote close was a fire-and-forget notify. Offline, it vanished and the tile was adopted back on reconnect with the agent still running; without the `control` permission it looked like it worked. A new `cockpit:closeSession` answers: offline closes are kept and delivered on reconnect, refusals put the tile back and say why. Older hosts get the old notify. - Update restarts and the restart counter skip tiles whose terminal already ended. - Sessions another machine starts no longer take the selection and the keyboard; "+ New session" on a remote tile opens on that machine; reopening a project only dedupes against tiles on the machine being opened; Enter on a focused Cancel cancels; a failed restart keeps the tile as a saved entry; a restart waits for the old agent to exit and starts a never-written conversation fresh under the same id instead of failing to resume it. Rendering: tiles draw with xterm's DOM renderer again. Under the WebGL addon Claude Code's screen built up stale cells - old status lines stacked down the screen, earlier text showing through where new text had spaces - while the buffer was right: minimize/restore, which only makes xterm redraw every row (measured: no pty resize at all), cleared it every time. WebGL had left the GPU process's cost unchanged when it was measured. Agent updates: Claude Code shows "Update installed · Restart to apply", and its in-session /restart exists only on macOS and Linux (gated on platform), so on Windows the update waited for a manual /exit. The tile header now offers "Apply update": the restart keeps the conversation, and while a turn is running it waits for the turn to end. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YTT8PjCsRNq19Rc53mSSta (cherry picked from commit d0162e01cf381057033255e66b97237a4ecf5c08) * chore(deps): Electron 43.8, drop the WebGL addon, clear the audit gate; add qa:close `npm audit --audit-level=moderate` (the CI gate) failed at HEAD on advisories published since the last release: Electron 43.3 (sandbox inheritance, protocol-handler CORS, preload cache poisoning; fixed in 43.4.1/43.5.0) and, through electron-builder's @electron/get, global-agent 3 -> roarr -> sprintf-js (no fixed sprintf-js exists; global-agent 4 no longer depends on it), pinned by override. Now 0 vulnerabilities. Verified on 43.8: unit tests, qa, qa:close, qa:link, and a packaged win-unpacked build whose embedded terminal opens, streams and closes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YTT8PjCsRNq19Rc53mSSta (cherry picked from commit 340d2bbb06506caef6e9bf34ccf66af532dbb7d6) * perf+fix(renderer): spinners cost a frame stream, not a spinner; machine-scoped project state - Measured (12 sessions, 6 working off screen): any running spinner makes Chromium redraw the window every step, ~6% of a core whether one spinner turns or six. Spinners now tick 4x/s instead of 10x and pause while the window is unfocused (all-frozen measured 6.8% vs 13.1%). - qa/perf.mjs: its own rAF frame counter ran through every process-CPU window (idle read 6.8%, real 0.5%); it now runs only while a JS profile is taken. - Task board / project memory Open went to THIS machine for a remote project; project activity, badges, providers and sidebar project signals are now per machine. - A size change's ConPTY repaint no longer counts as agent activity (it flipped idle sessions to working); background refreshes no longer rebuild the header rename editor mid-typing; find bar follows a language switch; view mode is not reverted by a slow reload; tray overdue count stays this machine's; due dates save on blur/Enter; adding a single-project folder as a location no longer turns it into a scan root; the login terminal is not shadowed by the usage modal's keys. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YTT8PjCsRNq19Rc53mSSta (cherry picked from commit 39354df1770c5a0f100be2d6c906759693da7287) * perf+fix(main): stop blocking the main thread on scans; close remote-write and launch gaps Performance (measured on synthetic stores at the user's scale: 100 projects, ~520k transcript lines, 5,000 Codex rollouts; "block" = longest main-thread stall): - Codex session re-resolution walked all 5,000 rollouts per project every TTL: 18.8 s -> 277 ms for 100 projects (the deck pass's head cache is reused). Per-tile drift stat pass ~230 ms -> 32 ms. - codexUsageScan re-read, deep-copied and rewrote its 4.5 MB index synchronously on every scan: block 84-99 ms -> 6-11 ms (kept in memory, written async only when it changed). - Tail search for the last user message: an O(n^2) 8 MB sync read, 43.6 ms block -> 1 ms cached, 6 ms on a miss (async, incremental, keyed by file size+mtime). - Active time is an incremental tally instead of copy+sort of every timestamp on every scan: warm usage scan 57-90 ms -> 23-30 ms, heap 40 MB -> 21 MB; session meta tick 3-5 ms -> ~1 ms. - `where` for the Codex CLI cached (44-56 ms sync -> 0.1 ms); session file listing async; `projects:list` shares the build already running (git once for N concurrent callers). Fixes: - `cockpit:liveSessionId` (observe permission) could rewrite which conversation any terminal here announces; only this machine's window may record, on a terminal of the asked project/provider. - Notes: versioned `project:saveNote` (old `setNote` kept for older peers, and it bumps the revision) so a note saved on another device is no longer silently overwritten. - Paired machines may write only to projects this machine actually scanned (the allowlist is a string check); pasted images from them are capped per hour; their usage scans run one at a time. - Session counts were the length of a list capped at 5; counted from the store now. - Open in VS Code expanded %VAR% inside the folder name (shell:true) - escaped and verbatim now, verified byte-exact through a real .cmd shim; wt args escape `;`; openFolder opens folders only; `git init` is bounded; the in-progress task is read from the newest 50 task files, not the oldest. - Tray menu and tooltip take their words from the window, in the app's language. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YTT8PjCsRNq19Rc53mSSta (cherry picked from commit 8826a51a4da6f418f2519bdab11ca1769f3ebc10) * perf+fix(renderer): spinners on a tick, not a CSS animation; versioned notes; i18n and a11y - A running CSS animation keeps Chromium producing frames whether or not a step is due. A/B in one app run, 12 sessions with 6 working: CSS spinner at 10 steps/s 16.0% of a core, 4 steps/s 12.3%, 1 step/s 9.0%; a 250 ms JS tick setting a custom property 5.5%, nothing turning 4.6%. The spinner now turns on that tick (paused when unfocused, hidden or reduced-motion); the shipped build measured 3.6% in the same scenario. - Note editor saves against the revision it read; on a conflict both notes are shown to reconcile, never one erased. Max length shown on the box. - Owed remote closes survive an app restart. - Hard-coded English (project menu, checkboxes, chart title) localized; shutdown history dates use the app language; per-project checkbox names, aria-pressed toggles, calendar day labels, keyboard edit of task text. - qa: the update-button scenario retries Enter on the trust prompt; the spinner check verifies the spinner actually turns rather than that a CSS animation exists. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YTT8PjCsRNq19Rc53mSSta (cherry picked from commit 5c45a1b1ee0ff71273260052329f6ae349c37e96) * release: 1.38.0 notes, docs and version Release notes, README highlight and compatibility, state-consistency contract for notes and session close, backlog (note concurrency done), qa:close documented as a supervised harness. Gates on this commit's tree: tsc 0, vitest 1420 passed, check:docs, qa:maintenance, qa:resilience, qa:session-sync, qa:audit (0 violations), qa (0 console/page errors); supervised qa:close (22/22) and qa:link (0 failing checks). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YTT8PjCsRNq19Rc53mSSta --------- Co-authored-by: Si Hyeong Lee <writingdeveloper@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent cb714bc commit 2b215fd

94 files changed

Lines changed: 4238 additions & 705 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ See what needs attention, resume the right conversation, and work on a paired PC
1616

1717
</div>
1818

19-
> **1.37.9** adds confirmed session renames and task conflict protection ([release notes](docs/releases/1.37.9.md)). Use 1.37.9 on both machines for the new write protocol. See [the maintenance review](docs/quality-review-2026-09-08.md) and [session synchronization review](docs/session-sync-review-2026-09-08.md).
19+
> **1.38.0** makes closing a session reliable across paired PCs, fixes stale terminal drawing, adds **Apply update** for Claude Code on Windows, protects notes from being overwritten by another device, and cuts background CPU use ([release notes](docs/releases/1.38.0.md)). Use 1.38.0 on both machines for confirmed remote close and versioned notes; 1.37.9 added confirmed session renames and task conflict protection ([notes](docs/releases/1.37.9.md)). See [the maintenance review](docs/quality-review-2026-09-08.md) and [session synchronization review](docs/session-sync-review-2026-09-08.md).
2020
2121
<div align="center">
2222
<img src="docs/screenshots/projects.png" width="820" alt="DevDeck project overview with repository and session navigation" />
@@ -83,7 +83,7 @@ Both machines must be reachable, for example on the same LAN, through an existin
8383
| Application settings | Belong to the PC whose Settings screen you are using. |
8484
| Terminal screen | The host provides bounded recent output for reconstruction, not an unlimited durable archive. |
8585

86-
**Compatibility:** confirmed titles and task editing need compatible 1.37.9-or-newer builds on both ends. The app checks title-version support and refuses unversioned writes rather than silently treating a viewer-only rename as shared success. Titles of disconnected or ended sessions cannot be remotely committed.
86+
**Compatibility:** confirmed titles and task editing need compatible 1.37.9-or-newer builds on both ends; confirmed remote close and versioned notes need 1.38.0 on both ends (with an older peer they fall back to the previous behavior). The app checks title-version support and refuses unversioned writes rather than silently treating a viewer-only rename as shared success. Titles of disconnected or ended sessions cannot be remotely committed.
8787

8888
Reconnecting reads the host's current title; it must not send an old cached title back. Clearing a custom title is an explicit change to the default project name, not an invitation to restore the old name. See [state ownership and consistency](docs/state-consistency.md) for the implementation contract and remaining gaps.
8989

‎docs/backlog.md‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@ Updated 2026-09-08. The 1.37.9 maintenance work was released on 2026-10-15. Impl
1212
| P2 | Persistent terminal history | Separate screen reconstruction from archive; bounded retention, deletion/export, redaction and visible gaps |
1313
| P2 | Task operation API | Task-ID mutations after revision-checked baseline; explicit same-task conflicts and concurrent create/edit/delete/clear-completed checks |
1414
| P1 | Remaining shared-metadata migrations | Apply the [ownership contract](state-consistency.md) to notes, project flags and Link settings; explicit failure/conflict UI and multi-observer QA |
15-
| P2 | Note concurrency | Notes remain last-write-wins across devices; add revision checks while preserving drafts |
1615
| P2 | Focused module extraction | Terminal lifecycle/rendering boundaries protected by regression tests; no rewrite solely for file length |
1716

17+
## Implemented in 1.38.0
18+
19+
Reliable session close across paired PCs (answered remote close, offline closes delivered on reconnect, tombstones against re-adoption, `closed` exits), DOM terminal rendering, Apply update for Claude Code, versioned notes, Windows process-tree kill, Link pre-auth/notify/event hardening, scan performance and spinner CPU reduction, and the supervised `qa:close` harness. See the [release notes](releases/1.38.0.md).
20+
1821
## Implemented in 1.37.9
1922

2023
Revision-checked task writes, legacy refusal, conflict review with retained attempted change, settings save rollback and inline validation, js-yaml/Vitest advisory fixes, Node24 CI, maintenance regression gates, fail-closed performance preconditions, and current documentation/maintenance configuration. See the candidate review for evidence and limits.

‎docs/quality.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Results and screenshots are written to `qa/shots/maintenance/` and `qa/shots/res
2828

2929
## Manual harnesses
3030

31-
`qa:link` is a supervised two-app harness that may start installed provider CLIs. `npm run qa:perf -- 4 --allow-live-agents` is a manual Windows benchmark and explicitly opts into real-agent startup. Performance QA refuses missing terminals, invisible workload, incomplete streaming/repaint activity and missing CPU samples instead of treating low CPU as success. It has not become a deterministic CI performance gate; that conversion remains a separate task.
31+
`qa:link` is a supervised two-app harness that may start installed provider CLIs. `qa:close` is a supervised two-app harness for the session close lifecycle (remote, offline, refused and bulk close, adoption focus, restart, Apply update); it starts the installed `claude` CLI in a throwaway repository and sends no prompts. `npm run qa:perf -- 4 --allow-live-agents` is a manual Windows benchmark and explicitly opts into real-agent startup. Performance QA refuses missing terminals, invisible workload, incomplete streaming/repaint activity and missing CPU samples instead of treating low CPU as success. It has not become a deterministic CI performance gate; that conversion remains a separate task.
3232

3333
Do not run these against a live development conversation. Do not equate a short loopback test with sleep/resume, VPN changes or multi-day field validation.
3434

‎docs/releases/1.38.0.md‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# DevDeck 1.38.0
2+
3+
## Closing sessions
4+
5+
- A session closed on one PC disappears from every paired PC. It no longer stays behind as an "exited" tile that was saved and reopened as a previous session — or started again by an update restart.
6+
- Closing several sessions at once no longer brings some of them back as dead tiles.
7+
- Closing a session on another PC now waits for that PC's answer. If the PC is offline, the close is delivered when it reconnects (also after restarting DevDeck). If this device may not control that PC's sessions, the tile comes back with an explanation instead of looking closed while the agent keeps running.
8+
- Closing a session on Windows also ends what the agent started in the background (MCP servers, hidden dev servers).
9+
- A renderer crash no longer stops every running agent; the window reloads and picks the sessions back up.
10+
11+
## Terminals
12+
13+
- Terminals draw with the DOM renderer. Under the WebGL renderer, Claude Code's screen could keep stale cells — old status lines stacked down the screen and earlier text showing through gaps — until the window was minimized and restored.
14+
- When Claude Code shows "Update installed · Restart to apply", the session header offers **Apply update**. It restarts the session on the new version and keeps the conversation; during a running turn it waits for the turn to finish. (Claude Code's own `/restart` is not available on Windows.)
15+
- A session started from another PC no longer takes the screen or keyboard focus.
16+
- "+ New session" on a remote session opens on that PC. Opening from the task board or the project memory window opens on the machine those projects belong to.
17+
- Resizing the window no longer marks every idle session as working.
18+
19+
## Data safety
20+
21+
- Notes are saved with a revision. If another device saved the note first, both versions are shown so nothing is overwritten unseen. Older paired builds keep working without the check.
22+
- Usage totals could double when two scans read the same growing transcript at once; they no longer do.
23+
- The project card shows the real number of sessions instead of stopping at 5.
24+
25+
## Performance
26+
27+
- Working-session spinners cost roughly a quarter of the CPU they did (measured 16.0% → 3.6% of one core with 12 sessions, 6 working) and stop while DevDeck is not focused.
28+
- Codex session lookups and usage scans no longer stall the app (100 projects: 18.8 s → 0.3 s; longest stall 84–99 ms → 6–11 ms).
29+
30+
## Security and robustness
31+
32+
- Paired machines: stricter checks for unauthenticated connections, request types and rate limits, event forgery, audit-log flooding, removed machines reconnecting, and remote writes to paths that are not real projects.
33+
- Opening a folder in VS Code no longer expands `%VAR%` in its name; `;` in folder names no longer breaks Windows Terminal tabs.
34+
- Electron 43.7, no open dependency advisories.
35+
36+
Remote close confirmation and versioned notes need 1.38.0 on both PCs to take effect; with an older peer DevDeck falls back to the previous behavior.

‎docs/state-consistency.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ Related paths treated cached session labels as authority when attaching, used ba
1515
| Session title | Main process of the machine running the terminal | Permission + terminal incarnation + expected label revision; persist before publishing/acknowledging | All renderers, including the host, consume the owner snapshot. Drafts are separate. |
1616
| Saved session membership and pins | Each viewer | Local persistence; overlay host-owned titles when reconciling local live sessions | Never send cached titles back merely because a viewer attached or restarted. |
1717
| Project task list | Main process owning the project | Expected task revision; reject conflicts; no silent retry | Display returned snapshot; preserve unsaved attempted change. |
18-
| Project notes/flags | Project-owning machine | Existing last-write-wins behavior | Not yet covered by title/task CAS guarantees. |
18+
| Project notes | Project-owning machine | Expected note revision (`project:saveNote`, 1.38.0); legacy `setNote` still accepted and bumps the revision | On conflict show both the saved note and the attempted one; nothing is overwritten unseen. |
19+
| Project flags (pin/hide) | Project-owning machine | Last-write-wins; remote writes only to scanned projects | Not yet covered by CAS guarantees. |
20+
| Session membership (close) | Machine running the terminal | `cockpit:closeSession` answers ok/offline/denied; closes owed to an offline machine are kept (also across restarts) and delivered on reconnect | Closed ids are tombstoned until the machine stops listing them; an exit carries `closed`/`closedBy` so every deck removes the tile. |
1921
| Settings | Current PC | Await write; rollback or retain invalid draft with explicit feedback | No success state before persistence. Link settings still need broader failure coverage. |
2022
| Terminal input/output | Host terminal | Ordered byte stream; do not turn every keystroke into a storage transaction | Screen replay is bounded and is not an archival transcript. |
2123

‎package-lock.json‎

Lines changed: 22 additions & 96 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "devdeck",
3-
"version": "1.37.9",
3+
"version": "1.38.0",
44
"description": "Project command deck — at-a-glance state + Claude Code, Codex, and Antigravity resume",
55
"main": "dist/main/main.js",
66
"type": "commonjs",
@@ -34,6 +34,7 @@
3434
"qa": "npm run build && node qa/screenshot.mjs",
3535
"qa:audit": "npm run build && node qa/audit.mjs",
3636
"qa:link": "npm run build && node qa/link.mjs",
37+
"qa:close": "npm run build && node qa/close.mjs",
3738
"qa:resilience": "npm run build && node qa/resilience.mjs",
3839
"qa:perf": "npm run build && node qa/perf.mjs",
3940
"qa:maintenance": "npm run build && node qa/maintenance.mjs",
@@ -119,11 +120,11 @@
119120
"@fontsource-variable/geist": "5.3.0",
120121
"@homebridge/node-pty-prebuilt-multiarch": "^0.13.1",
121122
"@xterm/addon-search": "^0.16.0",
122-
"@xterm/addon-webgl": "^0.19.0",
123123
"electron-updater": "^6.8.9"
124124
},
125125
"overrides": {
126126
"undici": "^6.28.0",
127-
"form-data": "^4.0.6"
127+
"form-data": "^4.0.6",
128+
"global-agent": "^4.1.3"
128129
}
129130
}

0 commit comments

Comments
 (0)