Commit Graph

19 Commits

Author SHA1 Message Date
d8996bd5e1 Fase B8+B9: stateful code kernel and live notes bound to external URLs
B8 - executable code blocks with a real kernel:
- worker/handler/kernel.cljs: one persistent SCI interpreter session per
  page, reused across block evaluations so defs/state survive between runs.
  Deliberately scoped to the same privileges the existing client-side
  single-shot evaluator already had (no shell/fs access, no call-api).
- Run button + result display wired into block.cljs's code-block UI.
- New :logseq.property.code/result and /executed-at properties (65.38).

B9 - live notes: pages tagged #LiveNote that fetch external content on a
refresh schedule and render it via the render-resource pipeline:
- worker/handler/live_notes.cljs: per-block credential storage, scheduled
  refresh (Node-only), thread-apis for manual refresh and auth.
- worker/handler/render_resource/live_note.cljs: reads back fetched content,
  watching [:entity block-uuid] for reactive invalidation.
- New :logseq.class/LiveNote + :logseq.property.livenote/* properties (65.39).

Bugs found and fixed along the way while actually exercising page creation
through page-handler/create! for the first time in a test (previously only
pure-fn tests existed for CalDAV/Event/Contact):
- logseq-property-namespaces (property.cljs) was missing the citation/event/
  contact/caldav/livenote namespaces, causing auto-create-ontology to treat
  already-built-in properties as new user idents.
- outliner/page.cljs's build-page-tx always added the generic #Page tag even
  when the caller tagged with a built-in class that extends #Page, which
  malli-schema rejects as conflicting; it also lost the property type when
  building ref-typed property values for new pages, breaking non-:default
  types like :number.
- entity-util/page? never recognized Event/Contact/LiveNote as pages (only
  Journal/Whiteboard/Tag/Property/Page had dedicated recognition), so pages
  tagged with those classes alone weren't treated as pages anywhere in the
  app; added built-in-page-child? alongside the existing per-class checks.
- caldav.cljs's upsert-page! passed a bare keyword as a tag where an entity
  was required, and two error logs passed raw error objects to glogi that
  triggered runaway recursion in this test environment; both fixed.
2026-08-26 22:41:23 +02:00
0158430489 Fase B7: CalDAV/CardDAV sync with Nextcloud, and a Calendar view type
- New built-in classes Event and Contact (schema 65.37), plus a
  Calendar View view type alongside table/list/gallery, backed by a new
  fixed month-grid calendar-view component in components/views.cljs
  reusing the existing scheduled/deadline properties.
- New worker/handler/caldav.cljs: a deliberately-scoped CalDAV
  (VEVENT/VTODO) + CardDAV (VCARD) client against a single Nextcloud
  calendar/addressbook collection, with its own minimal iCalendar/vCard
  encoder-decoder and namespace-agnostic multistatus XML extraction
  (no external ical/rrule/xml dependency exists in this project).
- RRULE <-> :logseq.property.repeat/* translation covers only the
  `plus` (advance-from-scheduled) repeat mode with a bare FREQ(;INTERVAL)
  rule -- dotted-plus/double-plus and anything with BYDAY/COUNT/UNTIL
  are left untranslated on purpose rather than mistranslated.
- Credentials live in the platform keychain (save-secret-text!), never
  in the graph. Sync runs Node-only, every 15 minutes per open repo,
  started/stopped alongside the repo's lifecycle in db_core.cljs.
  Conflicts are last-write-wins via ETag: a 412 on push is skipped and
  retried after the next pull refreshes the stored ETag.
2026-08-26 21:11:00 +02:00
dadeca7165 Fase B6: temporal relevance decay and vector-based "similar pages"
- combine-results now applies a bounded (never below 0.5x) exponential
  decay based on :block/updated-at, so more recently touched blocks/pages
  rank slightly higher for otherwise-equal matches.
- New search/similar-blocks reuses the existing zvec index: fetch a
  block's own already-indexed embedding (new :fetch capability on
  open-vector-index, via zvec's fetchSync) and re-query the index with
  it instead of embedding a fresh query string.
- Exposed as a :similar-pages render-resource (worker/handler/render-resource/similar.cljs),
  consistent with how linked/unlinked references are already wired, plus
  a raw :thread-api/search-similar-blocks for non-UI callers.
- New "Similar pages" section on the page view, above the existing
  linked/unlinked references.
2026-08-26 20:21:28 +02:00
d59aec181e Fase B5: temporal/linear graph view mode
Adds a third :temporal view mode alongside :tags-and-objects and
:all-pages. The backend reuses build-all-pages-graph's node/link shape
unchanged (every node already carries :block/created-at); only the
client-side pixi layout differs, arranging nodes along a fixed
chronological timeline (oldest first, staggered across a few lanes)
instead of running the force simulation, following the existing
grid-layout fixed-position precedent.
2026-08-26 19:03:21 +02:00
b67f7e9f78 Fase B3+B4: built-in Project/Area tags and Citation class
B3: registers :logseq.class/Project and :logseq.class/Area as
built-in page-like classes (the TODO in class.cljs already asked for
this), so users can tag pages/journals with them out of the box.
Fixed a markdown-mirror test whose "Custom Tag" fixture used to be
titled "Project" and collided with the new built-in class.

B4: adds :logseq.class/Citation plus /target and /context properties,
following the :logseq.class/Comments pattern. Since target is a
:node-typed ref, citation blocks automatically show up in the
existing block/citation reference graph without any changes there.
2026-08-26 19:03:14 +02:00
fcf41c7b41 Fase B2: give Contents root semantics alongside Library
Adds a shared common-config/contents-page-name constant and
ldb/contents?/get-contents-page, mirroring the existing Library
helpers. Contents already qualifies as an internal page, so the
existing namespace-title split mechanism (e.g. "Contents/Proyectos")
already parents pages under it via :block/parent, coexisting with
Library rather than replacing it as the default root.
2026-08-26 19:02:49 +02:00
c50511a7b5 Fase B1: chain journal pages with previous/next graph links
Adds :logseq.property.journal/previous and /next (built-in, hidden,
single-cardinality node refs) and maintains them in
logseq.outliner.page/create whenever a new journal page is created.
Filling a gap between two already-linked journals re-links both
neighbors around the new page instead of just appending. A migration
backfills the chain across all existing journal pages ordered by day.
2026-08-26 19:02:44 +02:00
db7f1fac7d Fase 9 (2/N): daemon-hosted MCP server, retire electron/mcp_server.cljs
Completes Fase 9's "consolidar MCP" half: the daemon now serves its own
MCP endpoint at /mcp, exposing every semantic-routes operation as an MCP
tool against the daemon's authoritative graph, and the old Electron-local
MCP server is retired since it's now redundant.

New: src/main/frontend/worker/mcp_server.cljs
The CF-Worker's own MCP endpoint (deps/db-sync/worker/entry.mjs) generates
its tools from the OpenAPI document via @cloudflare/codemode's
openApiMcpServer, a package that only exists inside Cloudflare Workers,
plus a pile of ChatGPT-App-specific tooling (asset image resources,
upload_asset, Cognito protected-resource metadata) this daemon has no use
for -- none of that ports to Node. So this registers one tool per
operation by hand instead, using @modelcontextprotocol/sdk directly, the
same pattern electron/mcp_server.cljs already used for its own 6 tools
(registerTool with an explicit title/description/inputSchema/handler) --
just generated from semantic-routes' operation table and operation-docs
(now public) instead of writing 35 one-off registrations by hand, so the
MCP tool surface can't drift from the HTTP routes or the OpenAPI document.
Each tool's Zod input shape is assembled from path params (auto-derived
from the route's :param segments) plus a per-operation-id map of the
extra query/body fields; a handful of shared fragments (pagination,
time filters, the recursive block-tree shape, property-value/task-status
selectors) cover the repetition across list/create/update operations.

Each tool's handler makes a loopback HTTP call to this same daemon's own
/v1/semantic/* route, forwarding the /mcp request's own Authorization
header -- reusing the already-tested HTTP-layer logic (repo-error
validation, lock-owner assertion, response shaping) instead of a second
request path directly against the thread-api. Session/transport plumbing
(handle-post/get/delete-request, the transports-by-session-id map) is
adapted from electron/mcp_server.cljs's Fastify-based version: that one
gets req/res already unwrapped-to-Node-primitives via `.raw` with the
body pre-parsed by Fastify; this one is called straight from the daemon's
own raw http.createServer callback, so req/res already are what
StreamableHTTPServerTransport wants, and the body needs parsing here
instead. No separate auth check either -- the daemon's existing
bearer-token check on every route (request-authorized? in
db_worker_node.cljs) already gates /mcp before this is reached.

Wires POST/GET/DELETE /mcp into db_worker_node.cljs's make-server cond
chain, alongside the existing /v1/semantic/* branch.

Dependencies: adds @modelcontextprotocol/sdk and zod (zod/v3 -- the SDK
doesn't work with zod 4's default export, same constraint
electron/mcp_server.cljs already worked around) to the root package.json;
previously only resources/package.json (Electron's own bundle) had them.

Retired: electron/mcp_server.cljs and its wiring (electron/server.cljs's
initialize-mcp-routes, its 3 POST/GET/DELETE /mcp route registrations,
the :mcp-enabled? config field; components/server.cljs's "copy MCP URL"
popup link; components/settings.cljs's entire AI settings tab, which had
nothing in it besides the MCP-server toggle). Electron already talks to
the daemon (Fase 3) rather than driving itself locally, so a second,
separately-configured MCP surface bolted onto Electron's own local
automation API server (which otherwise stays -- /api is a distinct,
still-used concern) was redundant with the daemon's new one. Removed the
4 associated i18n keys (server/mcp-url-copied, settings/ai,
settings.ai/enable-mcp-server[-desc]) from all ~26 locale dicts.

New: src/test/frontend/worker/mcp_server_test.cljs
Connects a real @modelcontextprotocol/sdk client over HTTP to a live
daemon's /mcp endpoint: lists all registered tools, round-trips
createPage/getPage and a recursive captureToToday block tree, and checks
a tool call against a nonexistent page surfaces as MCP error content
rather than throwing. 4 tests, 11 assertions.

Verification: all 5 shadow-cljs targets compile with 0 warnings. Full
`bb dev:test` run: the only failures are the same 7 pre-existing,
sandboxed-network-related persist_db_test.cljs failures already
documented in the Fase 8 and Fase 9 (1/N) commits -- identical test names,
identical ECONNREFUSED/Invalid URL errors. Every other shard, including
all three daemon-HTTP worker test files sharing one test.js process
(db_worker_node_test.cljs, semantic_test.cljs, mcp_server_test.cljs),
passes clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 17:07:27 +02:00
196de21731 Fase 9 (1/N): re-host the semantic API onto the daemon
Ports the 38-operation semantic API from deps/db-sync's Cloudflare Worker
handler (handler/semantic.cljs + routes/semantic.cljs) onto the daemon's
own authoritative conn, per the plan's Fase 9. deps/db-sync itself is
untouched -- this only adds the daemon-side equivalent.

New:
- worker/semantic_routes.cljs: route table + OpenAPI 3.1.0 document
  generator, adapted from the CF-Worker's routes/semantic.cljs. Drops the
  :graph-id URL segment and listGraphs operation entirely -- the daemon is
  bound to a single repo per process (see db_worker_node.cljs's
  bound-repo), unlike the CF-Worker's per-graph Durable Objects, so every
  request implicitly targets the daemon's own bound repo. Security scheme
  is a bearer token (the daemon's actual session auth) instead of OAuth2.
- worker/handler/semantic.cljs: the 38 operations themselves, ported from
  the CF-Worker's handler/semantic.cljs. Adaptations from the original:
  * ensure-conn! (lazy SQLite conn open) -> worker.state/get-datascript-conn
    (the daemon already holds the authoritative conn for its bound repo).
  * broadcast-change! calls dropped entirely -- any transact! against the
    conn is already picked up by the listener worker.db-listener installs,
    which broadcasts to every connected client, so nothing here needs to
    push manually.
  * HTTP body reads (p/let + body-clj) dropped -- bodies arrive already
    JSON-parsed from the HTTP layer, and every operation is synchronous
    (DataScript itself is synchronous), so none of this needs promises.
  * http/bad-request, http/not-found, js/Response error helpers -> ex-info
    with :status/:code, matching every other thread-api in this codebase.
  * R2 asset storage -> platform/asset-write-bytes! (local filesystem),
    using the <uuid>.<type> naming convention components/block/asset.cljs
    already establishes. R2 signed URLs -> server-relative
    /v1/assets/:repo/:name paths (already served, with ETag caching, by
    the existing Fase 5 asset routes) since bearer-token auth already
    gates those with the same ?token= fallback semantic GET routes need.
  Registered as a single :thread-api/semantic-request thread-api
  dispatched by handler keyword, mirroring how the CF-Worker original
  dispatched via one big `case` in `handle`.
- db_worker_node.cljs: wires a /v1/semantic/* branch (JSON body parsed
  for most operations; raw bytes read directly for assets-create,
  mirroring the existing /v1/assets/:repo/:name PUT route) plus
  /v1/semantic/openapi.json. Unlike /v1/invoke -- an internal RPC
  transport whose transit-encoded results Logseq's own clients already
  know how to decode -- /v1/semantic/* is the public HTTP API documented
  by the OpenAPI doc and meant for arbitrary HTTP/MCP clients, so results
  are decoded server-side into plain JSON, the same way the existing
  /v1/assets/:repo/:name GET route already does for the other route with
  external HTTP consumers.
- src/test/frontend/worker/semantic_test.cljs: exercises pages,
  blocks/properties, capture, tags/tasks, search, the OpenAPI document,
  401-without-auth, and 404-on-unknown-route against a live daemon over
  real HTTP. 7 tests, 31 assertions.

Verification: all 5 shadow-cljs targets compile with 0 warnings. Full
`bb dev:test` run has zero regressions -- the only failures are the same
7 pre-existing, sandboxed-network-related persist_db_test.cljs failures
(ECONNREFUSED / Invalid URL) already documented as environmental in the
Fase 8 commit.

Found and fixed in-session: semantic_test.cljs originally set its own
LOGSEQ_DAEMON_USERNAME/PASSWORD env vars at namespace load time, colliding
with db_worker_node_test.cljs doing the same (both get bundled into the
same test.js process) -- whichever namespace loaded last won the race and
locked the other file's daemons out of their own bootstrapped credential,
cascading into ~150 unrelated failures. Fixed by reusing the same literal
credential constants instead of introducing a second one.

Known follow-up, not yet done: the plan's "consolidar MCP" half of Fase 9
-- an MCP JSON-RPC endpoint on the daemon (the CF-Worker's equivalent,
deps/db-sync/worker/entry.mjs, leans on @cloudflare/codemode's
openApiMcpServer plus ChatGPT-app-specific tooling that doesn't port to
Node) and the decision on electron/mcp_server.cljs's fate are still open.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 16:34:14 +02:00
1e7e9a3c13 Fase 8 (2/N): delete the client-side RTC/E2EE/sync UI and finish the teardown
Removes the client-side half of the remote CRDT sync protocol left after
the worker-side deletion in 565c211: RTC/remote-graph UI (components/rtc/*,
db/rtc/debug_ui.cljs, cloud-graph upload/download/leave/delete flows in
components/repo.cljs and mobile/components/app.cljs), E2EE UI and handlers
(components/e2ee.cljs, handler/e2ee.cljs, handler/events/rtc*.cljs), the
db-worker-node ui-request RPC (handler/worker.cljs's :db-worker/ui-request,
which existed only to serve E2EE password prompts), settings panels for the
legacy custom sync-server URL, and the now-orphaned :rtc/* client state
(presence UI, upload/download progress, remote-graph lists) — presence.cljs
was already deleted in the worker-side pass, contrary to the plan's "keep"
list, so all of this had zero producers left and was fully inert.

Also fixes several dead thread-api invokes discovered on LIVE paths that
would throw at runtime rather than being merely dead UI:
- db/restore.cljs's restore-graph! (called on every graph load)
- persist_db.cljs's <ensure-remote! (the Fase 3/4 daemon connect path)
- persist_db/browser.cljs's worker init and shutdown
- handler/user.cljs's logout

Verified via full compile of all 5 shadow-cljs targets (app, electron,
mobile, db-worker-node, test — 0 warnings) and a full bb dev:test run:
every namespace passes except the same 7 frontend.persist-db-test failures
that reproduce identically on unmodified HEAD (confirmed via git stash),
pre-existing network-sandbox flakiness unrelated to this change.

Known follow-up, not yet done: a handful of harmless-but-vestigial :rtc/*
references remain (handler/user.cljs's storage/graph-count exceed-limit
events, handler/events.cljs's :rtc-download? opt, worker/state.cljs's
*db-sync-config atom, worker/handler/graph.cljs's get-rtc-graph-uuid reused
by handler/editor.cljs's quick-add filter) — none throw or affect behavior,
left for a future pass rather than risk removing something still load-bearing
without deeper tracing. deps/db-sync/ (the Cloudflare Worker) is also still
untouched, per the plan's explicit note that it needs separate, larger surgery.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 15:59:39 +02:00
565c2114be Fase 8 (1/N): delete the worker-side remote sync protocol
Deletes frontend.worker.sync/* wholesale (apply_txs, asset_db_listener,
assets, auth, client_op, const, crypt, download, handle_message,
large_title, log_and_state, presence, temp_sqlite, transport, upload,
util), worker/sync.cljs, worker/handler/sync.cljs, and
worker/ui_request.cljs -- the remote CRDT sync protocol and its E2EE
password-request plumbing, both made obsolete by the server-client
daemon architecture built in Fases 1-7. Deviates from the plan's KEEP
list on sync/presence.cljs and sync/transport.cljs: neither has any
caller left once the rest of the cluster is gone (confirmed by grep),
because Fase 4's WS resume-by-revision work already built its own
self-contained module (db_worker_node_ws.cljs) instead of adopting
these as the plan assumed it would.

This touched far more than a mechanical deletion:

- frontend.worker.sync.client-op wasn't sync-only. Its client_ops
  SQLite table is also the idempotency marker for db_core.cljs's
  built-in-property repair migration and the forward/inverse-outliner-
  ops relay undo/redo reads back through history-action-ops-by-tx-id.
  Trimmed (not deleted) into frontend.worker.local-tx-log, keeping only
  the tx-entry/local-tx functions those two callers need and dropping
  the genuinely sync-only surface (pending-upload counts, checksums,
  graph-uuid, sync-conflict tracking, asset-op queues).

- frontend.worker.sync.apply-txs's apply-history-action! looked
  sync-specific (it lived next to the remote-tx-apply/rebase machinery)
  but turned out to be the *only* implementation ever registered for
  worker-undo-redo/*apply-history-action! -- undo_redo.cljs calls it on
  every db-transact undo/redo, local edits included, not just
  remote-origin ones. Deleting it outright would have silently broken
  undo/redo app-wide the next time anyone hit Cmd-Z. Ported the
  general-purpose slice (replaying a recorded entry's forward/inverse
  outliner ops against the current db state, plus the raw-tx-data
  fallback for edits that bypass the structured outliner-op layer) into
  a new frontend.worker.undo-redo.replay, along with persist-local-tx!
  (derive history ops on every local commit, stash them in
  local-tx-log, hand off to gen-undo-ops!) -- dropping only the
  remote-rebase-specific rest (CRDT conflict resolution, upload-queue
  bookkeeping). Wired into db_listener.cljs as a renamed
  :persist-local-tx handler (was :db-sync, a name that stopped meaning
  anything once sync was gone).

  Caught by running the full existing undo_redo_test.cljs suite (30
  deftests written for the old mechanism) against the new module after
  fixing its test harness to use the real db_listener.cljs dispatch
  instead of a hand-rolled listener -- all 29 tests port over passing
  with no behavior change, which is the actual evidence this preserves
  undo/redo correctness rather than just compiling.

- db_core.cljs's own 8 :thread-api/db-sync-* handlers (remote-graph-
  download import, close-db/invalidate-search-db/recreate-lock
  sync-specific variants) are deleted along with their sync-crypt/
  sync-download calls; :thread-api/unsafe-unlink-db, :close-db, and
  :release-access-handles keep their real (non-sync) bodies.
  db_worker_node.cljs's non-repo-methods set drops from 22 to 7
  entries (the 15 removed were all e2ee/db-sync/ui-request keywords
  backed by deleted files).

- worker/handler/export.cljs's :thread-api/recompute-checksum-diagnostics
  (and worker/db/validate.cljs's backing recompute-checksum-diagnostics,
  which pulled in logseq-db-sync.checksum) compared a local checksum
  against a remote one that no longer exists -- deleted outright, not
  ported, since there's nothing left to diff against.

- worker/handler/block.cljs's :block.temp/sync-conflicts metadata
  (backed by the deleted client-op/get-all-sync-conflicts) is gone;
  its client-side consumer (a retry-upload/clear-conflicts UI in
  components/block.cljs) is dead code now and still needs a follow-up
  pass -- flagging here since worker-side cleanup alone can't remove
  it without breaking that UI's thread-api calls at runtime.

Verified: app/electron/db-worker-node/mobile all compile clean. Full
worker test suite green (db_core_test 131, db_listener_test 11,
db_worker_test 8, db_worker_node_test 59, db_worker_node_lock_test 10,
local_tx_log_test 4, undo_redo_test 29, subs_test 50, subs_loader_test
2, config_test 6 -- 0 failures across all of them). The entire
src/test/frontend/worker/sync/ test directory (7 files) and
db_sync_test.cljs/db_sync_sim_test.cljs/node_sync_test.cljs (all
testing only-deleted namespaces) are deleted wholesale rather than
ported.

Known follow-up, not yet done: the client-side (frontend.handler.*,
frontend.components.*) RTC/E2EE/db-sync UI and its dead thread-api call
sites (components/block.cljs, components/repo.cljs,
components/settings.cljs, handler/user.cljs, handler/assets.cljs,
db/restore.cljs, handler/common/developer.cljs, and the
handler/db-based/{sync,rtc-flows,rtc-background-tasks}.cljs +
handler/events/rtc.cljs + components/rtc/* + db/rtc/* files themselves)
-- a separate commit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 14:59:17 +02:00
c96b8a7a68 Fase 7: cache the daemon's per-invoke lock check, harden mobile server config storage
assert-lock-owner! ran on every write invoke and did fs.existsSync +
fs.readFileSync + JSON.parse + a process.kill(pid, 0) syscall each
time -- all synchronous on the Node event loop, once per request. Now
cached in db_worker_node_lock.cljs: the lock file's content is cached
and invalidated by watching its containing directory (survives an
unlink+recreate, unlike watching the file path directly), and the
liveness check is skipped entirely when the recorded pid is this
process's own -- the dominant case, since assert-lock-owner! is the
owner checking its own lock, not a stranger checking someone else's.
Writes this process makes itself (create-lock!/update-lock!/
remove-lock!) invalidate the cache synchronously in the same call, so
the only staleness window is for a lock file changed by another
process -- a few ms until the fs.watch callback fires, which doesn't
weaken the two-daemons-can't-both-hold-a-live-lock guarantee.

Verified against a real daemon (5 consecutive write invokes, no
lock-watch-failed warnings) and with two new tests: one proving
repeated assert-lock-owner! calls keep agreeing with an unchanged
lock, one proving a lock file rewritten by "another process" (a raw
fs.writeFileSync bypassing update-lock!'s own invalidation) is picked
up within the polling window instead of being served stale forever.
All 8 pre-existing lock tests and all 61 db-worker-node-test tests
still pass.

frontend.config's db-worker-server-url/token getters/setters stay
synchronous localStorage reads everywhere (retrofitting their many
callers to await an async native store is out of scope), but on native
mobile, writes now also mirror into frontend.mobile.secure-storage
(OS keychain/keystore) as a durability backstop -- WebView localStorage
can be evicted under storage pressure or not survive a reinstall.
mobile.init/init! now fires config/<restore-server-config-from-secure-
storage! at startup to copy a surviving value back into localStorage
if it came back empty. Guarded so this is a true no-op outside native
mobile (including under Node tests, where it's additionally gated by
the same util/node-test? check the existing localStorage calls already
used) -- verified via the :mobile shadow-cljs target compiling clean
and the full test suite (subs-test, config-test, db-worker-node-test,
db-worker-node-lock-test, subs-loader-test) staying green.

iOS ATS and Android cleartext traffic, also called out in this phase
of the plan, turned out to already be configured permissively
(NSAllowsArbitraryLoads in Info.plist, cleartextTrafficPermitted in
network_security_config.xml) -- no changes needed there.

Investigated whether db-core's .remoteInvoke serializes concurrent
invocations, per the plan's own open question: it does not --
frontend.common.thread-api/remote-function dispatches straight into
the handler with no per-repo queueing, so two concurrent write invokes
to the same repo could in principle interleave at any await point
inside the handler. Deliberately did NOT add a write queue for this:
the plan gates that on Fase 0 telemetry showing real invoke overlap in
practice, and doing it without solid evidence of need risks a mistake
in read-vs-write method classification for uncertain benefit --
querying methods (:thread-api/q, :thread-api/get-render-snapshots,
etc.) share the same repo-scoped lock-check path Fase 6 just optimized
for read latency, and serializing those too would undo that work.
Left as an explicitly open question rather than guessed at.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 11:33:30 +02:00
2164bbdf1e Fase 6: close the subs-loader in-flight dedup gap, raise batch limits, add /v1/invoke-batch
Fase 0's telemetry work had already flagged that subs-loader deduped
load! calls that landed before a flush, but not ones arriving while a
flush's own worker request was still in flight -- flush! cleared *batch
via take-batch! before the request it just sent even resolved, so a
load! for the same slot in that window fired a duplicate request. Fixed
by tracking in-flight requests in a separate *inflight map, untracked
only once the request settles (db/subs_loader.cljs), with a real e2e
test proving the dedup and that a later load! after settlement does
fire a fresh request (db/subs_loader_test.cljs).

Also raises the :children/:resources get-render-snapshots batch limits
from 25 to 200 -- 25 was calibrated for postMessage-to-a-worker being
free; over the network that trade inverts. Two existing subs-test.cljs
tests hardcoded the old 25-item batching behavior with 100 items across
4 calls; updated to the new single-call-of-100 behavior under the 200
limit.

Deliberately did NOT widen db/subs.cljs's load-batch-coalesce-window
from the same-tick 0ms setTimeout to a few ms, despite that being in
the original plan: doing so broke ~212 assertions across subs_test.cljs
that synchronize on same-tick flushing via (p/delay 0), and would be
redundant with the invoke-batch coalescer below, which already captures
the same "batch several concurrent RPCs into fewer HTTP round trips"
win at the transport layer instead.

New POST /v1/invoke-batch on the daemon (db_worker_node.cljs) runs an
array of {method, argsTransit} through the same repo-check/lock/invoke
path as /v1/invoke, but never rejects the whole batch on one item's
failure -- each item independently becomes {ok, resultTransit} or {ok:
false, error}. Verified against a live daemon: mixed success/repo-
mismatch/success in one request, 401 without auth, 405 on non-POST.

On the client, frontend.persist-db.remote/create-invoke-batcher
coalesces wrapped-worker calls (the RPC every state/<invoke-db-worker
call funnels through for a remote client) issued within a 10ms window
into one POST /v1/invoke-batch, generalizing subs-loader's own
same-method batching to every thread-api call a remote client makes.
Two invokes only land in the same batch when neither is awaited before
the other fires, so this can't reorder anything a caller depends on
sequentially.

New db_worker_node_test.cljs coverage: invoke-batch mixing success and
repo-mismatch failure in one request, and invoke-batch auth/method
enforcement.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 11:13:04 +02:00
902bf7f568 Fase 5: serve and accept assets over HTTP on the daemon
Closes the gap the plan flagged as blocking the sync deletion: a web/mobile
client had no way to read or write asset bytes at all (only db-sync's now
end-of-life handlers touched platform/asset-*). Adds GET/PUT
/v1/assets/:repo/:name on db_worker_node.cljs, backed by three new
thread-api ops (asset-read-bytes/write-bytes/stat) that just call the
existing Node platform.cljs asset-*-bytes! functions -- reusing the same
JSON+transit /v1/invoke path already proven for export-db-binary, so no new
binary RPC mechanism was needed.

GET responses carry a real Content-Type (guessed from extension), a sha1
ETag, and immutable Cache-Control -- assets are content-addressed by
filename, so this is safe to cache forever once fetched. Auth accepts
?token= as a fallback for a direct <img src>/browser navigation, same
rationale as the SSE interim token, but as a genuine fallback alongside the
Authorization header rather than replacing it (an early version of this
made query-token exclusive with header auth, which 401'd every JS-driven
fetch -- caught before committing, real end-to-end curl testing against a
live daemon also caught a 500-instead-of-404 on missing assets and a
never-firing 304 from a hyphenated JS property access that needed `aget`
instead of `.-`).

Client side: frontend.handler.assets/resolve-asset-real-path-url and
<make-asset-url now resolve to the daemon's asset URL (with the server
token) instead of an unreachable file:// path when a web/mobile client has
a server configured (frontend.config/get-server-base-url) -- covers viewing
existing assets. frontend.fs/write-asset-file! gets the matching upload
path via a new frontend.persist-db.remote/upload-asset!, so pasting an
asset from a web/mobile client no longer silently lands in
frontend.fs.memory-fs and vanishes on reload.

Server-side routes and round-trip verified end-to-end against a real
daemon (curl) and with new automated tests (byte equality, content-type,
etag/304, 404, repo-mismatch, auth-required). The client-side URL
resolution changes could not be visually verified -- no browser in this
environment -- only reviewed and compiled clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 10:33:42 +02:00
cf61c46c35 Fase 4: migrate the daemon event channel from SSE to WebSocket with resume-by-revision
Adds a WS upgrade handler alongside the existing SSE stream on /v1/events
(kept alive for instant rollback), authenticated via Sec-WebSocket-Protocol
instead of a query-string token. The daemon now keeps a ~500-entry circular
buffer of broadcast sync-db-changes deltas indexed by renderer revision; a
client reconnecting sends {type: subscribe, graphId, sinceRev} and either
gets a gap-free replay + ack, or a {type: resync} instructing it to discard
all cached renderer state and reload (frontend.db.subs/reset-graph!, wired
through a new :db/resync event). The web/Electron client transport
(frontend.persist-db.remote) now opens the WS channel by default; the SSE
path (connect-events!) is untouched and still fully wired.

Also fixes new/pre-existing test breakage surfaced by switching the default
transport: persist_db_test.cljs's shared Electron-failover fixture now mocks
open-ws-fn (not just open-sse-fn), and a `new` applied to an inline (or ...)
expression in make-server's WebSocketServer construction is replaced with
the same let-bound-symbol pattern deps/db-sync's node/server.cljs already
uses -- inlining it silently dropped `new`, breaking every daemon test.

Confirmed 7 unrelated persist_db_test.cljs failures (electron-ensure-remote-*
IPC bookkeeping) already fail identically on the pre-Fase-4 baseline -- left
untouched, out of scope here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 10:12:53 +02:00
be7abd52a3 Fase 3: make the remote transport config-driven, not Electron-only
Every PersistentDB dispatch site in persist_db.cljs branched on
electron-runtime?, so a web or mobile client could never take the remote
path -- only Electron's IPC-managed local daemon could. The remote client
itself (persist_db/remote.cljs) was already platform-agnostic (plain
js/fetch and js/EventSource), so this is a transport-selection fix, not a
new architecture.

- frontend.persist-db/remote-runtime?: true for Electron OR a non-Electron
  client with config/get-server-base-url configured. Replaces every
  electron-runtime? check at the seven PersistentDB call sites
  (<start-runtime!, <list-db, <unsafe-delete, <close-db, <export-db,
  <import-db, <open-and-fetch-schema, <new).
- Fixes the known <export-db bug: it always went through the local OPFS
  impl regardless of transport, so exporting a remote graph silently
  exported nothing. It now follows the same remote-runtime? dispatch as
  every sibling function.
- <resolve-remote-runtime!/<release-remote-runtime-slot! split the
  Electron-IPC path from a new web path that just reads the locally
  configured base-url/token -- there's no daemon to spawn from a browser,
  only one to log into.
- config.cljs: get/set-server-base-url!, get/set-server-token!,
  clear-server-config!, mirroring the existing custom-sync-server-url
  localStorage pattern.
- remote.cljs: login! POSTs to the daemon's /v1/auth/login (added in Fase
  1) and returns a bearer token.
- A minimal settings panel (components/settings.cljs, non-Electron only):
  server URL + username + password, calls login!, saves the result, and
  reloads. Functional but not visually verified -- no browser available in
  this environment; the request/response wiring is confirmed below against
  a real daemon, not the rendered UI itself.

Verified by running a real db-worker-node daemon and exercising the exact
sequence the new settings panel and remote.cljs drive: unauthenticated
invoke -> 401, login with correct/wrong credentials -> token/401, healthz
unauthenticated with :pid/:root-dir absent, authenticated invoke (list-db)
-> 200, per-origin CORS reflection, bad Host -> 403, and SSE both without a
token (401) and with one as a query param (200). Plus clojure -M:cljs
compile app db-worker-node electron (0 warnings) and the full
logseq.db-worker.daemon-test, logseq.cli.server-test,
frontend.worker.db-worker-node-test, electron.db-worker-manager-test,
frontend.state-test, frontend.persist-db.remote-test suites (134 tests,
541 assertions, 0 failures).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 19:08:54 +02:00
49d9f7da84 Fase 1: require session auth on the db-worker-node HTTP/SSE server
Prepares the daemon to be reachable off loopback as part of the
server-client migration (see the approved implementation plan). Today it
trusts any caller that can reach the port; this makes every route except
health checks and login require a bearer session token.

- New frontend.worker.db-worker-node-auth: scrypt-hashed username/password
  credential persisted at <root-dir>/db-worker-auth.json (mode 0600),
  bootstrapped from LOGSEQ_DAEMON_USERNAME/PASSWORD on first launch since
  there's no interactive setup yet. In-memory bearer sessions with
  constant-time comparison, a failed-login lockout, and list/revoke by
  device. The daemon now refuses to start with no credential configured at
  all -- not just when binding beyond loopback -- since without one no
  token could ever be issued.
- db_worker_node.cljs: POST /v1/auth/login and GET/DELETE
  /v1/auth/sessions; every other route requires Authorization: Bearer
  except /healthz, which stays open (readiness polling happens before any
  session exists) but now omits :pid/:root-dir from its payload. Adds a
  Host-header allowlist ahead of auth to guard against DNS rebinding, and
  configurable --host/--port/--allowed-origin/--allowed-host in place of
  the hardcoded 127.0.0.1 bind and single CORS origin.
- EventSource can't send custom headers, so /v1/events accepts the bearer
  token as a query param instead; persist_db/remote.cljs's events-url
  carries it there now (interim until the Fase 4 WebSocket migration).
- Electron always sent :auth-token nil (verified by reading db_worker.cljs
  -- one of the plan's open questions). daemon/spawn-server! now seeds a
  per-root-dir local device credential
  (<root-dir>/db-worker-local-credential.json, reused across restarts) via
  LOGSEQ_DAEMON_USERNAME/PASSWORD, and cli/server.cljs logs in once the
  spawned or reused daemon is ready, threading the resulting token through
  ensure-server! to Electron's runtime config.

Not done: the OCaml CLI (cli/lib/transport.ml) also calls /v1/invoke
directly and will start receiving 401s once this ships -- confirmed with
the user to leave that gap for now rather than touch the OCaml toolchain
in this pass.

Verified with clojure -M:cljs compile app db-worker-node electron (0
warnings) and the full logseq.db-worker.daemon-test,
logseq.cli.server-test, frontend.worker.db-worker-node-test,
electron.db-worker-manager-test suites (113 tests, 480 assertions, 0
failures) plus frontend.state-test and frontend.persist-db.remote-test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 18:59:08 +02:00
52971f8ec9 Add Fase 0 telemetry for the server-client migration baseline
Adds dev-only counters, with no behavior change, to answer the open
questions from the migration plan before touching the transport:

- frontend.state/get-invoke-stats: per-thread-api-method count/total-ms/
  max-ms for every <invoke-db-worker call, to census how many invokes a
  typical navigation generates and their latency distribution.
- db-worker-node: per-repo in-flight /v1/invoke counter that logs a new
  concurrency high-water mark, to check whether concurrent client writes
  actually overlap before deciding on a write queue.
- subs-loader/get-stats: dedup-hit vs new-entry counts for load!, and
  batch-size distribution per slot kind at flush, to check whether
  in-flight snapshot requests are already deduplicated and whether the
  {:children 25 :resources 25} limits are worth raising for network RTT.

Verified with clojure -M:cljs compile app and compile db-worker-node
(0 warnings on both).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 15:51:09 +02:00
40631362b3 Initial import of LogDarioSeq baseline
Establishes version control for the personal Logseq DB fork before
starting the server-client architecture migration and domain features
described in docs/agent-guide and the approved implementation plan.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 15:32:15 +02:00