Skip to content

Local-first, dids, wasm + OPFS, flutter, iroh, dht - #1148

Merged
joepio merged 858 commits into
developfrom
did
Jul 25, 2026
Merged

Local-first, dids, wasm + OPFS, flutter, iroh, dht#1148
joepio merged 858 commits into
developfrom
did

Conversation

@joepio

@joepio joepio commented Mar 3, 2026

Copy link
Copy Markdown
Member

@gitguardian

gitguardian Bot commented Mar 3, 2026

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 1 secret following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secret in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
34682882 Triggered Generic High Entropy Secret 2dd47ba browser/lib/src/pairing.test.ts View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secret safely. Learn here the best practices.
  3. Revoke and rotate this secret.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@joepio joepio self-assigned this Apr 28, 2026
@joepio joepio changed the title Dids & invite refactor Local-first, dids, wasm + OPFS, flutter, iroh, dht Apr 29, 2026
joepio and others added 22 commits July 8, 2026 17:32
v5 matches on event.code mapped through its key table, so punctuation
hotkeys must be written as code names: pressing ? gives code "Slash",
which never equals the registered "/". Both shift+/ (shortcuts overlay)
and \ (sidebar toggle) silently stopped matching in the v4 -> v5
upgrade - the same class of breakage as the cmd+ alias already noted in
HotKeyWrapper. Register them as shift+slash / backslash and teach
displayShortcut to render the code names as ? and \.

The shortcuts overlay's hand-maintained list (a third copy of the
bindings, already missing cmd+up) now renders from the central
shortcuts registry, and its duplicate dead '?' registration uses the
registry value too.

Claude-Session: https://claude.ai/code/session_016i33YGkTZTowvaeq33yLs3
…data loss)

Cherry-picked from d5f79da on forms-#875 (the outbox portion only — the
FormBuilder/FieldRow.tsx + forms.spec.ts hunks are dropped, since that work
isn't on this branch). Applied as a 3-way delta so did's RBSR changes to
store.ts are preserved.

Two data-loss races closed:
- Debounce window: a save parked in a UI debounce timer (useValue,
  useDebouncedSave) was invisible to getSyncStatus — "synced" while a write
  was still pending in memory, dropped on reload. Now tracked via
  Store.start/finishScheduledSave so pendingDirtyCount stays > 0.
- drain() snapshot gap: a drain() arriving mid-pass shared the in-flight
  promise, so `await drain()` could resolve while the caller's subject was
  still dirty and un-POSTed (Resource.save reporting 'persisted' for a write
  the server never got). Now chains one follow-up pass with a fresh snapshot.
- Cold drain now loads the resource itself (OPFS-first, then server, gated on
  clientDb ready) instead of stranding an entry no view will ever hydrate.

Verified on this branch: lib + react build and typecheck, lint clean, 43 lib
unit tests pass (incl. the new outbox-race + store tests), and the sync /
offline-persistence / offline-reload / offline-create-then-online /
clientdb-edit-persistence e2e specs all pass (8/8).

Claude-Session: https://claude.ai/code/session_01CRafFfZwmuSmcHJfmGcT7v
…e JDK/AAR wiring

