Skip to content
| Marketplace
Sign in
Visual Studio Code>Programming Languages>TheourgosNew to Visual Studio Code? Get it now.
Theourgos

Theourgos

Theourgia

| (0) | Free
Browse and edit a theourgia block store from VS Code.
Installation
Launch VS Code Quick Open (Ctrl+P), paste the following command, and press enter.
Copied to clipboard
More Info

theourgia for VS Code

Browse a theourgia block store in the side bar, open a block in an editor, and save through a durable working draft and selected commit.

Licensed under the Apache License, Version 2.0. See LICENSE.

Before you start

The extension drives the theourgia core installed on this machine: it needs Chez Scheme and the theourgia programs, and asks the core through its own command line. Install the core first (from source today, build.ss into a directory of objects; a Homebrew tap arrives with the core's 1.0 release), then point the settings theourgia.corePath (the directory holding theourgia/ and igropyr/, objects or sources) and theourgia.scheme (the Chez executable: scheme by default, or chez on a Homebrew machine, whose formula installs it under that name) at it, and theourgia.store at a store made with theourgia init. One store is one machine's: the core serves it through one daemon, and two machines writing one store is not this design.

Versions. The core must be at least commit 9f806bb of the theourgia repository (the read --wire version clause that move and rename check); on an older core those two commands are refused by name. The three supply commands need commit 5230bb6 or later, which has supply; an older core answers it as an unknown verb, and the command shows that answer. The core runs on Chez Scheme 10.1.0, 10.3.0 or 10.4.1 from commit 8818b37 (10.1.0 only before it); with another Chez its datum export, def and eval refuse (unsupported-printer-version). VS Code 1.138 is what the suites run on, and the extension claims nothing older.

Platforms. 1.0.0 is packaged for macOS on Apple Silicon only: the extension's save queue takes a file lock through a small native module, built for the machine that packages the extension, and this first package was built here. Packages for Intel macOS and for Linux (x86-64 and arm64) are built by the repository's workflow, .github/workflows/package.yml, each on its own platform, and one is published only when the unit suite has passed on every target, against a real core. There is no Windows package: the lock uses flock, and a Windows lock is a piece of work of its own.

The extension's source is the vscode branch of https://github.com/guenchi/Theourgia.

What this batch does

  • An outline tree: the top level comes from outline --depth 1, and opening a node asks read <id> --recursive and shows the blocks directly under it. A block in a structural conflict is marked, and so is one whose parent is gone.
  • Opening a block: read <id> gives the block, and its heading-src and src fields are put in a markdown buffer, in that order. One file per (store, block), so opening a block twice reaches the same document.
  • Opening a subtree as one document: right-click a node in the outline and choose Theourgia: Open as Document. It asks read <id> --recursive --wire and composes the block and everything under it into one markdown document. It is a read-only, composed view under its own scheme, theourgia-document -- not a projection file, not on disk, and never written back: it cannot be edited or saved in place. (VS Code's Save As still writes a copy to a file you name; a copy saved over a block's projection file changes that file like any other edit would.) To change a block, open that block from the outline and edit its projection. Each block's heading level is its depth under the opened block (# for the block itself), the headings in a block's own body move down by the same depth (not inside fenced code), and anything past ###### stays at ######. A line <!-- theourgia block <id> depth <n> --> in front of each block marks where it starts: a rendered preview hides it, the editor shows it, and it carries the depth that the heading cannot past six levels. Headings written with an underline (===, ---) are not moved. If any block in the subtree cannot be placed -- deleted, an unsettled position, a parent missing from the answer, a title or body that is not text -- the document is not opened, and the message names those blocks. Opening it again reads the store again.
  • Saving: write stores the body in this window's working namespace. A verified readback supplies the immutable version selected by commit, carrying a request id and cursor through an outbox written before transmission.
  • A status bar entry with the store, the actor, the cursor, the number of conflicts and the number of saves whose outcome is not known. When the store cannot be reached at all, the tooltip carries the core's own sentence rather than a question mark: a refusal such as serve-path-occupied (path "...") names a directory you can remove.
  • Searching: the magnifying glass in the outline's title bar, or Theourgia: Search Blocks, asks the core's search for blocks whose title, keywords or text hold every word you type. One hit opens; several are offered in a list, best score first, each showing its keywords or a cut of its text.
  • Keywords in the outline: a block that has any shows them beside its id. They come from the block's own record, not from outline --with-keywords -- see What it refuses to guess at.

What it does not do yet

  • No graph view, no link editing, no title editing. Editing the heading line of a block is refused with a message rather than half-applied; changing a title is set <id> title, which is not in this batch.
  • Going to a definition by name is not here. It wants the core's whereis, which the core does not have yet. Rather than contribute a command that answers "not implemented", this extension asks the core what it can do -- describe returns the catalogue -- so the entry will appear when the verb does. Nothing needs removing when it lands.
  • A store that more than one instance has written to needs a core that names its local writer. A store gets a second log writer when a copy of it is adopted elsewhere and a segment published back. A core from F45 on says which writer is local ((local-writer "<id>") in its check answer), and a window writes as that one. With an older core, which does not say, a window with no cursor of its own is told so and refuses to write rather than guess; one that was already writing keeps its cursor and carries on. An answer naming a local writer its own listing does not hold is refused as contradicting itself.
  • This extension has no direct socket adapter of its own, and does not want one. The socket belongs to the core's (theourgia client), and the thin client is its adapter; a second implementation of that envelope in TypeScript would be a third packer of the same bytes, kept in step by hand. The unimplemented adapter that used to sit here has been removed along with its cell -- when the transport setting stopped offering socket, nothing could reach either. The setting itself is gone too: its other value ran cli.ss, which the core has split by role, and the thin client is now the only program this extension runs.
  • Working drafts retain their original block hash and causal cut. Commit refuses a stale baseline; accepting a new baseline is an explicit reconcile/rebase action.
  • No cache. Every view asks the core.

Recovering another window's unsent work: where it stops

Theourgia: Other Sessions lists the windows that left something behind and offers to take it over or to discard it. Four limits are deliberate, and each of them is a decision rather than an oversight:

  • One store per takeover. A window keeps a queue per store, because a queue carries one cursor and a cursor belongs to one store. A takeover moves the queue belonging to the store this window has configured, says how many requests it left in the others, and can be run again with a different store configured — the claim is re-entered by the window that holds it. What it will not do is move another store's requests into this one's queue, which would send them to a store the user never named.
  • A queue from before stores had their own directories is left alone. Nothing can establish which store it was for, and guessing would be the cross-store write above. It is counted among what was left behind and named in the message.
  • The destination is checked for existence before the claim, and read after it. A queue that turns out to be unreadable therefore takes the token before it fails. Repairing it and running the command again works, because the claim is re-entrant.
  • Changing theourgia.store while the list is open leaves the entries in the store that was configured when the command started.

Settings

Setting What it is
theourgia.corePath The directory holding the core. Required. Either form works -- see below.
theourgia.libDirs Extra directories for CHEZSCHEMELIBDIRS, after corePath. The core imports (igropyr crypto), (igropyr platform) and (igropyr sexpr), so the directory holding igropyr/ belongs here or the core exits before reading an argument.
theourgia.store The store directory, passed as --store. Required.
theourgia.actor The name recorded with every write. Defaults to the OS user name.
theourgia.writer The draft space this window writes into. Defaults to the actor. See One agent, one writer id.
theourgia.scheme The Chez Scheme executable. Defaults to scheme; Homebrew installs it as chez.
theourgia.timeoutMs How long one request may take before the child process is stopped. Defaults to 30000.

A checkout or a product directory

theourgia.corePath may be either a checkout of the core -- the library sources beside its programs theourgia.sc, theourgiad.sc and core.sc -- or a directory the core's build.ss produced, holding the compiled libraries beside the same programs. The extension reads the directory and decides: a client.sc in it means sources, a client.so means products, and the extension sets CHEZSCHEMELIBEXTS accordingly. A directory with neither is refused by name rather than left to fail inside Chez with a message about a library.

It is worth pointing it at a product directory. Measured on one machine, one request: about 460 ms reading the core from source, about 40 to 50 ms from a product directory, about 30 ms once a daemon is up.

One agent, one writer id

A writer id is a draft space. Two windows that share one keep overwriting each other's unsent drafts of the same block, silently -- a later session may bind an id and carry on with its drafts, which is what makes recovery possible and is also what makes sharing one dangerous. Give a second window its own theourgia.writer when it should keep separate drafts of the same blocks.

The id is passed to the core through the environment, as THEOURGIA_WRITER, together with THEOURGIA_ACTOR. It is deliberately not spliced into the argument vector: the verbs that do not take a --writer option refuse one, and a client that added it everywhere would turn ordinary requests into usage lines.

How a request is made

This extension runs the core's thin client, theourgia.sc, and nothing else. The thin client finds the daemon for the store, or starts one (theourgiad.sc, which it launches itself), and sends it the request; the extension holds no socket code and no envelope of its own. Starting, stopping, the exit codes and the words a failure is reported in all belong to the client, and the extension relays them.

The command line takes its verb from the first argument and only then scans for options, so the argument vector is

<scheme> --script <corePath>/theourgia.sc <verb> <args...> --store <store> --actor <actor>

An option placed before the verb is taken as the verb, and the core answers (error unknown-verb ...).

The core prints three kinds of answer and marks none of them: an (ok (text ...)) answer is printed as its own bytes, an (ok (items ...)) answer as one datum per line with no wrapper, and everything else as one datum. So this extension keeps a table of which verb answers which way (src/client.ts), and that table is the one place where it holds an opinion the core also holds. Over a socket the whole answer would arrive and the table would not be needed.

The exit code is the verdict. An answer beginning with ok that came with a non-zero exit is not a confirmed write. Exit code 75 is the thin client's own: the request was refused before the store saw it, and the answer says why.

One request asks for the machine rendering

A commit is sent with --wire. The human rendering drops every clause beside items, and one of those clauses is (behind ((<writer> . <seq>) ...)) -- which log writers have landed records since this save's draft took its baseline. With --wire the answer arrives wrapped, (ok (items (ok ...)) (behind ...)), so the client hands the item on as the answer exactly as before and puts the form round it beside it.

behind names other instances of the store, not other people. Every agent writing into one store on one machine appends through the same log writer, so a colleague's commit does not appear here; what appears is a copy of the store that was adopted elsewhere and had a segment of its log published back. The notice says so.

Saving, and what happens when the answer is lost

A save first becomes a durable W draft. The outbox then records request id, cursor, block, body and selected working namespace/version before commit is sent. Then:

  • The queue file written but its directory not flushed: the save stands -- the entry is there and is sent -- and a warning follows the save's notice, saying the entry may not survive the machine losing power.
  • ok naming the record it wrote: the entry is dropped and the cursor moves.
  • ok naming no record: the entry is kept and this is reported as a defect. Every write the core accepts says which record it appended, and an answer with neither a cursor nor an event leaves the next save with nothing to be composed against.
  • (error unknown <why>), a bare (error unreadable ...), a timeout, or a core that printed nothing: the entry is kept. The store may hold the record; asking again with the same request id is the only way to find out. "Theourgia: Retry Pending Saves" sends the same bytes again — the same id, the same cursor, the same body, not whatever the buffer now holds.
  • A refusal only a person can answer -- the store directory is missing, the socket path is too long, or (error refused (instance ...)), a store created under another THEOURGIA_HOME or moved since: the entry is kept and parked, and later saves of the same block wait behind it. Changing a setting sends it again, and so does "Theourgia: Retry Pending Saves" once the cause is fixed outside the editor; if it is not fixed, it is parked again.
  • Any other refusal: the entry is dropped and the refusal is shown, with everything the core said after its name (and in words, the remedy the core names), because retrying cannot change it.

Saves are sent one at a time, and an entry nobody can resolve holds the ones behind it rather than being stepped over. That serialisation belongs to the queue file rather than to any one object: the extension builds a fresh saver whenever a setting changes, over the same outbox, without waiting for the old one to finish.

The queue itself never changes before the file does: every change is written and only then adopted, because a queue that changed in memory and not on disk is worse than one that changed in neither — the next call sees the new state, believes it was recorded, and acts on it. The file is written, flushed, renamed and the directory flushed, so that what survives a machine losing power is, as far as this client can arrange it, what the user was told had been recorded. That is an effort, not a guarantee. A directory that will not open for flushing is passed over silently, because some file systems refuse it and the bytes are already down by then. A flush that is attempted and fails does not stop the save either, but it is no longer silent when a save is queued: the save stands and a warning follows it. And only a file that is not there is an empty queue: every other reason a read can fail leaves open the question of what was recorded, so the outbox refuses to be written to at all rather than replacing a file it could not read.

An entry is queued until the moment it goes out and sent from then on, and that is written down before the request leaves. A queued entry may still have its cursor corrected to the position the previous answer established; a sent one may not, because the cursor is part of the request's identity and changing it would turn a retry into a different request wearing the first one's id. A host killed between the send and the answer leaves a sent entry behind, and that is the state the distinction exists for.

Windows normally have separate session and working paths. Recovery can still reach an old session's current file, sidecar and queue. A stable kernel lock covers each participating read-modify-write and final ownership check. Contention reports busy; retry after the other operation finishes. Locks have no lease timeout and are never unlinked. Process death releases them. User dialogs and core requests run outside these critical sections.

The managed session layout shares one lock for all its block/queue operations, including discard. This is local process exclusion, not a distributed filesystem locking protocol. An editor or external tool that writes these files does not participate in this lock.

The first cursor, before any write has been answered, comes from check: a store with one writer has no ambiguity. check does not say which writer is local, so a store with more than one — one that was adopted or copied — refuses to be written to from here rather than guessing.

The file a block is edited in

The current layout is sessions/<session>/<store>/<block>/<slug>-<block>.md with its sidecar <slug>-<block>.md.meta, where the slug comes from the block's title when it is first published (<block>.md when the title leaves none) and is never changed after. A directory is read by its sidecar, not by its name, so one an older build left as current.md is still read. Refresh installs one complete temporary by rename; it does not add a numbered version. Owner records and replacement temporaries live in sibling control directories. History belongs to the core log and exported Git projections.

The editor's dirty flag does not answer "is there work here". The save handler runs on onDidSaveTextDocument, after the bytes have reached disk — so a save the core refused leaves a clean buffer holding text the store has not got. Dirty buffers are never refreshed. A clean buffer can refresh from its verified W source; a prepared or ambiguous source cannot be sent. Each projection has an identity independent of its byte hash, so a late receipt cannot confirm a newer source merely because the bytes match.

Sequential saves advance their baseline only with proof that the displayed W version was committed. The next draft uses that commit's causal cut, retaining any intervening external write as a stale-baseline refusal.

Theourgia: Migrate Legacy Block Files explicitly migrates a selected numbered block. The command retains an archive of every original file and resumes from its journal after interruption. Pending sends, live owners, dirty buffers, changed inputs, and drafts whose bytes cannot be verified against committed/W data stop migration. Multiple unprotected old drafts remain intact for manual handling; the command never overwrites them into one working slot. Re-run on the original selected path to resume an interrupted archive step.

Packaging, and the gate that checks a package

Package with dependency detection on:

npx vsce package --allow-missing-repository

Not with --no-dependencies. That flag leaves out every file under node_modules, the s-expression reader included, whatever .vscodeignore re-includes -- and activate() loads the reader before anything else, so a package built that way installs and never starts. (A package built on 2026-09-19 did exactly that.)

node scripts/package-gate.js (with THEOURGIA_CORE and THEOURGIA_LIBDIRS set, as for the real-core cells) builds a package the way above and reports, without judging: whether the files the extension needs to start are in it, and whether they would be with --no-dependencies; the install into an editor that is not yours (the one @vscode/test-electron keeps in .vscode-test/, with a profile and an extensions directory of its own, or the editor THEOURGIA_TEST_CODE names); the activation line in that editor's exthost.log; and one save made through the installed extension to a temporary store -- the store's log for the block before and after, and whether the saved line is in the store. What it makes for the run -- the package, the editor's profile and extensions, the store, the core's run and home directories -- is under one temporary directory, removed at the end with the count said. Two things it leaves, as any build does: the repository's out/, which it compiles, and the editor @vscode/test-electron downloads into .vscode-test/ if it is not there yet. It removes every VSCODE_* and ELECTRON_* variable from its environment first -- the editor would otherwise take its profile from VSCODE_APPDATA or VSCODE_PORTABLE before --user-data-dir -- and says which it removed. Lines it prints start with [gate]; the rest is the editor's own output. It exits non-zero only when a reading could not be taken.

Native lock build

The current native lock build supports macOS and Linux. npm run compile requires a C compiler and Node N-API headers; set THEOURGIA_NODE_HEADERS to the directory containing node_api.h if automatic discovery fails. The build does not download headers. N-API v3 allows the same platform/architecture binary to load in the tested Node and VS Code hosts. Windows support has not been implemented or tested. Directory-fsync limitations described above still apply; process locking does not strengthen power-loss durability.

What it refuses to guess at

An answer in a shape this client has not met stops the request rather than being trimmed to the part it understood. An outline line that does not parse, a block field in an unfamiliar shape, an answer line that will not read — each of those, skipped, would show a store with one block missing, a block with one field missing, or a shorter list of conflicts, and every one of those reads like good news. The same goes for the status bar: a conflict count that could not be fetched shows as unknown, never as zero, and changing the store setting clears it rather than carrying the old store's answer across.

Requests are awaited, and a setting can change while one is in flight. Every request records which generation of the settings it was made under and is dropped if that generation has been replaced — otherwise a block read from one store would be written into a file named after another, and saved into it.

Two cases the outline's text cannot express, both reproduced on a real store: a title ending in conflict, and a title whose second line looks like a row (second line\n- fake.1 invented). Neither reaches the tree any more — the outline is read only for ids and depth, the title comes from read <id> and the mark from conflicts, and every top-level id is confirmed against the store, so a forged row is a refusal naming the id and its line. The root cause is still that the outline is a rendering with no escaping, and the fix belongs in the core; until then this client does not read anything from it that a title could forge.

Keywords are read the same way. outline --with-keywords prints them, and this extension does not read them from there: a title holding two spaces and a bracket can forge that field as easily as it can forge a mark. The block is already being read for its title, and its record carries its keywords, so they arrive by the channel a title cannot reach.

The s-expression reader

src/vendor/goeteia/sexpr.mjs is a verbatim copy of goeteia's rt/sexpr.mjs, held to sexpr-vectors.json, the golden fixture generated from (igropyr sexpr) — the authority for this wire format. The copy exists only because goeteia's package does not export the deep path in a published release yet. Do not edit it: test/unit/vendor-sexpr.test.ts sweeps both fixtures and checks the file's own bytes against the digest its provenance note claims, so an edited copy that kept its note fails.

It is held to two fixtures, both copied beside it: sexpr-vectors.json, generated from (igropyr sexpr), and sexpr-escape-vectors.json, which covers the escapes a conforming R6RS writer emits — \a \b \f \v and \xHH;, in strings and in symbols. That second table exists because of a gap this extension hit: the reader used to accept only \n \t \r \" \\, so a block whose body contained a form feed was stored happily by the core and could never be read back. S14 in test/unit/real-core.test.ts was red for as long as that was true and is now the guard against it reopening.

Why it is still a copy. goeteia's package exports ./sexpr now, but the published 1.7.1 predates that export, so depending on it would not resolve. When a release carries the export, this copy goes and a dependency takes its place.

What the cells do not cover

Recorded here rather than left to be rediscovered. Each of these is a place where a cell exists and proves less than its name suggests, or where no cell exists at all.

  • The sentence a successful save shows. behind is measured end to end -- a store, a copy of it adopted elsewhere, a segment published back, and the next commit through this extension's own save path carrying the notice -- but that is a cell against the core, not a cell inside an editor. The notice reaches a user through showInformationMessage, and nothing in an editor-hosted run can read one. What is covered is that the clause arrives, is read, drops the writer the commit itself advanced, and becomes the sentence; what is not covered is that the sentence is put on the screen.

  • A daemon that cannot be started, during a save. The editor-hosted cell for this obstructs the socket path and reads the words back out of the status this extension hands to a caller. It gets there through the conflict count's own asking, because a save that meets the obstruction never reaches the queue -- the request that fails is the one that asks where the cursor is, before anything is written down. That is the right behaviour and it means the save path's own report of the failure, which goes to a message box, is not what the cell reads.

  • The test host's own storage. The editor-hosted runner empties the extension's globalStorage under its profile before every start, and refuses to start if it could not. That guard follows symbolic links in the profile path and treats a directory it cannot read as absent. It is a harness pointed at a directory it created itself; it is not hardened against a profile somebody else prepared.

  • A save the core refused. The editor-hosted cell named for a refused save exercises a refusal this client makes: a changed heading is turned away by splitDocument before the saver is reached. A save the store itself rejects travels a different path, and nothing here walks it — an implementation that preserved locally refused edits while overwriting ones the store turned down would pass every cell in this tree.

  • Atomic replacement and flushing. The disk-failure cells make the queue's parent directory unusable, so they fail at the directory check that precedes the temporary file. They establish that a failed write does not damage the queue; they do not establish that the write is a temporary file, a flush and a rename, which is what the code does. A writer that truncated the real file in place, or omitted fsync, would pass them.

  • Showing a buffer from a store the user has just left. openBlock used to check the settings generation after each of its editor waits as well as after the core read. Those five checks could not be guarded: the waits are VS Code calls, not core requests, so no stand-in core widens them, and a configuration change begun from inside onDidOpenTextDocument — which does fire during the first of them — has not reached the extension by the time the wait resolves. That was measured. Rather than leave five guards nothing could make fail, they were removed; what remains is that the buffer carries the store it was read from, so a save into a differently configured store is refused by name. The residue is cosmetic: a buffer from the old store can still appear after the settings change.

    The three generation checks that remain each have a cell that fails when the check is removed — refreshConflicts (C6), openBlock (C1), the retry report — verified one at a time by replacing each with a condition that is always false. OutlineProvider keeps two more comparisons of its own, which those three cells do not speak for.

    What replaced them is not another check. Removing the five exposed an older hazard they had been hiding: two opens of one block can overlap, and whichever finished last became the baseline a save is measured against — which has nothing to do with which reading the store answered most recently. A baseline older than the buffer has a prefix the buffer no longer starts with, and a prefix that fails to match is not refused: the heading is taken for body and written into the block. Each open now takes a ticket before it reads and cannot register over a newer one (src/open.ts). That rule lives outside activate precisely so a cell can drive the interleaving the editor cannot be made to produce.

  • The three lines that hand VS Code to the placement step. src/open.ts decides which reading of a block a save is measured against and src/placing.ts acts on that decision; both are driven directly by cells, because the interleavings they exist for cannot be produced through the editor's API. What is left uncovered is the adapter: the three closures in openBlock that forward openTextDocument, setTextDocumentLanguage and showTextDocument. A cell hands placeReading three functions of its own, so nothing checks that the real ones are wired to the right VS Code calls.

    This shape is why that separation exists. For one round the decision was acted on inside activate, and when register stopped returning a boolean the call site's if (!admission) kept compiling and stopped firing — every answer was a non-empty string, so a losing open would have gone on writing the file, with all 17 editor-hosted cells green. It was caught by reading. The answer is now an object whose falsy reading is a field, so the compiler finds that mistake, and the step it guards has its own cells.

  • What a retry actually displayed. The retry cells read the notice the command decided on and returned. Whether it reached the screen, and whether the status bar was repainted, is not observed: deleting the call that shows it would leave them passing.

  • Nested-document visibility. The tree does not mark a nested document, because the core's own handling of the shape is still being decided. The mark is read and carried; what the tree should draw for it is not settled.

Running the cells

npm install

# the parser, transport, outline, block, cursor and save cells
npm run test:unit

# the same, plus the ones that need the core itself (O3, S7, S14)
THEOURGIA_CORE=/path/to/theourgia THEOURGIA_LIBDIRS=/path/holding/igropyr npm run test:unit

# the cells that need an editor
THEOURGIA_CORE=/path/to/theourgia THEOURGIA_LIBDIRS=/path/holding/igropyr npm run test:integration

The cells start daemons, and they are accounted for. Every fixture that reaches a real core gives the core a run root and a home of its own under a temporary directory, and stops by pid the daemons that directory gave rise to. Two gates stand behind that rather than anybody's memory: one cell asserts that a fixture's daemon is visible before it is stopped and gone afterwards, and a hook over the whole suite asserts that no process of this run is left and that your own ~/.theourgia/run did not grow. Growth, not total: whatever is in that directory when a run starts belongs to whoever put it there.

It is growth and pids because it has happened. When these cells were first turned onto the shipping transport the fixtures set no run root, and one run left thirteen daemons running and thirteen directories in the user's own. Nothing went red. It was cleaned up by hand.

This extension needs a core that has request tracking. Every save carries --req <id> --cursor <writer>:<seq>, and a core without those options answers (usage (set <id> <field> <value>)) — measured against theourgia at 842cf46, where the tokens --req, --cursor, tracked-request and req-not-tracked do not appear in cli.ss or rpc.ss at all. The outbox is built on that feature, so against such a core every save is refused. A usage line can also mean an argument the core did not expect, so the message names both.

A nested document is shown under its parent, with its mark. The write path refuses a document anywhere but the top level, so a nested one exists only in history made before that rule or elsewhere. The pinned core (theourgia 5230bb6, as 9f806bb, 659fea2 and cba98ae before it) treats it as a block like any other: it is in its parent's recursive read, and it is reported once under conflicts as nested-document. So it appears in the outline where it is, as a child carrying that mark, never hidden. nested-document is not a mark that puts a block in the root listing; delete the parent and the block becomes an orphan as well, and that mark does put it there, where it shows with both marks. (An older core's walk stopped at such a child and the outline could not show it; queue item 48 moved these cells to the current behaviour.)

