Commit Graph
2648 Commits
Author SHA1 Message Date
Codeman maintainer 2d7cd49449 fix: keep foreign discovery from throwing, and correct the adoption docs (release review)
candidatesFrom had moved outside the try when the transports moved to
runForeignProbe, so a classifier failure would have reached the route as a 500.
The module promises it never throws, so the guard is back for local and docker.

Docs:
- The api reference said `blocked` waits are always refused, and listed Ralph as
  refused while two of its file routes were not (they are now).
- The invariants doc and CLAUDE.md gain the rules this review turned up: target
  syntax in names, the ping before list-panes, nothing typed into an adopted pane,
  quoteAdoptLaunch.
- CLAUDE.md no longer says every claude session gets hooks or a stop wait.
- ForeignTmuxSession's mode and workingDir describe the first listed pane, not
  the active one.
- The wiki had no page on adoption: The Dashboard gains an Opened outside Codeman
  section, and the remote and docker pages point at it. docs/docker-cases.md now
  separates an adopted container from an adopted tmux session.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 04:45:29 +01:00
Codeman maintainer 25214fcc5f fix: adopting a shell session no longer types into the foreign pane (release review)
startShell() writes `clear\n` 100 ms after a new mux attach, and for an adopted
session that write reaches someone else's live pane through the wrapper's tmux
client. Measured on an isolated instance: a shell with `echo DRAFT_NOT_SUBMITTED_`
typed at its prompt ran `echo DRAFT_NOT_SUBMITTED_clear` the moment the tab
opened. `shell` is the feature's primary case and also the classifier's answer
for anything it cannot name (vim, psql, a `read` prompt), so every adoption put
those six bytes in front of whatever was reading stdin.

After the fix a byte recorder in the foreign pane saw 0 bytes in the 6 s after
adopting, and a real input afterwards arrived intact. Not in the review: its
recorder covered the trust-dialog path, which is `claude` mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 04:38:05 +01:00
Codeman maintainer aa40cca343 fix: adopted tmux targets, hung servers and the attach chain (release review)
Names tmux reads as target syntax (`@1`, `%1`, `=eq`, a bare `~`) passed the
has-session guard by name and then joined something else in new-session -t:
another session, or a fresh shell on the foreign server. They are refused at
discovery now, and take the existing skipped note.

One stopped tmux server emptied the whole local list and left a stuck client
per scan. Each server now gets a bounded ping with stdout at /dev/null first,
because a tmux client hands dups of its stdout to the server: a stopped server
holds our pipe open after the client is dead, so a watchdog around list-panes
cannot work. The probe runs in its own process group, all three transports
keep the rows read before a failure, and a missed ping becomes a note.

The attach and the probe pass -u, so a POSIX-locale container no longer garbles
the tab or lists a non-ASCII name as underscores. A docker attach gets the same
TERM and COLORTERM an owned container's exec gets.

A missing tmux no longer reads as "session is gone", and the launch line's `$`
now reaches bash unexpanded, so the ssh attach's PATH line is expanded at the
far end, as the probe's already was.

fix-plan/write, fix-plan/read and ralph-prompt/write refuse an adopted session:
its workingDir is the neutral /tmp, so they read and wrote a predictable name
in a folder anyone can write to.

FOREIGN_MAX_SOCKETS is gone: it capped nothing, and a real cap in glob order
would never reach `default` on a box with many `codeman-*` sockets (this one
has 212). Closing an adopted tab re-reads the outside-sessions list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 04:33:42 +01:00
Codeman maintainer 36d8942744 docs(adopt): add the frontend note and scan the module for CLI ids
foreign-sessions.js joins the files the no-id test scans, so the catalog lookup
stays one. The block's rules go where a frontend reader looks, in
src/web/CLAUDE.md, and the architecture note no longer calls the file surfaces
read-only, since File Viewer edit mode writes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 03:38:38 +01:00
Codeman maintainer 1f6f21fd24 docs(adopt): document the routes and the overlay, correct stale comments
The two routes are public under /api/v1, so they get an API reference section.
The overlay gets an architecture note and a CLAUDE.md rule: an adopted session
has session.remote and session.docker undefined and a neutral workingDir, so any
new code that reads workingDir as a workspace must also test isAdopted. That is
the class of bug this round fixed a dozen times.

Records a measured limit: while the view exists an owner's kill-session leaves
the window alive under it until the tab closes.