The embedded server now runs on Android: rustls-platform-verifier is
initialized with the app's JVM context before the server thread starts
(reqwest 0.13 always verifies TLS through the Android trust store and
panics otherwise; ndk-context is gone since tao 0.35, so the handles are
bridged from wry's jni 0.21 via with_webview + jni_handle into the
verifier's jni 0.22 API). The verifier's Kotlin component is bundled by
resolving its AAR out of the cargo registry with cargo metadata, keeping
the gradle dependency in lockstep with Cargo.lock.

useCurrentSubject now resolves the root path against the embedded
server's origin instead of the unfetchable tauri://localhost webview
origin.

Claude-Session: https://claude.ai/code/session_0136CyX67ohdcXnhzZPgq6iS
The overlay's input looked like a search field but was readOnly, and the
list overflowed the fixed-height panel (rows rendered past the rounded
corner). Typing now filters by label or key; the list scrolls inside
the panel.

Claude-Session: https://claude.ai/code/session_016i33YGkTZTowvaeq33yLs3
…lope spec

atomic://pair QR/deep-link envelope with onboard (secret-in-QR, v1) and
pair (routing-only) kinds; mDNS/pkarr as routing-only discovery; drive
enrollment recorded in KnownPeer from day one. Resolves serverless-p2p
OQ3, narrows OQ1/OQ4. Flags the agent_secret-in-logcat leak as P0.

Claude-Session: https://claude.ai/code/session_0136CyX67ohdcXnhzZPgq6iS
The Tags popover (frosted translucent + scale(0) pop, no border) and the
More context menu (opaque, dark-mode-only border, plain fade, hardcoded
8px radius) each defined their own look, with CustomPopover as a third
copy. floatingSurface.ts now owns the surface (frosted bg + blur, theme
radius, soft shadow, dark-mode border, prefers-contrast fallback) and a
shared 100ms fade+grow entrance; all three consume it. Menu items are
transparent so the frosted surface shows through, and TagSelectPopover
drops its opaque background override.

Claude-Session: https://claude.ai/code/session_016i33YGkTZTowvaeq33yLs3
Three behavioral gaps vs the (Radix) popovers:

- The entrance transition ran while the menu was still visibility:hidden
  awaiting positioning, so it played unseen and the menu popped in. The
  transition now starts one frame after reveal (data-positioned).
- Positioning overlapped the bottom half of the trigger (a
  triggerHeight/2 offset), making the trigger unclickable for closing.
  The menu now sits above/below the trigger with a small gap.
- Clicking the trigger while open blurred the menu, whose blur handler
  closed it - then the same click's toggle reopened it. A pointerdown-
  on-trigger guard leaves closing to the click's toggle.

Claude-Session: https://claude.ai/code/session_016i33YGkTZTowvaeq33yLs3
Canvas broadcasts the pointer's world position (throttled) via
useResourcePresence().setData and renders remote cursors mapped through
the local viewport; tables announce the selected cell as (row subject,
column property) — identity, not indexes, so peers with different
sorts/filters match cells correctly — and ring remote-selected cells.

Shared PresenceUserTag name pill keeps agent identity consistent with
avatars and document cursors (colorForAgent). FancyTable gains a
presence-agnostic onSelectedCellChange prop. Receivers validate payload
shape: a cleared data field arrives as null, not undefined (found in
live two-session verification, crashed the first cursor overlay).

Also removes dead code in CanvasPage (unused store/navigate/imports)
and documents that same-subject presence announcers compose in
usePresence. Plan + verification notes in planning/presence-views.md.
Account login becomes a third pairing issuer: atomic-saas already holds
the password-encrypted secret (/api/recovery-secret) and enrollments;
add a per-account device directory (node_id, relay hint, origin,
last_seen) so a fresh sign-in restores identity and seeds KnownPeers
without QR or typing. Safe under Principle 1 (directory output is
routing-only; same-agent AUTH still gates); directory client lives
behind the cloud session per FOSS guardrail 3. Passkey+PRF noted as the
one-step-restore v2.

Claude-Session: https://claude.ai/code/session_0136CyX67ohdcXnhzZPgq6iS
…local-only drives

A one-click, fully client-side new-user demo and a Meetings feature that
supersedes ad-hoc follow-sessions.

Demo (chunks/Demo, DemoRoute):
- Spins up a fresh local-only drive per run ("Atomic Demo") with a meta
  onboarding kanban board whose cards ARE the tour steps, a team table,
  scripted teammates that show presence, type into a doc, and chat.
- Guest agent so logged-out visitors get the full experience in one click;
  bulletproof cleanup of prior demo drives via atomic.demoDrives.
- SimulatedTypist forks the doc and flushes deltas through applyIncoming;
  moodboard strokes baked in.

Local-only drives (@tomic/lib):
- Store.registerLocalOnlyDrive / isLocalOnlyDrive / isLocalOnlySubject;
  suppress outbox, websocket SUB/SYNC_VV, and server fetches for them so
  did: subjects resolve OPFS-first. saveLocalOnly() signs at save and
  materializes locally. All guards are inert when no local-only drive is
  registered (size-0 short-circuit), so synced drives are unaffected.
- presence.injectEntry/removeEntry: seam for scripted teammates.

Meetings (#1127) — a front door for follow mode:
- Meeting = ChatRoom+Meeting listed in drive.currentMeetings; live meetings
  surface a "Meet"/"Join" control in the nav bar and context menu (same
  weight as Share/Comments). Join = follow the leader + open the meeting
  chat; opening a live meeting auto-joins. Optional title (defaults to
  "Meeting · {date}", click-to-edit via EditableTitle), participant
  facepile, and a "meeting ended" message that keeps the ended panel.
- Follow-sessions deprecated; chat renders event descriptions verbatim.

Also: responsive sidebar/right-panel widths (narrower under 1728px), and
new lib unit tests (local-only drive, presence). Refs onboarding tour #1233.

Claude-Session: https://claude.ai/code/session_01WFDphAnkgand2pab1SDErz
…-fill

- websockets: WsClient.close() now flips serverConnected(false) and rejects
  in-flight requests synchronously instead of waiting for the WS `close`
  event. Once the transport is already blocked (offline toggle, dropped
  link), Chromium can suppress/delay that event, leaving serverConnected
  stuck true and pending GETs/commits hanging. The event handler still runs
  if it fires — both paths are idempotent. De-flakes the offline-edits-sync
  test (was ~1/5, now 5/5) and is the correct fix for disconnect() generally.

- table-views-filters: "Add view" became a menu (pick Table/Kanban/Calendar);
  the test clicked it expecting an immediate new view. Now opens the menu and
  selects Table.

- tables create-and-fill: the tag picker's Escape drops the grid from Edit to
  Visual mode, so Tab no longer wraps into the next row. Rewrote fillRow to
  self-position by clicking each row's first cell (matching the initial setup)
  and the post-loop edit to click its target cell, instead of relying on the
  dead Tab-wrap / cursor-column assumptions.

Claude-Session: https://claude.ai/code/session_01WFDphAnkgand2pab1SDErz
Two pieces of planning/device-pairing.md:

P0 hygiene: set_default_agent logged the entire config file - agent
secret included - via tracing::warn! on first boot. On Android that
lands in logcat (bug reports, adb logcat history); on servers in log
aggregators. Now logs only the config path plus a pointer to the
pairing/sign-in flow.

P2.5 zero-scan pairing (browser side): helpers/managed/devices.ts
registers this device in the account's directory (Tauri-only - a web
tab has no Iroh node of its own, /iroh-node-id there names the
connected server) and seeds the account's other devices into the same
atomic-peers KnownPeer records the QR flow writes, so they appear in
the Sync page one tap from syncing. Fired once per session from
IdentityReconcileGate after identity convergence; complete no-op
without a managed session (FOSS guardrail: the open server never
phones home). On 409 the per-install device id is rotated and retried
once, so a shared machine that switches accounts gets its own record
instead of hijacking the previous account's.

Control-plane side landed in atomic-saas aab521a.

Checks: lint, typecheck, 77 unit tests, cargo check -p atomic-server,
e2e chromium 102 passed / 1 pre-existing search flake (passes in
isolation) / 6 skipped.

Claude-Session: https://claude.ai/code/session_01Ffyo9voLDLbGZe4cU7us6P
The versioned QR / deep-link payload from planning/device-pairing.md:
one format, two kinds - onboard (routing + agent secret, for a fresh
device) and pair (routing only). Decode is strict: unknown v gets its
own error code ('update the app', never best-effort parsing), a pair
envelope carrying a secret is rejected (a routing-only code must not
smuggle an identity), node DIDs / urls / drive lists are validated,
and encode round-trips through the same validator so we can never
mint a QR this module would refuse to scan.

11 unit tests; full lib suite 156 green.

Claude-Session: https://claude.ai/code/session_01Ffyo9voLDLbGZe4cU7us6P
Device pairing P0/P1 (render side, planning/device-pairing.md): the
Sync page's Node DID row gains 'Pair device', opening a dialog with
two QR codes built on the lib's atomic://pair envelope:

- pair (default, safe): routing only - scanning tells the other device
  where this node lives; same-agent AUTH still gates everything.
- onboard (opt-in): routing + the agent secret, for setting up a fresh
  device. Blurred until an explicit 'Reveal setup code' press, with the
  same sensitivity framing as the copy-secret button; hidden entirely
  when no local agent exists. The secret is rebuilt from agentStorage's
  plaintext fallback record (new getAgentSecretFromIDB) - the
  SubtleCrypto keypair itself is non-extractable.

The peers input also accepts a pasted atomic://pair link (routing
only; an onboard link pasted there is deliberately NOT an identity
import - that belongs to the scan flow).