When a writer cannot be read. A store whose log for one writer cannot be read -- a directory without permission, a disk that answers with an error -- still answers every read with everything the other writers wrote, and says which writer it could not see. The extension shows those rows as usual and says so beside them, as information and never as an error: the outline's first row, above the blocks; the search's message, or the picker's title when there are several hits; a line before the text of a document or a block opened from it; a message after a save the store took; and incomplete in the status bar until a reading sees every writer again. Each names the writer, the path and the reason. While a writer is missing, the marks on blocks are shown as not known, and the conflict count is the other writers' count, since the missing writer's conflicts are not in it. Saving goes on as usual. A writer whose log is damaged partway is read up to the damage: its records up to the last good one are in the reading, and the note says so -- the record it was read up to, the log's own name for the damage, the path and the reason. A note of any other kind is not one this extension knows how to say, and the reading is refused by name rather than shown as complete.

Code blocks. A text-mode code block -- one import-code made from a source file -- keeps its source as bytes, and it opens as that source, decoded as UTF-8, in the editor mode its lang names (plain text when the editor knows no such mode). Line ends are kept as they are. A block whose bytes are not UTF-8 does not open: the store says so by name (non-text-projection), and a view that reads such a field refuses with text-not-utf8, the field, the block and the offset of the first byte that does not decode. A file that begins with a byte-order mark is not saved.