Comments that still called workingDir the foreign pane's cwd now say what is
true since the wrapper got its neutral folder. Also drops a zh-CN key that
duplicated an existing one, and updates the two structure tests pinned on the
lines this round changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 03:29:35 +01:00
Codeman maintainer d62fa10c01 fix(adopt): zh-CN, registry labels, and the outside-sessions block for non-admins
Every string the block shows now has a zh-CN entry, harvested by a test from the
real module. The row's mode comes from the CLI catalog, not a list of ids that
had already missed omp and copilot. The close dialog's line is static, so the
translator can reach it.

A non-admin's poll answers 403, which read as a failed poll: "Could not reach
the server" under a heading they cannot use, and a request every eight seconds.
That now stops the polling and shows no block.

Wide-scan notes are kept apart from local ones, so the next local poll no longer
wipes "remote X: unreachable" while its rows stay on the list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 03:13:52 +01:00
Codeman maintainer 7de4201955 fix(adopt): an adopted session has no workspace of ours
The wrapper's workingDir is the neutral /tmp, and everything master grew since
1.33.2 that branches on session.remote or session.docker read that as a local
session working in /tmp, because an adopted session keeps its connection on
adopt.remote and adopt.docker.

So uploads wrote into /tmp and closing the tab recursively deleted the shared
folder; the Git panel, its fetch and the fetch scheduler ran git on whatever sat
there; the image watcher, Read My Mind and the Discord bridge used that folder;
last-response paired a transcript by it and could show another conversation's
answer as this one's; a magic link in a container's output registered a file
from this host's /tmp; and guarded input could type into a human's composer.
Each now refuses or answers unsupported, the browser hides the rows that cannot
work, and Session Options shows where the foreign pane was instead of /tmp.

The custom-model route refuses as well, since it restarts the CLI in the pane.
The local version probe is skipped: the local binary says nothing about a
process someone else started, possibly on another host.

Also moves the guard test to port 0, which test-ports-guard now requires.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 03:13:52 +01:00
Codeman maintainer f464a22bc3 fix(adopt): bring the docker and ssh transports up to master
Docker discovery now spawns dockerEngineArgv(), not the shell-quoted
buildDockerBaseArgs(): execFile passed those quotes through, so docker
--context "'default'" failed and every container behind a named context or
remote daemon read as unreachable.

The ssh probe and attach gain withRemoteTmuxPath() like every other remote tmux
call, run in an explicit sh because the guard chain is POSIX and the remote
login shell may be fish, and the probe holds a slot of the remote-ssh limiter
and execs ssh so a timeout reaches it. Notes from a remote scan are no longer
dropped, and every note now opens with its location.

The adopted branch of killSession no longer blocks the event loop on docker or
tmux, kills the in-container view by exact name, and clears the pane-exit and
reconnect state the non-owned remote branch does. An adopted wrapper reports
no injected statusline, since nothing could carry it. Key tables on the foreign
server are left alone on purpose, and the invocation now says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 03:06:47 +01:00
Codeman maintainer a0e21109b2 fix(adopt): key adopted targets by location, not just socket and name
Session 0 in two containers, or one name on this machine and on an ssh host
with the same uid, shared a (socket, name) pair, so opening one returned the
other's tab and its row read "Go to tab". foreignTargetKey() is now the one
key, built from the location, the host identity, the socket and the name, and
both the one-wrapper check and adoptedBy read it.