New dep: uqr (zero-dependency QR-to-SVG renderer). Locale catalogs
carry the extracted dialog strings.

Not yet built: the phone-side scan path + atomic:// deep link
(tauri-plugin-deep-link / scanner spike on a real device).

Checks: typecheck, lint, unit 77 + lib 156, e2e chromium 101 passed /
2 known flakes (ontology, query-drive-filter - both pass in
isolation) / 6 skipped. QR dialog verified live.

Claude-Session: https://claude.ai/code/session_01Ffyo9voLDLbGZe4cU7us6P
…i, roster order

Four fixes from hands-on demo testing:

- Canvas draw crash: drawing onto a canvas whose strokes were seeded in bulk
  via set() (demo/template content) threw "pushContainer is not a function"
  and dropped the stroke. Root cause: loroSetProperty stored an array of
  objects as a LoroList of PLAIN values (list.push), while pushListItem /
  replaceListItems expect container elements. loroSetProperty now reuses
  writeJsonToLoroList (object elements → LoroMap containers); pushListItem is
  hardened to promote a plain-array value to a container, and removeListItem
  likewise. Regression test added.

- Team table: only had a text Role column, which doesn't show off the table.
  Added a tags column (Responsibilities), a relation (Doing task → a checklist
  card), and a checkbox (Completed onboarding), with per-persona values.