Go to definition. On a name in a block, "Theourgia: Go to Definition" -- or the editor's own Go to Definition -- asks the store's whereis which block defines it. One answer opens that block at the line of its (define ...), found in the text as it is shown, a draft included; several are listed with their library and kind, or export for a library that exports the name, which opens at the library's first line. A name nobody defines is said, with the nearest names the store knows. The line search reads ; and #| |# comments as comments; a #; datum comment is not recognised.

Suggest a split of a source file. On a source file on disk (not a block's own file), "Theourgia: Suggest a Split of This File" asks the core's split-suggest where the file could be divided into blocks, and opens the review file the core writes; nothing is recorded. The cuts come from the editor's own symbols for the file -- the top-level ones the language's symbol provider gives, sent as --symbols -- and the message beside the review says so (cuts from (vscode "<version>" "<languageId>")), with the core's warnings; with no symbols (no provider, or none yet) the core's own definition patterns choose, and the message says regex. A dirty file is saved first, and nothing is sent if the save fails. The positions are counted in the file as it is on disk, a byte-order mark and carriage returns included, and the file's digest goes with them; if the file changes while its symbols are being collected, or before the core reads it, nothing is cut (symbols-stale, shown as the core says it). Each symbol is sent at the start of its line, two symbols on one line as one, and a comment above a definition goes with it, as without symbols. A file whose lines end in CR alone is not cut by symbols: the core takes a line start to follow a line feed, and refuses (symbols-not-a-line-start). Which provider named a symbol cannot be said: the editor merges them.

Supplying what the editor knows. Three commands hand the store facts the editor's language support computes, which the core keeps beside the store and never in its log (the core's README, "Derived data from an editor"): "Theourgia: Supply Signatures and Keywords" and "Theourgia: Supply Calls" for the committed store, and "Theourgia: Supply Diagnostics" for this window's writer, from its working view. Each projects the store with export-code into a directory in this extension's own storage, emptied first, opens the projected files without showing them, asks the editor's providers, and sends one supply per language with every projected file listed by its digest and every file of that language named as replaced, a file with no fact included (so its old facts clear).

  • A block's signature is its first top-level symbol's detail, else the first line of its hover, else none; its keywords are the words of the names it declares (itself and its direct children), split at case changes, underscores and digits. A call is an edge from the call hierarchy to a block of the same projection; a call into anything else gives none. A diagnostic keeps its severity and its byte range in the projected file.
  • Which block a position is in is read off the projection's own marker lines. A fact depends on its own block and every other block of its file (a call on the target's file too), so the store drops it when any of them changes; the facts are as fresh as the last supply, and nothing supplies them on its own.
  • If a projected document changes while the facts are collected, nothing is sent and the command says which file. Diagnostics are taken once they have not changed for 1 s, and at most after 10 s, when the message says the analysis may be incomplete. A refusal is shown by name: supply-stale asks for the command again; supply-malformed is this extension's defect, and its supply file is kept.
  • The projection is outside the workspace, so a language server may see it with less context than a workspace folder (no project configuration); a folder of the person's choosing is a later option.

