v0.9.1
Part of the OpenKnowledge changelog.
Patch Changes
Agent-write content-divergence warnings now carry the converged document inline so an agent recovers without a second read.
structuredContent.contentDivergencegainscurrentState({kind:"inline", content}, or{kind:"truncated", byteLength, hint}over a 50 KB soft cap) plus a coarsedivergenceType. The Site A gate is extracted to a shared predicate and extended toversion({ action: "rollback" }). Divergence is now rate-measurable via theok.agent_write.gate_fired_total/ok.agent_write.content_divergence_totalcounters (bounded{handler}label). Adds a production-Electronagent-patch-divergence-probesmoke test as permanent regression coverage.fix(open-knowledge): edit_document / edit_frontmatter / rollback surface disk-persistence failures (PRD-6832)
PRD-6838 made
write_documentawait its disk flush and return a storage error instead of a false "success" when the write fails to reach disk. The other debounced-store MCP write handlers still returned success the instant the CRDT transaction committed — so a crash inside the persistence debounce window, or a swallowed disk failure (ENOSPC / EACCES / read-only FS), silently lost the write while the agent was told it succeeded.edit_document(agent-patch),edit_frontmatter(frontmatter-patch), andversionrollback now reuse the same awaited-flush + failure-surfacing pattern: the handler force-flushes the L1 disk store and, on failure, responds with the matching storage problem type (507storage-full/ 500storage-readonly/ 500storage-error) rather than a success. The CRDT copy is still retained in memory.create,delete,ingest, andrenameare unaffected: they write synchronously (no debounced-store window) and were already durable.fix(cli): exit immediately instead of hanging ~5s on short commands like
ok psandok stopcreateFileLoggerruns in the CLI'spreActionhook on every command and scheduled a deferred log-dir prune viasetTimeout(..., 5000)that was never.unref()'d. A referenced timer keeps Node's event loop alive, so short commands finished their work in well under a second but the process could not exit until the 5s timer fired, so everyok ps,ok stop,ok status, etc. paid a flat ~5s tail. The timer is now unref'd: short commands exit as soon as their work is done, while the prune still runs on long-lived processes (ok start,ok mcp) where log accumulation actually needs capping. Per-file size remains bounded inline byrotateIfNeededon every logger creation, so log hygiene is unaffected.feat(ok): make config self-describing and starter packs cleaner for novices
Configuration is now legible end to end. Every config field carries a description that flows into the published per-scope JSON schemas, so editors show inline field help and autocomplete from the
$schemacomment in.ok/config.yml. Config errors (scope violations, agent-not-settable, mixed-scope writes) now name the exact file to edit and explain the scope in plain language instead of citing internal codes.ok initno longer scaffolds a deadfolders:block or a Picomatch cheatsheet, and the schema-reference path it prints is correct. The docs site configuration reference and quickstart gain a scope column, the telemetry keys, a tiered env table, and the three-layer model.Starter packs are lighter and clearer. Template bodies are now bare headings (no instructional prose polluting new docs or
log.md), folder descriptions are one-liners, and the "frontmatter sweep" jargon is gone. The workflow guidance that used to live in template bodies now ships as a per-pack project skill:ok seed(and the in-app Initialize dialog) install anopen-knowledge-pack-<id>skill next to the platformopen-knowledgeskill for every editor set up in the project (.claude,.cursor,.agents). Install is unified server-side inapplySeed, so the CLI, the desktop IPC path, and the HTTP path behave identically. Pack skills are now composed into the built CLI and desktop artifacts, not just resolved from source.Add a "Create New Project…" item to the desktop File menu. It opens the create-new-project dialog and works in both the editor window and the Project Navigator.
feat(desktop): dev sessions auto-reclaim a stale foreign server (dev-only)
When a desktop dev session (
electron-vite dev/bun run dev—!app.isPackaged) opens a project whose contentDir already has a live Open Knowledge server it did not spawn — a leftover from a prior packaged-app run, aok startCLI, or another instance — it previously attached to that server. Devs then ran their editor against a stale server + core build instead of their working tree, with no signal anything was wrong.The dev attach path now terminates that foreign server and spawns a fresh own-build server in its place (reusing the same SIGTERM → grace → SIGKILL ladder as the user-initiated restart, then falling through the existing
runClean+ spawn path). It is gated strictly to dev: packaged builds still attach to a live server, which is the intended shared-server behavior. A server this session spawned itself is never reclaimed (pid guard), and if termination fails (e.g. EPERM, cross-user server) the session falls back to attaching rather than leaving the project window-less.Because no user action initiated the teardown, the freshly-spawned window surfaces an act-then-inform notice (new
ok:server-reclaimedbridge event) naming the side effect: connected agents (Claude Code, Codex, Cursor) just lost their Open Knowledge MCP connection and should restart the agent — or toggle its Open Knowledge MCP server off and on — to reconnect.No effect on published/packaged behavior.
Drop the redundant hover tooltip from the editor toolbar's labeled actions. The "Open with AI" and "Share" buttons already show their name as visible text, so the tooltip that repeated that same text on hover was noise. Icon-only toolbar controls (the sync-status cloud, settings, help) keep their tooltips, since the tooltip is the only place their name appears.
Encourage agents to pass a
summaryon every content write. The project skill (packages/server/assets/skills/project/SKILL.md) now tells agents to supply the one-linesummaryarg onwrite_document/edit_document— previously the field was only described as an optional parameter in the tool schema, so agents rarely sent one and document timelines showed writes without a human-readable change-note. The §Writing section gains a dedicated rule and the TL;DR Writes line gains a pointer to it. Thesummaryplumbing itself is unchanged; this only adds the guidance that drives adoption.fix(server): preserve PATH and ELECTRON_RUN_AS_NODE for the git credential helper so packaged-app sync can authenticate
In the packaged desktop app, sync to a private/HTTPS repo failed: the popover showed a
Yjs was already importedwarning and an ElectronUnable to find helper appFATAL, thencould not read Username for 'https://github.com': Device not configured.The desktop server runs as Electron-as-Node and sets
localOpCliArgsto[electronBinary, cli.mjs], so git's credential helper re-invokes the Electron binary directly (it bypasses theok.shwrapper that setsELECTRON_RUN_AS_NODE=1). ButcreateGitInstanceset the git child environment via simple-git's.env(obj), which replacesprocess.envrather than merging — so the inheritedELECTRON_RUN_AS_NODE=1was dropped. The Electron binary then booted as a GUI app, couldn't find its helper-app bundle, and crashed before returning credentials.buildGitEnv()now preservesELECTRON_RUN_AS_NODE(so the helper runs as Node, not a GUI app) andPATH(so git resolves its subprocesses and a bare-command helper like!open-knowledge auth git-credentialis found), alongside the existingLANG/LC_ALL=C.Fix the sync popover's "Set identity" button forcing a re-authentication. The auth modal now probes sign-in status on open: an already-signed-in user is taken straight to the git Name/Email fields (pre-filled from their GitHub profile) instead of through the GitHub device flow. It only falls back to sign-in when the user is genuinely not authenticated, and then still lands on the identity fields so the git identity actually gets set.
Fix
write_document({ position: "replace" }): the call now performs an atomic overwrite at the CRDT layer. Previously when an agent's payload shared substrings with the existing content, only the differing characters were written (a DMP-incremental merge), and concurrent human typing could combine with the result into a hybrid document. The case now routes through the same atomic primitiveversion({ action: "rollback" })already uses, which deletes the prior bytes wholesale before inserting the new payload.append,prepend, andedit_documentfind/replace are unchanged — they keep the item-preserving incremental primitive. (edit_documentroutes through an internalpatchposition so a surgical find/replace produces a minimal CRDT delta rather than a whole-document overwrite, which keeps a concurrent human's edits outside the matched span intact.)PRD-6667.
fix(server): folder delete and duplicate conflict pre-checks enumerate descendant docs from disk, not the lagging file index
Folder
delete-pathandduplicate-pathderived their affected-docs set from the in-memory file index, which the chokidar watcher populates asynchronously. Right after a freshwrite_documentcreate, that index lags on-disk truth — the same root cause as the folder-rename bug (PRD-6839). Both handlers now walk the source directory on disk (vialistManagedDocNamesUnderFolderFromDisk), the authoritative set of what the operation touches, exactly as folder rename does.- delete-path (folder): the affected-docs set gates
captureAndCloseDocumentsand therecentlyRemovedDocspopulation, butrmSyncremoves the directory regardless. Against a just-created tree the lagging index returned an empty set, so those cleanup steps were skipped while the directory still vanished — orphaning the in-memory Y.Docs (silent data loss). The disk walk runs before the delete, so it sees every descendant. - duplicate-path (folder): the conflict pre-check (refuse to copy a doc carrying raw
<<<<<<</=======/>>>>>>>merge-marker bytes) was silently bypassed for freshly-created children, producing a broken duplicate. The gate now sees every on-disk child.
The walk also
registerDocExtensions each descendant, so the downstreamgetDocExtensioncalls in both handlers reconstruct the correct extension-ful path.handleTrashCleanupintentionally keeps reading the index — the file is already gone from disk by then, so an empty index is the desired idempotent fast-path.- delete-path (folder): the affected-docs set gates
fix(mcp): exit
ok mcpwhen its launching host dies so stale agent-presence icons clearThe
ok mcpglobal stdio server opens a per-project keepalive WebSocket on the first tool call. That socket plus its reconnect timer keep the Node event loop alive, which silently disabled the only host-disconnect exit path the server had: the passive "stdin EOF then the event loop drains then the process exits" behavior never fires once a keepalive is open, and there was no active handler watching for the host going away.As a result, when the launching host process ended (for example a Claude Desktop per-turn agent harness finishing its run), the
ok mcpprocess did not exit. It was reparented to launchd, kept its keepalive WebSocket open, and the server's presence heartbeat kept refreshing the entry past both the client 5s TTL and the server 20s eviction, so the agent's presence icon never cleared. Each new run spawned a freshok mcpprocess with its own id, so ghost icons accumulated one per run.ok mcpnow exits promptly when its host is gone via two signals: an activestdinend handler (clean disconnect) and a parent-death watch that pollsprocess.ppidand shuts down when the process is reparented (covers the case where a wrapper holds stdin open so no EOF arrives). Once the process exits, the existing keepalive-close to grace-timer to presence-clear path removes the entry. Live, idle agents are unaffected because their host process is still alive.Tighten the MCP write/edit surface's error contract and normalization (PRD-6837): malformed
docNamerejections now surface the specific reason (path traversal, hidden-dot segment, control character, …) instead of one flat "invalid request" line;write_documentstrips every trailing.md/.mdxsofoo.md.mdnormalizes tofoo(was the doubledfoo.md.mdon disk);append/prependjoin with a single blank-line separator regardless of edge newlines (was an occasional\n\n\ndouble blank line); andedit_frontmatteradds the blank line after the closing---when it creates a frontmatter block on a doc that had none.Rename the editor toolbar's agent-handoff action from "Ask AI" to "Open with AI" and drop the duplicated menu copy. The top-nav dropdown no longer repeats an "Open with AI" header above the agent list, and the "Edit with AI" keyboard-shortcut help text now refers to the renamed menu. The bubble-menu "Edit with AI" button (selection scope) keeps its label.
Give the "Relaunch" update notice immediate feedback. Clicking Relaunch now swaps the card in place to "Relaunching to install the update…" and removes the button the instant the click lands, instead of sitting unchanged while the app shuts down running servers before quitting. If the relaunch fails, the card is restored so it can be retried.
fix(share): refresh remote status after Publish to GitHub so the wizard does not re-open on the next Share click
After a successful Publish to GitHub, the project gains an
originremote, but the sync engine had snapshottedhasRemote: falseat boot and nothing told it to re-detect. The Share button reads that stale state, so the next Share click re-opened the Publish wizard as if the project had never been published — and submitting again 422'd on the repo that now exists (<owner>/<name> already exists). The publish handler now fires a fire-and-forgetrefreshRemote()on success (mirroring the set-identity nudge): the engine re-detects the new remote and broadcastssync-statusover CC1, so the client'shasRemoteflips to true and the next Share click constructs the share URL directly instead of re-opening the wizard.