- Say hi / meeting ordering: the director wound down (ended the meeting, team
  left) on a 45s timeout even if the user hadn't chatted, so a later "hi"
  landed in a dead room and never ticked the card. The "Say hi" payoff is now
  reactive and memoized (completeSayHi) — fired by the chat listener the moment
  the user posts, and the script waits generously before winding down. It also
  ticks the user's "Completed onboarding" checkbox.

- Roster order: the user's row now sorts to the BOTTOM (below the seeded
  members) — the guest profile and the created row get an explicit createdAt so
  the createdAt-ascending default sort places them last, not first.

Claude-Session: https://claude.ai/code/session_01WFDphAnkgand2pab1SDErz
…r name

- The welcome doc no longer types a redundant "Explore the / Doodle on the /
  Meet the" todo list (the kanban board already IS the tour). Keeps the doc
  from distracting the user before they've opened the onboarding meeting.

- Resource mention nodes (block + inline) are atoms with no text content, so
  copying text that contained them dropped the visible resource name from the
  clipboard. Add `renderText` (→ schema `toText`) so plain-text copy emits the
  resource's title, matching what's on screen.

Claude-Session: https://claude.ai/code/session_01WFDphAnkgand2pab1SDErz
The QR pairing loop closes without an in-app scanner: the QR encodes an
atomic://pair URI, so the system camera scans it and launches the app
via deep link. This wires that path end to end:

Shell (desktop/src/lib.rs): tauri-plugin-deep-link + a VIEW/BROWSABLE
intent filter for scheme 'atomic' (gen/android manifest; desktop scheme
in tauri.conf.json). Links are forwarded to the webview as
'atomic-deep-link' DOM events - no Tauri JS API or capabilities needed,
the frontend stays Tauri-free. Queued Rust-side and flushed on every
finished page load with a delivered-set, merging get_current(), so the
cold-start link (camera scan launches the app) is delivered exactly
once; helpers/deepLinkQueue.ts queues from module scope before React
mounts.

Import flow (components/PairingLinkHandler.tsx, mounted at the root):
'pair' links persist a KnownPeer (shared helpers/knownPeers.ts) and
open the Sync page; 'onboard' links import the secret via the sign-in
primitives (Agent.fromSecret -> setAgent -> saveAgentToIDB), then
navigate to the personal drive. A device already holding a different
agent gets an explicit 'Switch account?' confirmation - never a silent
key replacement; an onboard code for the agent already held degrades to
routing-only. Unknown envelope versions toast 'update this app'.

Verification: typecheck, lint, unit suites green; desktop crate cargo
check clean; receiver paths (pair persist+navigate, unsupported
version, malformed) verified live by dispatching the DOM events;
presence-follow + meetings e2e pass against a fresh HEAD server with
this change. Full-suite runs are currently unreliable for environmental
reasons: the long-lived dev server on 9883 died mid-run, and its
datastore now 500s ('class not found') on some e2e fixtures - the same
specs pass against a fresh --initialize server, so this is data state,
not code. On-device Android verification (deep link + scanner spike,
OQ1) still pending.

Claude-Session: https://claude.ai/code/session_01Ffyo9voLDLbGZe4cU7us6P
Reworks the message row in the Comments panel and ChatRoom:

- Alignment: drop the extra 1rem inline padding so the message text lines up
  with the panel title and the composer; the hover band bleeds subtly into the
  panel padding via a negative inline margin instead.
- Long authors: the name/DID truncates with an ellipsis so it can no longer
  shove the timestamp off the row.
- Timestamps: a new compact formatter — "5 minutes ago" / "yesterday" for the
  last week, "Jul 1" within the year, else a short numeric date — replacing the
  long "7/9/2026 at 8:37:49 AM".
- Actions: the edit/reply/link/copy buttons use the app's subtle IconButton
  instead of the bright-blue filled icon button (the `icon` prop was overriding
  `subtle`), matching the rest of the UI.

Claude-Session: https://claude.ai/code/session_01WFDphAnkgand2pab1SDErz
- Show each message author's avatar (profile image or initial) to the left of
  the message, in both the Comments panel and meeting/ChatRoom.
- Give follow-event lines ("Started the meeting.", trail "Viewing …") the same
  treatment as regular messages: truncate a long author name/DID so it can't
  push the timestamp off the row, and use the compact relative time instead of
  a full clock string. Line them up with the messages too.

Claude-Session: https://claude.ai/code/session_01WFDphAnkgand2pab1SDErz
- Comments / AI / Meet nav buttons now show the theme's main colour (over a
  subtle background) while their sidebar panel is open — the same "current
  item" cue the left sidebar uses. Extracted the shared LabelButton so the
  meeting button matches the others instead of its own pill shape.