Both also read live sessions instead of mux records, which only exist once the
first attach starts the pane. An in-flight mark closes the rest of that window:
the route awaits the registries and the layout lock before the session lands in
the map. Docker targets are keyed per engine for the same reason, or a second
daemon's container of one name was never scanned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 03:03:05 +01:00
Codeman maintainer 57b2dcc466 Merge remote-tracking branch 'origin/master' into relprep/pr-510
Brings the 1.33.2 based adoption feature up to master. Conflicts were
resolved by keeping master's intent and re-applying the feature on top:
both sides kept for imports, the script tag and the stylesheet blocks; the
trust-dialog guard keeps master's stopped check plus the adopted refusal;
the Ralph and image watchers keep master's remote branch plus the adopted
skip; the close confirmation keeps master's worktree option plus the
adopted wording.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 02:51:00 +01:00
Codeman maintainer 2aa6016d33 chore: version packages
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 00:04:01 +01:00
Codeman maintainer 4960bee779 chore: one changeset for the release, and no new em-dashes
Consolidates the four contributor changesets, and rewords the em-dashes this release
added in comments and docs. The five in test/settings-save-failure-ui.test.ts stay:
they assert existing toast text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 17:31:48 +01:00
Codeman maintainer eb3b08d58b docs: fix text the release left stale (release review)
The startup hint is no longer typed, a Discord reply is one of the input paths that
set fromUser, edit-history is a state file, POST /api/filesystem/mkdir is in the API
reference, and the store's header says which routes really refuse it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 17:29:09 +01:00
Codeman maintainer 7803d5616a fix: probe bounded before a realpath in clone and New folder (release review, #516)
Clone ran an unbounded realpath over every Docker case workspace, and the mkdir
route inherited the picker resolver's realpathSync, which freezes the whole server
on a mount that stopped answering. The picker's browse route has the same call and
is left as it was.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 17:29:08 +01:00
Codeman maintainer 345c901e81 fix: refuse an unlaunchable model id before quick-start asks to evict a model (#752)
The dedicated route got this order at merge and quick-start kept the old one, so
a user could agree to a swap only to get the 400. It also runs on the pure build
now, so a refused id writes no config dir to clean up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 17:29:08 +01:00
Codeman maintainer e68afdd2ac fix: bound the Discord table conversion input and keep received files off the watcher (release review, #741)
A table costs its widest cell times its rows, so one wide cell in an agent's last
message built a 202 MB string on the event loop although only 8 posts can ever go
out. Only what can be posted is converted now. A file a person sent from Discord
also fired attachment:detected as if the agent had produced it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 17:29:08 +01:00
Codeman maintainer 4276b53a8f fix: keep credential files out of the file routes and Discord uploads (release review)
The Discord bot token was readable through the attachment routes, because
discord.json was not on the sensitive list, and so were webhook.json,
custom-model-hosts.json and the generated custom-model configs. Config backups
were the wider gap: a dated copy of users.json or settings.json matched none of
the name-anchored patterns. All are blocked now.

`codeman notify --file` relied on folder confinement alone, which holds nothing
back when a tab's folder is the home directory, and it would also post a
workspace .env. It now applies the attachment blocklist to the real path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 17:28:52 +01:00
Codeman maintainer 126d967ba3 docs: put the remove() comment back on remove() (#693)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 17:04:46 +01:00
Codeman maintainer f36bea86c7 Merge pull request #693: feat: edit history panel for local claude sessions (files edited, diffs, generated images) 2026-11-04 17:04:46 +01:00
Codeman maintainer 9d04a4fb02 feat: drop the typed first-message hint from the Discord bridge (#741)
Owner decision. Typing the note into a brand-new non-Claude tab spent a model
turn per tab and, for a spawned worker or a scheduled run, landed with the first
real prompt. Claude keeps its hint through the system prompt, and every other CLI
is told nothing. The setting goes with the path it gated: sessionIdle, the fresh
set, isHintableSession and the session:idle wiring. sendInput loses `fromUser`,
which was only ever false for the hint, and the deps test pins that a reply is a
person's own input.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 17:03:14 +01:00
Codeman maintainer 7c8d440928 Merge pull request #741: feat(discord): two-way Discord bridge with a thread per tab 2026-11-04 16:59:16 +01:00
Codeman maintainer 91d157cae4 fix: name the pane directory field paneCwd and apply the review items for #740
`cwd` beside the fixed `workingDir` reads ambiguously on a public API surface, and
`paneCwd` says where the value comes from, like `paneExit`. Also pin the field on
the payload the browser really receives (getSessionStateWithRespawn), and give the
pane-directory text its own architecture-invariants paragraph.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 16:59:16 +01:00
Codeman maintainer 4f50af6a9f Merge pull request #740: feat(terminal): resolve relative path links against the pane's current directory 2026-11-04 16:57:30 +01:00
Codeman maintainerandDDT dc6f8d27e8 fix: keep a Bash( row out of the parenthesised path detector (#735)
A Bash( row holds a command, so its parentheses do not bound one spaced path:
`Bash(/tmp/a.out input.txt)` became a single link to a file that cannot exist and
lost master's `input.txt` link. The path tools keep linking a spaced name whole,
and a hard-wrapped Bash( row gets no spaced lookup either. Test rows from #734.

Co-Authored-By: DDT <141296842+Hugo-DDT@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 16:57:10 +01:00
Codeman maintainer 2526fdb0d8 style: format the DSH_HOME warning
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 16:55:58 +01:00
Codeman maintainer 76e9f364f9 Merge pull request #735: fix(links): preserve wrapped configuration filenames 2026-11-04 16:55:44 +01:00
Codeman maintainer 8c317d84c8 fix: apply the review items for #755 at merge
Say that settings.yaml is shared only until the session's first settings write
(dsh saves by rename, which replaces the link), warn when a session's own DSH_HOME
does not answer and the server's home is linked instead, test
guardSessionConfigDirHome, and document the linkFromRealHome exception and what a
custom-model dsh session does not carry over.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 16:55:44 +01:00
Codeman maintainer 4c29aedbc5 Merge pull request #755: feat(custom-model): DeepSeek Harness injection through a per-session patch 2026-11-04 16:54:32 +01:00
Codeman maintainer 6232dffe03 fix: apply the review items for #752 and #757 at merge
The launch-id check is pure, so it now runs before the context warning and the
swap confirmation: nobody agrees to evict a model only to get the 400. Also the
search-box and in-use comments, the model-plain-colon wording, and the wiki's
order of the picker's marks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 16:53:26 +01:00
Codeman maintainer f7dfe091c5 Merge pull request #757: feat(custom-model): mark the model a session is using in the endpoint model picker 2026-11-04 16:51:56 +01:00
Codeman maintainer 3bcb55ce96 Merge pull request #752: fix(custom-model): context limits, Codex model override, context discovery and picker filter 2026-11-04 16:51:56 +01:00
Codeman maintainer 7b8e295647 fix: retire the OpenCode Console breach notice
A new install has no seen list, so every new OpenCode user was still warned about
an October incident that is long over. The mechanism stays for the next notice;
an empty list is now a valid file, and the validators keep running over whatever
it holds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 16:51:56 +01:00
Codeman maintainer 0dbfc8dc2d fix: apply the review items for #744 at merge
Classify OpenSSL and LibreSSL's SSL_connect errors as unreachable again (`_` is a
word character, so \bssl\b missed them and the user read the temp directory as the
reason), point a cases-folder refusal at Clone Repo instead of Create New, and move
the mechanism and history out of CLAUDE.md into architecture-invariants.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 16:50:44 +01:00
Codeman maintainer d9d6fad698 fix: cap the first tab on every pass, before a selection widens it (#748)
selectSession() toggles .active at once while _syncTabStripCorner() only runs in
the debounced render, so a cap set after measuring left a newly selected first
tab wider than the room and dropped the first row below the toolbar until then.
max-width changes nothing on a narrower tab, so the cap is simply always on, and
the clear-before-measure and finish-the-animation loops go. The no-usable-room
test uses 8 tabs: at 40 the scroll fallback fires first and it passed at any
threshold.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 16:48:57 +01:00
Codeman maintainer 823013226d fix: record the settings baseline when a tunnel enable is abandoned (#736)
Both _abandonTunnelEnable() returns come after saveAppSettingsToStorage() wrote
the new values to this device but before _unsavedSettingsBaseline was set, so
after cancelling the no-password prompt the next Save sent no
showPlanUsageLimits flip and did not reload for a gesture-control change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 16:48:25 +01:00
Codeman maintainer 5e7ec1c522 Merge pull request #744: feat(cases): clone a repository into a custom folder 2026-11-04 16:47:48 +01:00
Codeman maintainer 1efe1d40c3 Merge pull request #745: feat(picker): New folder button in the path picker, on for Link Existing 2026-11-04 16:47:48 +01:00
Codeman maintainer 9d6f84f446 Merge pull request #748: fix(tabs): keep the L-shaped strip whichever tab is selected 2026-11-04 16:47:48 +01:00
Codeman maintainer 58f7dbc7ad Merge pull request #736: fix(settings): stop a failed settings write from saying it was saved 2026-11-04 16:47:48 +01:00
Codeman maintainer 7ed660ff11 Merge pull request #737: fix(mobile): show a press on the phone toolbar and accessory keys 2026-11-04 16:47:48 +01:00
RandalixandClaude Opus 5.5 109fd98038 fix(edit-history): colons in file:// paths, yield on forward reads, no store for unsupported
Follow-up from an own review pass of de436576:
- PATH_TOKEN_RE branch 1 stops at the next `file://` instead of at `:`,
  so `file:///tmp/plot-12:30.png` is found again; still linear.
- Forward reads yield between files like "Load older"; a store removed
  meanwhile ends the read and its printed paths are never probed.
- POST /edits/older and GET /edits/:entryId no longer load a store for
  remote, Docker or non-claude sessions (shared unsupportedList()).
- Time budgets widened (1 s per line, 3 s per chunk; ~3 s / 6.6 s before
  the fix) so the throttled suite does not flake.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 09:50:21 +01:00
RandalixandClaude Opus 5.5 de43657676 fix(edit-history): bounded image probe, linear path scan (review of #693)
- probeLocalPaths: every agent-printed candidate goes through
  probePathKind() first (no pastCap, bulk scan), sequentially; only a path
  that answered 'file' gets realpath + stat. A path on a mount that stopped
  answering no longer hangs the sync, a queued sync or remove().
- PATH_TOKEN_RE: branch 4 starts only at a token boundary (`:` excluded so
  URLs stay ignored), branch 1 stops at `:`. Linear in the line length
  (30,000 `#` took ~3 s before).
- Load older yields to the event loop between the files it parses.
- GET /edits answers unsupported without loading a store.
- CLAUDE.md paragraph trimmed to the rules; probe and regex rules added to
  architecture-invariants#edit-history-panel.

Tests: stall test through the real sync and probe with a stat that never
settles; time-budget tests with 30,000-character inputs and a 139-result
base64 chunk; route test asserts no store load for unsupported sessions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 09:46:23 +01:00
Devvyn 4aeab0d9ce fix(custom-model): keep the in-use picker honest about down endpoints and exited panes
Review round on PR #757. The route turns EVERY upstream failure into
isLlamaSwap: false, so the old line silently promised an unreachable
endpoint was merely mute, and the live-session check missed a null pid
(the PTY exited, the same test keep-warm applies).

- Require pid !== null in _getCustomModelInUseBySession, pinned by a
  'process exited' case in the marks-nothing table
- Reword #customModelPickUnknown to claim only that nothing was
  reported, true for a down endpoint too; drop the 'stays quiet for a
  failed probe' promise from changeset, docs and comments
- architecture-invariants: add the In-use tier, both pin fences
  (customModel not displayModel; stopped/null-pid/paneExit never
  count) and the unknown-line rule
- Mirror the wording through i18n (key + zh-CN), index.html fixture,
  styles.css comment, and the run-menu tests
2026-11-02 19:15:37 +08:00
Devvyn e1c792f491 fix(custom-model): ignore a remembered model id the endpoint no longer lists
Round-four review, split fix 1: a stale 'last used' id kept the promotion truthy, so no row was tagged and the 'Nothing is marked' line stayed hidden. Derive it from promoted && models.includes(promoted).
2026-11-02 19:05:22 +08:00
Devvyn 1f0823c582 fix(custom-model): show which model a session is using when an endpoint cannot report it
Only llama-swap answers what is loaded, so behind a gateway such as Bifrost
nothing was marked and the picker fell back to discovery order apart from the
one CLI that had launched there before. A live session whose own injected
endpoint is that host now promotes its model as "In use by a session", under
its own tag and below llama-swap's "Currently loaded" answer, reading only
what Codeman injected rather than terminal text, and never a stopped session.
When genuinely nothing is known the picker says so instead of leaving an
unmarked wall, and stays quiet for an endpoint that merely failed to answer.
2026-11-02 19:05:22 +08:00
Devvyn 5c8a557390 refactor(custom-model): one exported MAX_DISCOVERED_CONTEXT_TOKENS for routes and schema
Round-four review smaller item: the discovery comment claimed a shared ceiling while schemas.ts restated the literal twice; the constant now lives in schemas.ts (routes already import from it, no cycle) and both bounds reference it.
2026-11-02 17:04:12 +08:00
Devvyn 2cc5f3f9e6 fix(custom-model): keep the picker search box unfocused on touch devices
Round-four review item 4: focusing the search input on every picker open raises the OS keyboard over the list on phones; guard it the way readmymind-ui.js does.
2026-11-02 17:04:01 +08:00
Devvyn 181e419644 fix(custom-model): check the forced launch id before any config dir write
Round-four review item 3: the swap route wrote the new config.toml into the very dir a live codex session uses as CODEX_HOME before checking the pattern, and its refusal then deleted that dir. buildCustomModelInjection is pure, so the check runs on its result and a refused id never touches the filesystem. Pinned by a two-apply codex route test.
2026-11-02 17:04:01 +08:00
Devvyn 82d251d52b docs(custom-model): align the docs and comments with the core behaviour 2026-11-02 16:54:57 +08:00
Devvyn fa2b81484b fix(custom-model): guard, document and test the session-supplied dsh home
Second review round on PR #755: skip non-absolute DSH_HOME candidates so a relative home can never mkdir under the server cwd; probe a session-supplied home with probePathKind in both async routes before the synchronous apply; test the session env-override lookup (seventh arg) incl. the self-link fall-through and the relative-home skip; reject '.' and '..' as realHomeFallback and enforce linkFromRealHomeDirs subset in the schema; fix the stale apply JSDoc and the two doc lines lagging the verified dsh run; state the shared/isolated split plainly.
2026-11-02 15:50:45 +08:00