Browsing by file. The tree has two modes, switched from its title bar: Files, the directory tree export would write, and Outline, the store's own parents and order. The directories are not kept anywhere; they are read from the path field of the blocks export writes as files -- a document at the top level, a text-mode file block wherever it sits, and a datum-mode library -- so a directory appears with its first file and goes with its last. Directories come before files, each sorted by name; a file is labelled with the last part of its path, and its title is in the tooltip. A path is taken as export takes it, never tidied: one export refuses (../a.md, docs//e.md/) puts its block under not in any file with the path in the tooltip, beside the top-level blocks with no path, those with a path that are not a file, and the orphans. Two documents with one path are both listed and the second by id says not exported: path taken; two text files or two libraries with one path both say export refused: duplicate path, as export refuses the whole export. A document below the top level is never a file, whatever its path: export writes its content into the enclosing document. A store opens in Files when any of its top-level blocks carries a path and in Outline otherwise, decided the first time it is shown and kept for that store; it does not change by itself when a path appears later. On a directory, New File Here makes a markdown document at that path (one batch through the save queue, so an answer that is lost is kept and sent again); on a file, Move to Directory and Rename File write its path. A directory has nothing to open, and there is no new or deleted directory: a directory is only ever a prefix. The Files listing reads each top-level block with its whole subtree, where the Outline listing reads each alone. A path is a path only when it is stored as a string, as the exporters take it, and a document is a file only when export takes it as a root: an orphan document with a path is listed in no file. Text files are written as a family, as export-code writes them: one text file with no safe path, one path two text files share, or one block under a text file that is not a text-mode code block, and every text file says export refused with the reason; datum libraries likewise. Two of the datum exporter's refusals are not predicted here and are answered only when exporting: a library whose child's doc holds a projection marker (marker-in-doc) or is not whole-line comments (invalid-doc); such a library is shown as written. A row names a block and nothing more: Move and Rename read the block afresh (read <id> --wire, whose answer carries the block's version), offer its current path, and write the new one with --if-unchanged <version>, so a block changed after that read -- by another window, or while the prompt was open -- is refused by the store as changed; the window says "this item changed since it was listed; the view is refreshed" and lists it again. A row, a node or an open kept from a listing of another store is refused before anything is read, and an action whose store was switched while its read or its prompt waited sends nothing; once queued, a write belongs to the store it was read from. A settings change that keeps the store refuses nothing. A write of the path is never queued without the version it was read at. A queue holding a new document not yet settled, or a write kept with the version it was read at, is written as version 2, which an earlier build of this extension refuses by name rather than misreading or sending without its condition; every other queue is written as version 1, as before. A kept write is sent with the same version however late it goes, so it is refused if the block has changed meanwhile; whichever send drains it -- the retry command, the drain at startup, or the drain in front of another save -- the window says so, and lists the view again when that store is still the one shown.

Point THEOURGIA_CORE at a copy nobody is editing. The core is somebody else's working tree, and a suite that reads one is only as stable as the editing going on in it — a run of these cells once reported the core exited 255 without an answer, and a probe built to fish for it caught Exception: variable t is not bound at the exact second another session wrote store.ss. Neither was a defect in the core; both were a half-written file being read. The cells take a digest of the core at the start and check it at the end, so a reading taken against a moving tree says so instead of looking like a flake.

The cells that need the core fail when it is not there; they do not skip. A skip and a pass are the same colour, and the two situations — "this works" and "nobody has checked" — should not be.

  • Contact us
  • Jobs
  • Privacy
  • Manage cookies
  • Terms of use
  • Trademarks
  • Your Privacy Choices
  • Consumer Health Privacy
© 2026 Microsoft