- The meeting button is a proper toggle: clicking it while you're in the
  meeting and the panel is open now closes the panel, like the other toggles.
- A refreshed leader keeps their meeting going: activeMeeting is stashed in
  sessionStorage (per tab, per drive) and restored on load. A follower's
  meeting isn't restorable, so instead of a dead "No meeting yet" placeholder
  the follow-session panel just closes; the top-bar Join banner still offers
  any live meeting.

Claude-Session: https://claude.ai/code/session_01WFDphAnkgand2pab1SDErz
- AgentAvatar gains an optional green "online" dot (agent has a live presence
  entry).
- PresenceAvatarMenu (Show profile / Follow / Stop following) now computes
  online status from drive presence, shows the dot, and only offers Follow for
  someone else who's actually online — never yourself or an offline author.
- Use PresenceAvatarMenu wherever an agent avatar appears: chat/comment message
  avatars and the meeting facepile, so the menu is consistent everywhere. (The
  Following badge keeps its own menu — nesting one inside it would conflict.)

Claude-Session: https://claude.ai/code/session_01WFDphAnkgand2pab1SDErz
joepio added 27 commits July 23, 2026 14:19
Ships the "Local cache and session isolation" mechanism from
planning/encryption.md: after sign-out or an agent switch, a session can
no longer read the previous agent's cached private data — without wiping
the cache.

- Split the single per-origin atomic_data.redb into one OPFS database
  per agent (fingerprint filenames; shared plaintext anon DB when signed
  out). Leader lock + RPC channel are scoped per database, and the
  worker restarts on agent change. The in-memory seed only runs on the
  first start of a page load so an identity switch can't copy one
  session's resources into another identity's database.
- Encrypt each agent's database at rest with XChaCha20-Poly1305 at the
  redb StorageBackend layer (new EncryptedBackend: 4 KiB blocks, fresh
  random nonce per write, block-index AAD, key-check header) so
  resources, Loro snapshots, blobs and derived indexes are all
  ciphertext.
- Key hierarchy per the vault plan: random 256-bit DbKey per agent,
  wrapped by HKDF(agent private key) — the agent key wraps, never
  encrypts bulk data. Session copy in IDB is cleared on sign-out; the
  wrapped copy is unwrapped at the next sign-in, so the same agent
  regains their intact cache.
- Migrate the legacy shared database into the active identity's file
  (encrypting on the way) on first start; adopted, never dropped, since
  it can hold local-only data with no server fallback.
- Attempt ghost-leader lock stealing on all browsers (verified working
  on Firefox/Zen, previously assumed Chromium-only), silence election
  errors from superseded workers, and prevent destroyed workers from
  re-attaching to the Store.
- Sync page: disable Disconnect while no working local store exists —
  the server would otherwise be the only data source.

Design, tradeoffs and known gaps: planning/opfs-per-agent-encryption.md
…ew drive"

Creates a drive on the connected node and immediately enrolls it in
Cloud Sync via the existing enableCloudSyncForDrive helper, then opens
it — the portal only ever holds a session cookie, never the agent's
private key, so drive creation has to happen here rather than there.
The post-steal wait reused the 2s election timeout, but leadershipGained
only resolves after the stolen leader's FULL init — wasm import + OPFS
open — which legitimately takes seconds on a cold dev server. Slow
successful steals were misreported as 'reclaiming the lock did not
succeed' even though the DB recovered moments later.

The steal now gets its own 15s budget that ends early on success, on
another tab announcing leadership (-> follower), or on a definite init
failure — in which case the real error (typically the OPFS file handle
still held by a live background tab, which a stolen Web Lock cannot
take) is surfaced instead of the generic message.
An i18n commit accidentally reverted QuickCreateRow.tsx back to its
pre-meeting-feature state, silently dropping the New Meeting button
(and demoting its translation string to obsolete in every locale) —
the drive page rendered New Document/Table/Folder/ChatRoom but no way
to start a meeting. Restore the button and re-activate the string.
Two React warnings on document open:

- "Cannot update a component (BubbleMenu) while rendering...
  (CollaborativeEditor)": the AI-diff comparison sync ran via
  useOnValueChange(cb, deps, true), which invokes the callback directly
  in the render body. That callback dispatches a ProseMirror transaction
  (editor.commands.setComparisonContent), which BubbleMenu subscribes to
  via useEditorState — so a re-render of CollaborativeEditor synchronously
  forced a state update in the already-mounted BubbleMenu mid-render.
  Moved the sync into a plain useEffect keyed on the same dependencies.

- "Can't perform a state update on a component that hasn't mounted yet":
  useEditor's onCreate option relied on tiptap's Editor.mount(), which
  schedules its 'create' event via a genuine setTimeout(0) outside React's
  render/commit cycle. An abandoned render attempt (e.g. a Suspense retry)
  could still have that timeout fire later and call setEditorReady on a
  fiber that never mounted. Replaced onCreate with a useEffect that
  subscribes to editor's 'create' event (or fires immediately if
  editor.isInitialized already), tying the listener's lifetime to a
  committed instance of the component.

Verified interactively (both warnings reproduced and gone after the fix)
and via tests/documents.spec.ts.
Db::apply_commit held its per-subject lock across the entire function,
including the AFTER APPLY COMMIT HANDLERS loop that fires class-extender
after_commit hooks. A plugin's after_commit is allowed to issue its own
follow-up commit to the same subject (atomic_plugin::commit, via the
commit host function in server/src/plugins/wasm.rs) — that re-enters
apply_commit for the same subject while the outer call's guard is still
held, and the task deadlocks waiting on a lock only it could release.

subject_lock.rs's own invariant comment already named the danger
("do not acquire a subject's lock while already holding it") but claimed
nothing could trigger it, since the plugin ABI seemed to have no store
access — stale: the commit() host function has let a plugin self-commit
since #73, well before the subject lock was introduced, and the two were
never reconciled.

Fix: drop the guard right after the write-back transaction and
subscriber notification are done (the read-modify-write it protects),
before running after_commit handlers. before_commit handlers still run
under the lock, since they precede the write.

Root-caused via browser/e2e/tests/plugin.spec.ts:21 hanging on the
plugin's rename commit; the existing race regression tests
(concurrent_commit_and_peer_apply.rs) still pass.
…MIC_REPOPULATE_DEFAULTS

populate::bootstrap() only calls populate_base_models (genesis, drive,
the Commit class, and the other fixed base-model properties/classes)
once, on a store's very first init. A store seeded before a base model
was added never gets it, and ATOMIC_REPOPULATE_DEFAULTS's repopulate
path only re-ran populate_default_store — the JSON ontology imports —
leaving no way to retrofit an already-seeded store short of wiping it.

Both writers upsert (overwrite_existing: true), so calling
populate_base_models here too is idempotent and safe against live data.
…tched versions

Routine, compatible-range updates fixing 6 of the 12 real vulnerabilities
cargo-audit flags against this branch's Cargo.lock (the other 6 —
hickory-proto x2, rustls-webpki x4 — all trace to the already-known,
already-deferred iroh 0.35 transport migration; see
planning/rust-dependency-upgrade-audit.md, not attempted here):

- wasmtime/wasmtime-wasi/wasmtime-wasi-http 45.0.0 -> 45.0.3
  (RUSTSEC-2026-0182, RUSTSEC-2026-0188 — the WASM plugin sandbox)
- quinn-proto 0.11.14 -> 0.11.16 (RUSTSEC-2026-0185)
- crossbeam-epoch 0.9.18 -> 0.9.20 (RUSTSEC-2026-0204)
- plist 1.9.0 -> 1.10.0, pulling quick-xml 0.39.4 -> 0.41.0
  (RUSTSEC-2026-0194, RUSTSEC-2026-0195 — Tauri desktop's plist chain)

Verified: cargo check on atomic-server --features wasm-plugins and on
the desktop (Tauri) crate; atomic_lib (278/278) and atomic-server
(46/46) test suites; the plugin e2e spec (exercises the bumped
wasmtime directly) plus sync/tables specs against a rebuilt server.
…ides

pnpm audit against this branch's actual pnpm-lock.yaml (not the stale,
842-commits-behind develop numbers Dependabot reports) found 48 real
advisories: 1 critical, 17 high, 25 moderate, 5 low. All but one were
transitive — mostly under netlify-cli (dev tooling), the MCP SDK's hono
peer, and vite-plugin-webfont-dl's axios — so a plain `pnpm update`
either couldn't reach them or, when tried broadly, swept in unrelated
major bumps to react/vite/tiptap/loro-crdt that were never part of the
ask. Used pnpm-workspace.yaml's `overrides` instead (package.json's
`pnpm.overrides` key is no longer read by pnpm 10.x) to force each
vulnerable package to its minimum patched version, one at a time,
verifying no unintended major-version drift after each addition:

- an open-ended ">=x" override let pnpm resolve straight past the
  patched version into a newer unrelated major wherever nothing else in
  the graph pinned one — caught @babel/core jumping 7.29.0 -> 8.0.1,
  fast-uri 3 -> 4, http-proxy-middleware 3 -> 4, and linkify-it 5 -> 6
  this way; every override below is now bounded to >=patched <nextMajor.
- brace-expansion needed two scoped overrides (its 2.x and 5.x lines
  coexist) plus a parent-scoped `minimatch>brace-expansion` override,
  since the range-selector alone didn't reach older minimatch@5.x/9.x
  duplicates still resolving the vulnerable 5.0.6.
- cookie is scoped to `cookie@0` only: cookie 1.x is independently
  required elsewhere (@sveltejs/kit, fastify, @netlify/redirects all
  declare ^1.x directly), so an unscoped override would have fought
  that instead of just patching the 0.x line advisories target.

Remaining state: 0 vulnerabilities (was 1 critical, 17 high, 25
moderate, 5 low). One pre-existing peer-only @babel/core@8.0.1 (an
orphaned @rolldown/plugin-babel peer, unconstrained by any direct
dependency) is left as-is — it isn't itself vulnerable and isn't caused
by these overrides.

Verified: pnpm typecheck, pnpm test (one pre-existing agentStorage.test
failure confirmed identical on the baseline lockfile, unrelated), pnpm
lint (failures confined to unrelated in-progress files), @tomic/lib and
data-browser production builds.
…affordance, empty column

- Add per-view-kind icons (table/kanban/calendar) to the view tabs and the
  "+ new view" dropdown, sourced from a new VIEW_KIND_ICONS map.
- Give kanban column headers a subtler darker background and a bolder,
  slightly larger status pill.
- Rework the "Add card" control to reuse SkeletonButton (dashed,
  card-shaped, rounded) instead of a plain hover-highlight button, and
  drive its label from the table's own row name (e.g. "Add Issue") rather
  than a hardcoded "card".
- Hide the "No status" kanban column when it has no cards, keeping it
  visible for the duration of a drag so a card can still be dropped there
  to clear its status.
… color

A new appearance setting. When on, ChromeTheme (a nested ThemeProvider
around the sidebar and navbar) swaps the neutral ramp for tones of the
main color, so no grey ever sits on a colored surface; content and text
stay neutral for readability. bgBody gets a whisper of the same hue.
Off, the theme is byte-identical to before.

The main-color presets move to styling.ts as presetColors, now also the
default tag palette (replacing the neon tagColours).

Includes a stray oxfmt fix in AllProps.tsx.
Drive name and switcher form one segmented control: the name fills the
row and truncates to a single line, the switcher (now a double caret,
always visible) is pinned right. Header inset matches the tree rows.
The splash now follows prefers-color-scheme via CSS, and an inline
script applies the in-app dark mode override (localStorage 'darkMode')
before first paint. Replaces the old end-of-body script that wrote the
OS preference as inline styles, which could not be overridden and
ignored the in-app setting.
Adds a camera toggle to the meeting side panel that starts a fully
peer-to-peer call via Trystero (WebRTC, Nostr signaling): the meeting
subject is hashed into the room id and doubles as the signaling
password, so only participants can join or read the handshake. Media
never touches a server.

The call UI (tile grid, mute/camera/hang-up controls, mic-only and
permission-denied fallbacks) lives in a lazy chunk, so the ~24 kB
gzipped library is only fetched when someone actually starts a call.

Mentioned in #1127
Replace the floating tag pill with a header strip that's part of the same
rounded rectangle as the card list below it: solid background in the tag's
own color, white title text, and the card list gets a faint wash of that
same hue over the usual grey instead of a flat neutral background.

useTagData is now exported from Tag.tsx so KanbanColumn can read the raw
color/text without rendering the pill itself.
…r More menu

The navbar meeting banner now only shows while a meeting is live (Join
pill / open-chat pill); the idle "Meet" start button is gone. The More
menu loses its Start meeting and New draft quick-actions — creating
meetings and drafts belongs to the New page — along with the meeting
plumbing in ActionContext that only they used.

E2e follows the new UX: the top-bar test asserts the affordances are
absent, the two-session meetings tests start via New Meeting → Start
meeting, and the drafts spec is a fixme until the New page offers
drafts. Locale catalogs regenerated (wuchale --clean) after the string
removals.
The PWA manifest allows one theme_color (white); these per-scheme metas
override it so installed-app titlebars and mobile browser UI follow
dark mode.
…vers

The origin serving the app is usually an atomic-server node worth
listing on /sync, but the managed deployment serves the SPA from a
shared app origin that is not a node — listing it would offer a
'Switch' that points the store at something that can't answer. Ask
/server first (isAtomicServer) and register only real nodes; a
non-answer also removes entries older builds added blindly.

Rescued from stash@{0} (2026-07-23); its Kanban/locale hunks were
already landed elsewhere.
…tually works

Tantivy only scopes a JSON-field query to a path when the clause is
prefixed with the field's own name (`propvals.<path>:<value>`). The
`filters` param's documented syntax is bare `<property-uri>:<value>`,
which tantivy silently fails to constrain (no parse error, no match) —
so `filters=isA:File`, used by the File picker's `parents`+`isA` query,
never actually found anything.

Rewrite each AND/OR-joined clause to add the `propvals.` prefix before
parsing, so the documented unqualified syntax works without any client
change. Added an integration test that uploads a file and confirms it's
found via the exact query FilePickerDialog issues.
…set component

@tiptap/suggestion's plugin view runs its update handler async: it
assigns a shared `props` before awaiting `items()`, and only calls
`onStart` (which is what creates our `component`) after that await
resolves. Its `destroy()` teardown (fired on editor unmount) only
checks that `props` was ever set, not that `onStart` ran — so if the
editor is torn down in that window, `onUpdate`/`onKeyDown`/`onExit` can
fire on a `component` that was never created, throwing "can't access
property 'destroy', component is undefined".

Guard all three handlers. This bug predates recent commits but is much
easier to hit now that editor mount/create timing changed (Suspense
retry handling).
…t drops

The interactive New-Resource form (useNewForm.ts) mints a client-only
`_new:<random>` subject via store.createSubject() and relies on
Resource.new staying true until an explicit save derives the real
did:ad: subject (store.newResource() mints that DID up front instead
and never has this problem).

Resource.subscribeLocalUpdates only gated dirty-tracking on `.new`,
assuming the subject already carries its real DID. If anything reset
`.new` to false first (e.g. applyToStore merging in an unrelated fetch
response) before the resource completed its genesis save, the next
local edit reached the outbox's plain incremental-commit path with the
subject still literally `_new:...`. The server rejects that (it was
never a complete resource), and onTerminalDrop's best-effort refetch
could itself reset `.new` again — re-arming the same failure on the
next edit, producing a repeating "Dropped stuck commit" error.

Skip dirty-tracking for `_new:` subjects regardless of `.new`, and skip
the terminal-drop refetch for `_new:`/`_local:` subjects (nothing to
realign with on the server). Added a regression test that reproduces
the loop condition directly and confirms it fails without the fix.
…ort keys, query ids

Collection queries no longer decode a Loro CRDT snapshot per member: they
read the materialized Tree::Resources row and attach the stored snapshot
bytes raw as loroUpdate (wire-identical, zero decodes). Subjects-only
queries skip bodies entirely; rows missing (endpoints/externals) fall back
to the full path. 1000-member query: 62ms -> ~6ms (-90%), non-sudo ~7ms.

- hierarchy::RightsCache: per-request memo threaded through check_rights,
  so a listing resolves shared drive/parent ancestry once, not per member
- QueryMembers keys (members_index_v6): 16-byte blake3 query id prefix
  instead of the full serialized filter, plus order-preserving typed sort
  keys (null < bool < f64-keyed numbers < case-folded strings). Fixes
  numeric sort order (#287) and string-prefix ordering; QUERY_UPDATE
  subscriptions now route by id
- Watched filters routed by (drive, property): commits only evaluate
  filters referencing the changed property (create -12%, edit -14%)
- AND-filter index builds pick the most selective constraint via
  scan-capped cardinality estimates and verify all constraints against
  rows (also fixes unverified single non-equality filters)
- Behavior: sorted collections now include members lacking the sort
  property (sorted first; previously dropped by accident), and
  QueryResult::resources stays empty when include_nested is false

planning/index-performance.md rewritten as the architecture doc
(Loro authoritative, rows for reads, indexes for candidates,
intersection planner) with paired benchmark results.
…ments

The write-path gains claimed in the previous commit (create -12%, edit -14%)
did not reproduce in a clean paired f235561 vs 3578d08 run — they came
from a Criterion baseline recorded under concurrent machine load. Clean
paired numbers: write path is flat-to-slightly-negative on a fresh store
(create +10%, edit +6% Rust-level; ~noise over HTTP).

The verified wins:
- 1000-member query: 51.8ms -> 5.9ms Rust (-88.5%), ~104ms -> ~30ms over
  a single HTTP round trip (-70%); ~5x from NextGraph's comparison query,
  down from ~20-25x
- Filter routing pays off exactly where it was built to: with accumulated
  watched filters. New paired scaling measurement
  (lib/tests/watched_filter_scaling.rs, ignored-by-default): per-create
  cost before grew linearly with bystander filters (14.5ms at 0 ->
  52.6ms at 10k, +263%); after it is flat (~15-17ms) — 3.2x faster at
  10k filters. Fresh-store benchmarks can't see this win.
@joepio
joepio merged commit 2ae326f into develop Jul 25, 2026
1 of 2 checks passed
@joepio
joepio deleted the did branch July 25, 2026 16:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Consider switching ureq for something async (reqwest, hyper) Add metrics / Prometheus support

2 participants