KineDB Labs

@kinedb/kdmem (0.1.13)

Published 2026-10-23 10:42:07 +00:00 by skadefro

Installation

@kinedb:registry=https://git.kinedb.com/api/packages/kinedb/npm/
npm install @kinedb/kdmem@0.1.13
"@kinedb/kdmem": "0.1.13"

About this package

kdmem: the shared live memory of a team. One command sets up a machine.

kdmem: the shared live memory of a team

One central kinedb server (kdmem.kinedb.com) holds the shared state. A daemon on each machine publishes the state of its Claude sessions. A web page shows all machines and branches.

Install

kdmem is published on the kinedb package registry, never on npmjs (browse: https://git.kinedb.com/kinedb/-/packages). Reading it needs no login. Run this ONE command on each machine. It makes the device key, installs the mod and starts the daemon:

NPM_CONFIG_REGISTRY=https://git.kinedb.com/api/packages/kinedb/npm/ bun x @kinedb/kdmem --user you@yourcompany.example --company yourcompany

On Windows (PowerShell) write it so:

$env:NPM_CONFIG_REGISTRY="https://git.kinedb.com/api/packages/kinedb/npm/"; bun x @kinedb/kdmem --user you@yourcompany.example --company yourcompany

Replace you@yourcompany.example with your own e-mail address and yourcompany with the name of your own company. The command stops with an error if you keep the example text. It takes the latest version, so it has no version number (B128). bun x is the same program as bunx; two people reported that bunx fails on their computers, and the cause is NOT DETERMINED. The registry must come from the environment (or from a @kinedb:registry= line in ~/.npmrc, the second way in kdmem --help). bun x --registry=<url> @kinedb/kdmem FAILS on a clean computer: bun asks registry.npmjs.org and gets 404 (DIRECTLY OBSERVED 2026-10-23, with no .npmrc and an empty temp folder; bun keeps its copy in the temp folder, so a test that reuses it hides the failure). The Linux and macOS form is checked on Linux. The PowerShell form is NOT YET checked on Windows.

The command asks the server whether the company exists. A company that exists adds a NEW person by itself (B14): the server makes the user, adds it to the company and attaches the key of the device, so nobody approves anything and the setup ends with state: APPROVED. The device pins the root key of the company. An older install with no pin takes the key from the database once, by itself; a pin that exists is never replaced. The e-mail address is NOT proven: anyone who knows the company name can join. The operator turns this off with KDMEM_AUTO_JOIN=0. An address that already exists is never joined this way, because a key must not go onto an existing account. An admin of the company adds that device on /manage (Devices tab). A new company is made by the command itself when the server allows it (KDMEM_SELF_SERVICE=1; off on kdmem.kinedb.com): the user becomes the first admin, and the key of the device becomes the root key of the company.

The password. A person made by a join or a signup has NO usable password: the server makes a random one that nobody keeps, and the device key is the proof. To sign in on the fleet page or on /manage, the person sets its own password with the device key (no old password needed; the server turns the switch SET_OWN_PASSWORD on for a join, a signup and an admin reset). The daemon file runs it:

bun "$(ls -t ~/.config/kdmem/code/agents-*.js | head -1)" --password             # asks twice, the typing is hidden
bun "$(ls -t ~/.config/kdmem/code/agents-*.js | head -1)" --password --generate  # a random password, shown ONCE

An admin of a company (or a global admin) sets a new temporary password for a member on the Users tab of /manage ("Reset password", two clicks). The password shows once. An admin cannot read or set a password of another company, and only a global admin resets the password of a global admin. A person cannot change the password of another person with the device key.

A signed-in person changes its own password on /manage (any person, also a user who is no admin): the tab My password asks for the old password and the new one twice (6 to 128 characters). The service checks the old password again, takes the person from the session and counts a wrong old password as a failed login. An admin can also TYPE a password for a member: the form "Set a password for" on the Users tab (the password is not shown again). "Reset password" stays for a random temporary one.

Status: @kinedb/kdmem 0.1.13 is published. The command of 0.1.5 is checked on Linux, macOS and Windows. NOT YET checked for 0.1.6 to 0.1.13: the full command on macOS and Windows. 0.1.8 added the messages between sessions, the mod from the database and the self reload of running sessions (B14). 0.1.9 adds the join of a new person with no admin step. 0.1.10 stops the command when the example address or company of the README is kept. 0.1.11 carries the mod with the commands, hooks and prompts from the database (a new machine gets them from the database at once anyway). 0.1.12 carries the fixes B117 (a new install no longer allows every folder), B120, B121, B125 and B126 in the daemon and the mod. The help text of 0.1.12 named a short command (bun x --registry=<url> …) that fails on a clean computer; 0.1.13 has the command with the environment variable. The install of 0.1.13 is NOT YET run on a clean machine. The signup, the lists of signers and the company layer are checked against the live server by the probe q14-signup-e2e.ts.

What starts the daemon

No operating system service starts it. The install command starts it once. After a restart, the first Claude session that starts runs the mod hook: it reads the heartbeat file of the daemon (~/.config/kdmem/heartbeat.json), and it reads it again at each trigger (a prompt, a tool call, a turn end, the 2 s message tick), at most once in 30 seconds (B126). A live daemon gives no command at all. A stale heartbeat (2 minutes) makes the hook run <daemon file> --ensure once, and that call starts the daemon detached. One start in a minute at most. A daemon that cannot publish for 10 minutes ends itself (B125), so its heartbeat goes stale and the next check starts a new one. The daemon replaces its own code from the signed database. The daemon does not poll for it: a WATCH on the table kd_assets wakes it when a skill or the code changes (a safety pull every 5 minutes stays; without a working watch it pulls every 30 seconds).

The mod is a thin shell. A mod cannot evaluate script text (probe 2026-10-18: new Function and eval fail with "Code generation from strings disallowed"), so it cannot load a script file. Its numbers (the stale limit, the gap between starts, the check interval) come from ~/.config/kdmem/mod-config.json, which the daemon writes at its start. A new daemon bundle from the database changes them with no reinstall of the mod. The mod code itself also comes from the database (B14): bun daemon/assets-cli.ts <env> mod publishes the hooks and the plugin.json as signed rows mod/.... A mod can load only files inside its own plugin folder, so the daemon writes a marketplace folder of its own (~/.config/kdmem/mod-market) and runs claude plugin marketplace add|update and claude plugin install|update. A running session reloads by itself: after each pull the daemon writes ~/.config/kdmem/reload.json ({ mod, skills }). The mod reads the file every 5 seconds. A changed value makes the mod run the commands reload-plugins or reload-skills ($.command.run, the name has no slash). Only a session that started before this feature needs one restart. The reload waits until the session is idle (no turn runs) and the value was stable for 20 seconds, so several releases are ONE reload (reloadPlan in mod/hooks/reload-core.js). mod is the key of the mod rows. skills is the key of the skill and command files that the daemon wrote (assets-manifest.json), so a release of the mod or the daemon does not run reload-skills.

The mod has no file watch. It compares the text of commands.json, hooks.json and prompts.json every second, so a change that the daemon writes (a package turned on) takes effect within about a second.

What every session used: models, run time and tokens (B96, B97)

The daemon logs the tokens of ALL sessions of its computer, with no skill and no package. It reads the transcripts that Claude Code writes (~/.claude/projects/*/<session>.jsonl and, for each subagent, <session>/subagents/agent-<id>.jsonl) and stores one row for each session and each subagent in the table kd_arc_tokens (tokens-scan.ts, arc-tokens-ship.ts). Make the table once with bun daemon/company-cli.ts <env-file> arc-tokens-table (run it again to add the columns cwd and branch to the table of B96). Each person reads, writes and removes only the own rows. Read them with bun daemon/tokens-cli.ts [days] [text], for example ... 3 B96 or ... 1 opus.

  • The daemon keeps an offset for each file and reads only the new bytes (at most 32 MB for each pass of 5 seconds, the newest file first). It reads only a transcript that changed in the last 30 days (B99; it was 3 days). At its start it needs a few minutes to read a big backlog (measured on one computer: 3,936 rows and 8,373 hour rows, about 7 minutes).
  • The table kd_arc_hourly (B99) holds one row for each session, subagent, hour (UTC) and model: requests, input, cache write, cache read, output, thinking tokens and time, tool calls, tool errors and ms, and the company, the folder, the branch and the issue. A request counts in the hour of its LAST line, a tool call and its error in the hour and model of the request that made the call. The graphs of the page /usage read this table. The daemon sends only the hours that changed since the last stored shipment, at most about 200 hours in one pass. company-cli.ts <env-file> arc-tokens-table makes it, and arc-hourly-company gives the role of each company the right to READ the rows of its own company (WHERE company = ...). So the people of a company see the usage of each other: this changes the rule "each person reads only the own rows" for this table. It holds no text of a task, no tool name and no description. Each person still writes only the own rows. B112: addCompany makes the read grant for a new company (when the table exists), so nobody must run arc-hourly-company again.
  • A row is written at most once in 60 seconds for a session. The last change of a session that stops reaches the table within 60 seconds. A row is replaced, never added.
  • The issue is set for each REQUEST (B102; B101 did it for each row). A request takes the first of these sources: the latest command /arc <id> of its own transcript (a command block at any line; typed text only in the first lines); the command of the main row of the session (so a subagent gets it too); the branch arc/b<id>… or arc/<id>-… of its line (arc/b3191-pr2 gives B3191, no letters give B); the folder name arc-b<id>. A folder like arc-14-company-peers has no letters before the number, so it gives no issue. A request on main or on a detached HEAD without a command has an empty issue.
  • A session that works on several issues is split. An hour and model with two issues has one row for each issue in kd_arc_hourly: the key is owner|machine|session|agent|hour|model|issue. The daemon deletes ALL the rows of an hour and model by the columns, then inserts the parts, so an old row without an issue in its key goes away and nothing is counted twice. The issue of the row in kd_arc_tokens is the issue with most requests. A row that was stored without an issue is stored again, with its hours, when the issue is found later.
  • The guess of the issue (B103). Both tables have the column issue_guess. The daemon counts the ids (B and 3 to 6 digits, in any case) in the text that a person typed and that the model wrote. It does not count a tool result or the thinking of the model. When one id has at least 10 mentions and at least half of all mentions of an id, it is the guess of the session. It is stored apart from the exact issue and never replaces it. The same guess is in all rows of one session file (a subagent has its own). When the guess appears or changes, the daemon sends all hours of the file again. A guess can be wrong: the page marks it with ~. Run company-cli.ts <root.env> arc-tokens-table BEFORE the new daemon runs, because the INSERT names the new column.
  • An issue id is unique only inside one project: the id B12 of a demo project and the id B12 of kinedb look the same in the tables.
  • /compact does not reset anything: the session keeps ONE transcript file, and every request keeps its own usage line. peak context is the biggest value ever, not the value now. /clear starts a new session id, so it is a new row.
  • The request that writes the summary of a /compact is NOT in the transcript (B98; measured: no assistant line and no request line in the compact window). So the totals of the requests leave it out. The line compact_boundary holds the only numbers, and the daemon keeps them apart, because they are an estimate and not usage: compacts, compacts_auto, compact_pre_tokens (the context that the summary requests read, summed), compact_post_tokens and compact_seconds. Measured on one session with 34 compacts: about 10.9M input tokens, 1% of its cache reads. The tokens of the summary text itself are not known. tokens-cli shows compacts and compact read.
  • The column by_model holds the numbers of each model as JSON (B98): requests, input, cache write, cache read, output, thinking tokens, thinking time, tool calls, tool errors and ms. A tool call and its error belong to the model that made the call. ms is the time of the requests, from the line before a request to its last line: an estimate, because Claude Code does not write the time of a request. A row with more than 8 models keeps the 8 with the most requests. The numbers of the models add up to the numbers of the row. A comparison of models shows speed, size and tool errors. It does not show the quality of the work: the tasks of the models differ.
  • No text of a transcript leaves the computer. A row holds numbers, the names of the models and of the tools, the folder, the branch and the task text of a subagent. KDMEM_TOKENS=off in the environment of the daemon turns the log off on that computer.

For each session and each subagent the row holds: the model (and the count of requests for each model), the start, the end, the wall time (end minus start), the active time (the gaps of more than 5 minutes do not count), the requests, the messages of the user, the tool calls (for each tool name) and the tool errors, the input tokens, the cache write tokens (and the 5 minute and 1 hour parts), the cache read tokens, the output tokens, the thinking tokens, the thinking time, the peak context (the biggest input of one request) and the web searches and fetches.

  • One API request writes several lines in the transcript. Only the last line has the final output_tokens and thinking_tokens, so the daemon keeps the last line of each requestId.
  • output holds the thinking tokens. A session that works on several issues counts all of them together.
  • There is no price. A price needs a table of the prices of each model, and that table changes.

The page /usage (B100)

The page https://kdmem.kinedb.com/usage (a link "Usage" next to "Manage" on the fleet page) shows the table kd_arc_hourly as graphs and tables. It uses the sign-in of the fleet page (the token in sessionStorage) and only reads. The files are site/usage.html (the page) and site/usage-core.js (the logic, with a test in daemon/usage-core.test.ts); deploy-site.sh copies both and the Caddyfile serves them.

  • Personal / Company. Personal shows the own rows. Company shows the rows of the company that the person may read (the grant of arc-hourly-company, see above), so it shows the usage of each person of the company. The list of companies holds the companies that the rows name and the companies that the person manages (B111: the fleet page saves them at sign-in in sessionStorage kdmem.companies), so a global admin gets the list also for a company with no row yet.
  • Time range. The presets 24h, 7d, 30d, 90d and All, in UTC. Drag on the graph to select a range: every graph and table follows it. Zoom out doubles the range, Reset zoom (or a double-click on the graph) goes back to the preset. A bar is 1, 3, 6 or 12 hours, or 1 or 7 days (at most 120 bars).
  • Group by project (the last part of the folder), computer, user, model, issue, branch, main or subagent, or subagent type. Number: output, cache read, cache write, input and thinking tokens, requests, tool calls, tool errors or model time.
  • Pie charts of the sessions and of the output tokens (B104, B105, B107). Each grouping has two donut charts: the number of sessions and the output tokens (the tokens count each hour on its own). A slice shorter than the 2 px gap keeps a small visible length (pieArcs): a negative length made the browser draw one full ring. The text below describes the sessions chart. When Group by is issue, it shows the sessions with an issue, with only a guess (B103) and with neither. A session has an issue when one of its rows in the range names an issue. For the other groups (for example model or user) a session counts once, in the group with most of its requests, so the slices add up to the total. The colors follow the group, and the groups after the seventh fold into "Other". A table gives the sessions, the share, the requests and the output tokens of each slice (the requests and tokens are those of all groups of the session).
  • The colors follow the group, not its rank: the order comes from the preset range, so a zoom never repaints a group. The first 7 groups have a color, the others are "Other". The colors pass validate_palette.js in light and dark mode. The hover shows every group of a bar, and Table view shows the same numbers as a table.
  • Tables. Totals by group (sums, share of the output), the comparison of the models (output for each request, thinking share, cache read for each request, tool error rate, seconds for each request and output tokens for each second) and the largest sessions. A column sorts on a click; a table shows 25 rows until Show all. The comparison shows size, speed and tool errors, not the quality of the work: the tasks differ.

The viewer role: the global admin reads every table (B113)

The role kd_viewall is the global admin role. A person who holds it reads EVERY table of every company (read only: it has a SELECT grant and no other right). company-cli.ts <env-file> grant-viewall <email> gives the role to a person and syncs the grants. The sync (CompanyAdmin.syncViewAll) lists the tables of the server (SHOW TABLES) and grants SELECT on each one, so a table that is made later is covered too: addCompany runs the sync, and a company-cli.ts command that makes a table (CREATE TABLE) runs it at its end. viewall-sync runs it by hand. The role writes only through its own company role.

Trace (B14)

The table kd_trace holds a trace of what happens on each machine, so that one query shows a chain across machines. Make the table once with bun daemon/company-cli.ts <env-file> trace-table. Each person reads, writes and removes only the own rows. The daemon removes rows older than 7 days.

The fleet page (/) has a Trace view. It reads the rows of the last hour. A feed of the 30 newest events comes first. Below it, one small table has a row for each computer: how many asks of the engine came, how many a hook allowed (auto-approve) and how many reached you, the sounds and the time of the hook programs. A watch on kd_trace updates the view while it is open.

  • The daemon writes its own events: choice-changed (with the time from the click to the daemon), pull and hooks-file.
  • The mod and the hook scripts have no database login. Each drops a small file into ~/.config/kdmem/trace/; the daemon ships the file after its pass and removes it. The mod writes hook-run (the duration of a hook program), files-changed and check-ask (each ask of the engine, the final verdict and the number of loaded hooks); play.ts of the package sounds writes play-start, play-spawned and play-skipped. On Windows and WSL the PowerShell script of the player also writes script-begin, player-created, sound-started (the sound really plays) and sound-ended: the time between play-spawned and sound-started is the start time of the player.
  • Read the rows: bun daemon/trace-cli.ts [minutes] [text]. Each row has the machine, the source, the event, a time in ms and a text. The time of a row is the clock of its machine.

Messages between Claude sessions (B14)

The mod gives each Claude session two tools: peers (the other sessions) and send (a message to a session by name or session id). It gives a third tool, context: it tells Claude the tokens used, the window size, the percent and the tokens left. Claude cannot see this itself. A message reaches an idle session: the mod wakes it with a new turn that names the sender and the reply address ([kdmem message from NAME (session ID).]). The frame tells the model nothing about how to treat the text (B95, owner 2026-10-20: the old words "not the user" and "a request from a teammate" made sessions ignore the orders of the owner's own sessions).

  • One computer. The mod writes ~/.config/kdmem/msg/outbox/<id>.json. The daemon routes it into msg/inbox/<session>/. The mod reads the inbox every 2 seconds and writes an .ack file.
  • Two computers of one user. The table kd_messages is on by default. The daemon sends a message for another computer as a row of the table and delivers the rows for this computer. A message that cannot be delivered comes back as a message from kdmem with the reason. "messageTable": "" in ~/.config/kdmem/device.json turns it off; another name picks another table. A server with no table logs the error once. One company. The sessions of a company see each other and message each other. peers lists the sessions of every person of the company (the table fleet_agents, by company), and the file claims (fleet_files) of every person count too, so two people do not change the same file. A message to a session of ANOTHER person carries two more columns, company and to_owner. The company role reads and marks the rows of its own company; the daemon of the target takes only a row with its own user in to_owner and the same company, and checks both again. A computer is named by its owner AND its name: two people can have a computer called nixos. Every message of a device with a company carries company, so the Messages view of the page shows all messages of the company to its members; message-company also gives the old rows the company of their owner (run it again after this release). A message never crosses to another company. A device with no company works as before: only the sessions of the own user. Run bun company-cli.ts <env-file> message-company ONCE as root, BEFORE the release: it adds the columns and the company grants (run it again for a new company). A message row is NOT signed.
  • The table. Make it once, as root (daemon/messages-remote.ts names the columns): kd_messages with mkey (key), owner, from_machine, from_session, from_name, to_machine, to_session, body, at, delivery, note, and GRANT INSERT, UPDATE, DELETE, SELECT ... TO MATCH owner. The server refuses text and status as column names.
  • The page. The fleet page has a Messages view: all messages, grouped by pair (the default), session, computer, project or user. A pair is ONE card for two sessions, with both directions in time order. The card with the newest message is on top. Each session row shows how many it sent and received, with a button to its messages.

The fleet page and a text selection (B94)

A redraw of the page ends a text selection: the page rebuilds the board when a field of a session changes, and the clock cells change their text each second. So the redraws that nobody asked for (the timer, the data refresh, the refresh of the session actions) wait while the person has text selected inside the page. They go through renderLive in kdmem/site/index.html. When the selection ends, the page draws the newest data. A redraw that a click asks for (render) is never held. While text is selected the page shows the data of that moment: the clocks stand still.

The chat of a session on the fleet page (B14)

The Sessions view has a Chat button on each session of your own user. It opens the chat of that session: the text of the turns, a small chip with the NAME of each tool call (never a tool input or a tool result), and a box to write to the session.

  • How it works. The page keeps a row in kd_chat_open alive (a heartbeat of 20 s; a row lasts 60 s). The daemon of the machine reads the rows and writes chat-open.json. The mod of that session writes one file for each chat line into <config>/chat/ ONLY while its session is on that list; the daemon ships the files into kd_chat and removes them. The page watches kd_chat. The text of a session reaches the database only while a person has its chat open, and the daemon removes lines older than 3 days. When a chat opens, the mod also writes the last 40 lines of the transcript (between two marks, earlier messages and live; they have no real time). Speed. The mod has no file watch, so it reads chat-open.json every 500 ms. The daemon watches <config>/chat/ and wakes at once (after 250 ms of quiet, 1 s at the latest) to ship the files, so a chat that opens fills in about 1 to 2 s. Before B14 part 38 it waited for the 2 s read and the 5 s pass.
  • Writing to the session. The page inserts a row into kd_messages with from_machine = 'web' and from_session = 'web'. The daemon of the target machine delivers it as for any message and sets web: true in the inbox file, and the mod submits the text as the user. Only such a row is the person. A message of a session that only says from: "web" is refused (outbox-bad) or shows as a peer message. A text of the page that starts with a slash (/compact, /model sonnet) runs as a slash command ($.command.run, queued until the session is idle), because a prompt that a plugin submits is only text for the model. The chat shows kdmem: /compact finished or the reason that it did not run. A message of another session that starts with a slash stays a peer message and runs nothing.
  • Send now. A normal text of the page waits until the turn of the session is done ($.prompt.submit runs "once the session is idle"; measured: 43.5 s behind three 12 s tool calls). The check box Send now (off after each send) writes priority = 'now' into the row. The daemon sets urgent: true in the inbox file, for a row of the page only. A busy session (the mod follows turn.start and turn.complete of the main loop) keeps the text and adds it as the context of the next tool result of the main loop: the model reads it after the running tool, before the end of the turn (measured: 8 to 14 s earlier). A text that no tool result carried goes out as a normal prompt when the turn ends; an idle session gets it at once. The chat shows kdmem: your urgent message reached the model at its next tool call. It cannot interrupt a tool that runs: the model reads the text after that tool. The column is added once, as root: ALTER TABLE kd_messages ADD COLUMN priority TEXT (a table without it still works: the page adds the column only for a Send now text, and the daemon reads all columns). $.session.send to the own session does not work (the engine says "no live session"); $.session.append also reached the model in the probe. A session that can run commands can write any file of your user, so this is not a wall against a session that is already out of control.
  • The tables. Make them once, as root: bun daemon/company-cli.ts <env-file> chat-table (kd_chat and kd_chat_open; the grants TO MATCH owner limit each person to the own rows). The column is body, not text (the server refuses text and status).
  • The trace. chat-open, chat-close, chat-ship (daemon), chat-open, chat-close, web-message (mod) and chat-open, chat-close, chat-send (source page) go into kd_trace: read them with bun daemon/trace-cli.ts 10 chat.
  • An old mod shows no text (the page says so after 15 s): run /reload-plugins in the session.
  • The time of a line (B92). A line that the mod writes while the chat is open has its own time. The history that a session sends when the chat opens has NO time in the session ($.session.messages() gives the role and the text only). The mod writes the history as ONE file, the lines with the flag h, and the last 100 lines of text (the tool chips do not count). The daemon reads the end of the transcript file of the session, matches each history line from the newest to the oldest (same role, same text), and writes the time into the column ts of kd_chat. A line that it cannot match has no time. A line from another day shows its date. Root adds the column once, before the new daemon runs: bun daemon/company-cli.ts <env-file> chat-time-column. A table with no column still works: the daemon writes the line with no time, and the page reads it without the column.
  • Share (B92). A Share button sits next to Chat on each session of the own user, and in the chat view. It makes a link to the last 100 lines of the text of the session. Tool calls are left out. A session ships its text only while a page has its chat open, so the button opens the chat: the mod then sends the history by itself, and the link is made when the history has come (it waits 30 s at most). The page encrypts the lines in the browser (AES-GCM) and writes the cipher into kd_share; the key is in the part of the link after #, which the browser never sends to the server. Only the owner writes and removes a row (TO MATCH owner). Every signed-in user may read a row (TO AUTHENTICATED; the role kd_member is not on every account), but needs its id, and sees only a cipher without the key. A signed-in person who opens /?share=<id>#<key> sees the lines, read only. The row ends after 7 days; the daemon removes it. A person who has the link can pass it on: the link is the right to read. Make the table once, as root: bun daemon/company-cli.ts <env-file> share-table.

Start a new session from the fleet page (B14)

The Sessions view has a New session button. The form asks for a computer, a folder and a prompt. The computer and the folder are toggle buttons: one of each is pressed at a time. The form reads the computers again when a daemon writes its folders or a heartbeat changes. The daemon of that computer starts claude --bg in that folder with your prompt as the first message.

  • The folders are the folders of the Scope (B92). No command is needed. In mode only the daemon offers the folders of the Scope tab. In mode all or except it offers the project folders that the sessions used (in scope; read at most each 5 minutes). A request starts a session ONLY when its folder is in that list (an exact folder; no parent, no child, no ..). A folder that you add or remove in the Scope tab counts at once. device.json may add more folders (bun daemon/spawn-cli.ts add /work/a) or turn the start off for this computer ("spawn": { "on": false }, or bun daemon/spawn-cli.ts off; on turns it back; show and remove <folder> also exist). The daemon reads device.json and the Scope again on each pass.
  • Windows: the spelling of the folder. The Scope keeps a Windows folder in lowercase (c:/users/skade/src/kinedb). Claude keeps its trust of a folder under the exact spelling, so a session that starts in the lowercase folder asks "Is this a project you trust?". A background session cannot answer, and the mod did not load there (no tools, no trace row). The daemon now starts the session in the spelling of the disk (C:\Users\skade\src\kinedb) and compares Windows folders without case and with any slash. A path of another system stays case sensitive.
  • The session. It runs in the normal permission mode (--permission-mode default), so it asks before it runs a tool. The auto-approve hook is not part of the start. The prompt is ONE argument after --: no shell reads it. The daemon gives the child no CLAUDE_* variable of the session that started the daemon (it keeps CLAUDE_CONFIG_DIR).
  • Limits. At most 5 starts in 10 minutes for each computer. A request that waits more than 10 minutes expires. A prompt has at most 8000 characters.
  • The flow. The page inserts a row into kd_spawn (state new). The daemon wakes at once (a watch), sets starting (the claim; a second pass finds no new row), starts the session and writes started with its id, or refused, failed or expired with the reason. The daemon also writes the allowed folders into kd_spawn_host; the form lists only the computers that sent a heartbeat in the last 2 minutes.
  • The tables. Make them once, as root: bun daemon/company-cli.ts <env-file> spawn-table (kd_spawn, kd_spawn_host; the grants TO MATCH owner limit each person to the own rows).
  • The trace. spawn-request (source page) and spawn-start, spawn-refused, spawn-failed (daemon): bun daemon/trace-cli.ts 10 spawn.
  • No process in the mod. Only the daemon starts the program (one spawn( in daemon/spawn.ts, pinned by a test).

Start a session from a session (B92)

A Claude session starts a NEW session in a folder, on its own computer or on another computer of its owner. This is the New session button of the fleet page, as two tools of the mod:

  • spawn_targets lists the computers of the owner and the folders that each computer allows (the Scope folders, see above), and marks this computer and an offline computer.
  • spawn with folder, prompt and an optional machine (the default is this computer). It returns at once with queued. The result comes back as a message from kdmem: the id of the new session, or the reason. The new session then shows in peers.

How it works. The mod writes files only, in <config dir>/spawn: out/<id>.json is a request, targets.json is the list that the daemon writes. The daemon (daemon/spawn-api.ts) checks the request (a computer that is online, a folder in its list, a prompt of 1 to 8000 characters), inserts the same row as the page into kd_spawn (the owner is the user of the daemon, never a value of the file) and tells the session the result. The daemon of the chosen computer starts the session with its own checks (daemon/spawn.ts): an exact folder of its list, the normal permission mode, at most 5 starts in 10 minutes. At most 20 requests of one computer wait for a result; a request that gets no result in 15 minutes is answered with that fact. A computer with an old daemon offers no folder, so a request for it is refused with the list of the allowed folders. The trace shows spawn-api (daemon) for each stored request. A change of the mod works in sessions that start after it, or after /reload-plugins.

Stop, start and delete a session (B92)

The fleet page and the mod can stop a session, start a stopped session again, and delete a session that does not run (owner 2026-10-20).

  • The fold (B110). In each group of the fleet page the live sessions show first. The sessions with no process are in ONE folded line below them (▸ 12 sessions not running). The fold is closed at first. A click opens it, and the page keeps the choice for each group in the browser.
  • The list shows all sessions. The collector also lists the sessions with no live process: it reads ~/.claude/jobs/<id>/state.json (files only, no program start). They have the status offline and the state of the job (stopped, done). The newest 50 show. The page chip shows one word (stopped, done; B108), and the peers tool says not running (stopped). The model, the id line and the chip stay on one line (B108). Only the id, the name, the folder and the state leave the computer, never the text of the job. A message to a session that does not run waits until it runs again.
  • The page. On a session of the own user: Stop on a running session; Start and Delete on a session that does not run. Delete asks twice. Another person's session has no button. The page writes a row into kd_session_act; the result shows below the buttons. The buttons of one session (the message count, Chat, Share, Stop, Start, Delete) are always in ONE row (B109, sessionCell).
  • The mod tool session with action (stop, start or remove) and session (an id from peers). It returns at once; the result comes back as a message from kdmem. It works on any session of the same owner that the fleet rows list, also on another computer that is online. It refuses a session that is not listed, an offline computer, and the session that calls it.
  • What runs. The daemon of the computer of the session (daemon/session-act.ts) runs claude stop <id>, claude respawn <id> or claude rm <id>: one program start in that file, no shell, the id is 8 hex characters. A running session cannot be deleted: stop it first. remove deletes the session, its history and its worktree. It cannot be undone. The daemon never passes a flag that discards unpushed work: claude refuses, and its text comes back to the caller.
  • The table. Make it once, as root: bun daemon/company-cli.ts <env-file> session-act-table (kd_session_act; the grants TO MATCH owner limit each person to the own rows). A row older than 3 days is removed. The trace shows session-api and session-act.

Compact a session by itself: the tool bettercompact (B92)

Claude calls the mod tool bettercompact (no arguments) when its context is nearly full, or when the owner asks (owner 2026-10-20). The mod does the steps that a model cannot do for itself:

  1. The RESULT of the tool is the compact prompt (compactInstruction in hooks/compact-core.js; the words come from the old prompt hook of the package arc, no word changed; owner 2026-10-20), with the path <temp folder>/claude-compact-<session8>.md. The mod sends no prompt for it. Claude writes ONE dump file and tells the owner the path. The hook of the package arc no longer adds this text for a typed prompt with the words "prepare for compact": it fired also when the owner only talked about the words, and never on a tool result. The SessionStart hook of the package still reads the dump file after the compact; its path must stay the same as dumpPath. The mod accepts any path that fits step 2.
  2. The mod watches the finished Write, Edit and MultiEdit calls. A path with the word compact AND a folder named tmp or temp (any case; /tmp/... on Linux, ...\Temp\... on Windows) is the dump file. The temp folder of macOS is /var/folders/xx/yyyy/T/, with no folder named tmp or temp: a path with the word compact inside the temp folder that the tool used (TMPDIR) also counts. A sibling folder does not. The mod never looks for a sentence or an exact name, because Claude changes both (hooks/compact-core.js). A write before the tool call and a failed write do not count.
  3. When the turn ends, the mod runs $.session.compact(), the same call as /compact. The first try is made AT the end of the turn, with no wait. The call rejects while a turn runs, and a busy session (a fleet leader) starts the next turn 50 ms after the last one ended. A wait before the first try (the old design: 0.5 seconds) then never found a gap (live failure of a leader, 2026-10-20). If a try fails, the mod tries again 20, 100, 250, 500, 1000 and 2000 ms after the turn end while no new turn starts. When a new turn starts, the next turn end tries again. After 40 turn ends, or after 2 seconds of quiet with every try rejected, the mod gives up: the approval switch goes off and a message tells the person to type /compact.
  4. After the compaction, the mod submits compact completed as the words of the owner (no frame). A compaction that a hook vetoes sends nothing. The text is a normal prompt: it waits if a turn runs.

The approval switch. Not everyone has the package auto-approve, and the write of the dump file and the delete are outside the project folder, so the session would ask. The mod has its own tool.check hook with a switch, off by default. The tool turns it on: every tool call is allowed until the compaction ends. Then the switch goes off, and exactly ONE tool call is allowed after compact completed (Claude deletes the dump file then). The allowance belongs to the id of that call, so a second check of the same call passes and the next call asks again. If Claude reads the file before it deletes it, the Read takes that one call and the delete asks. If Claude never writes the dump file, the switch stays on.

Live test 2026-10-20, in tmux, with a separate test mod before it came into kdmem, in default permission mode and with only call bettercompact sent: the tool result was the prompt, Claude wrote the dump file, the mod compacted, Claude received compact completed, read the file and deleted it, and the session asked no permission question. A submit from inside the tool call started NO turn (checked live). Tests: kdmem/mod/tests/compact.test.ts, kdmem/daemon/mod-compact.test.ts.

Files that sessions edit (B14)

The mod records each file that a session edits (the tools Edit, Write, MultiEdit and NotebookEdit) as a claim. A claim has NO time limit: it counts while its session runs. A session that ends removes its claims. A computer that sends no heartbeat for 2 minutes (the lid is closed, no network) gives no claim: its state is unknown, so the edit is allowed, and we accept that risk. A pull request that stays open after its session ended is not visible to kdmem; git shows it at the merge. The daemon sends the claims to the table fleet_files and gives the claims of all computers of the user back to the mod. It writes files-all.json at least once a minute; the mod ignores a file that is older than 3 minutes (a dead daemon), so a dead daemon never blocks an edit.

  • A read or a search. When Claude reads a file, or runs a Bash, Grep or Glob call that names a file that another live session edited, the mod adds a note after the result. The note names the session and the computer, and tells Claude to use the send tool. A function is not tracked: only a file.
  • An edit. The first edit of such a file is refused. The refusal gives the same note. The same edit again passes: Claude has then agreed with the other session, or it has decided to go on. The mod never blocks twice.
  • The same file. Two claims are the same file when the path is equal, or when the repository and the path under its root are equal (another computer, or a worktree).
  • The files. ~/.config/kdmem/files/<session>.json (the mod writes it) and files-all.json (the daemon writes it).
  • The table. bun daemon/setup-fleet.ts makes fleet_files (the columns are in that file) and the grants, as it does for the other two.
  • The page. The fleet page has a Files view, grouped by session, computer, project or user. A file that two sessions edit has a shared with N chip. A user who sees more than one company gets a list in the top bar to pick the company to watch.
  • Companies. fleet_files is a fleet table: it has the columns company and dept, and a company reads and writes only its own rows.

Where kdmem works: the scope of the projects (B14)

Each person decides in which projects kdmem works, on each computer. The default is all projects. The choice is the file ~/.config/kdmem/scope.json. Change it with the daemon file (bun "$(ls -t ~/.config/kdmem/code/agents-*.js | head -1)" --scope …):

  • --scope only <folder…>: kdmem works ONLY in these folders and the folders below them.
  • --scope except <folder…>: kdmem works everywhere EXCEPT in these folders and the folders below them.
  • --scope add <folder…>, --scope remove <folder…>, --scope all, --scope show. A . is the folder where you type the command.

With the mode only, a session that starts in a folder that is not in the list asks once (the question dialog of Claude Code): "The question: kdmem is off in , because the folder is not in your scope. Allow kdmem to work here?" (it starts with "The question:" because the gate of the template denies any other question) Yes adds the folder, and the root of its project, to the list (--scope allow <folder>), and kdmem works in that session. No keeps the folder in scope-declined.json (--scope decline <folder>): kdmem does not ask again, and --scope allow turns it on later. Not now, a dismissed dialog and a run with no person (claude -p) write nothing, so the question comes again. The mode except never asks: the person left the folder out on purpose.

A new install starts closed (B117). At its first start the daemon writes {"mode":"only","folders":[]} when the config folder has no scope.json and shows no earlier run (no heartbeat.json and no assets-version.json). Claude Code was used in many folders before kdmem, and none of them was asked about, so every folder asks. Only an OLDER install (one of those two files exists, but no scope.json) gets the folders that it has seen, so that no session stops working. The daemon log says which case it was.

The migration never writes the home folder, the root of a drive, / or a folder of ~/.claude: a session that started there is not a project, and the home folder would put every folder below it in scope. The first release wrote the home folder on some computers, so the daemon removes it once (repairScope, marker scope-repaired.json) and adds the project folders that it has seen. A person may add a base folder again on purpose.

The trace row scope-check (B92). At the start of each session the mod writes one trace row, also when kdmem is off in that session. The text holds the session, the state that the mod believes (in scope, OUT of scope, the mod asks, OUT of scope, no question), the folder exactly as the engine gave it (in quotes, so an empty folder or a \\?\ prefix shows), its normal form, the mode and the number of folders. Search the Trace tab for scope-check, or run bun daemon/trace-cli.ts 10 scope-check. The row is written before the question of the mod, because a background session has nobody to answer.

The Scope tab of the fleet page shows each computer of the person: the mode, the folders (Remove), the folders that the person declined (Allow) and a field to add a folder (mode only). It also shows the patterns of the company, read only. The tab reads the table kd_scope (one row for each person, computer and folder; the grants TO MATCH owner limit a person to the own rows, so no signature is needed). The daemon of the computer publishes the rows at each pull. A click on the page writes a wish row (add or remove); the daemon applies it to scope.json and scope-declined.json and replaces the row (scope-sync.ts). Until then the row shows "waiting for the computer". The admin makes the table once: bun daemon/company-cli.ts <env> scope-table.

A session out of scope does nothing: no tools (send, peers, context), no chat, no file claims, no hooks of the database, no usage file, and the daemon does not list it (no name, folder or context leaves the computer). The mod decides ONCE when the session starts, by the folder of the session. Start the session again after a change. The skills, commands and prompts of the packages are files for the whole computer: scope does not hide them.

The company admin names patterns of projects that are ALWAYS included: bun daemon/company-cli.ts <env> scope <company> kinedb billing (unset removes them). A folder with a pattern as a part of its path (no case) is in scope for every member, and a member cannot leave it out. The row is signed with the company key (scope.json in the layer); the daemon writes it to ~/.config/kdmem/scope-company.json. --scope show tells it.

Commands, hooks and prompts from the database (B14)

Three kinds of row go to the mod: commands/<name>.md (a slash command), hooks/<name>.json (a hook) and prompts/<name>.md (text for Claude, like a CLAUDE.md file). The daemon writes NO file for them: not into a repository and not into ~/.claude. It writes ~/.config/kdmem/commands.json, hooks.json and prompts.json. The mod reads them and checks them again every 15 seconds. A change so works in a session that is already open, with no restart.

Global and project layers. A row projects/<repo>/commands/<name>.md (also hooks/ and prompts/) belongs to ONE project. <repo> is the repository name that the fleet page shows. The mod learns the project of its session from files-all.json. In a session of that project the project entry REPLACES the global entry of the same name. In every other project the global entry works. A hook of the project {"off": true} switches the global hook of that name off in that project. Until the project is known, only the global entries apply.

Hooks are data. A mod cannot load code from the database (on needs a function literal, and eval is blocked), so a hook row holds a rule:

Field Meaning
event tool.call, tool.check, prompt.submit or turn.complete.
tool A tool name or a list (tool.call and tool.check). No tool means every tool.
except A list of tool names that the hook does not cover (tool.call and tool.check).
ask allow or deny (tool.check only): the answer when the engine says ask. A deny of the engine never changes. A tool that needs the person (AskUserQuestion, a plan) keeps its dialog.
match A regular expression for the subject: the command (Bash), the path (Edit, Write, Read), the pattern (Grep, Glob), the prompt text, or the answer.
deny tool.call refuses the call with this text. prompt.submit drops the prompt.
note tool.call adds the text after the result. prompt.submit adds it as context.
run An argv list, never a shell line. {subject} in an element becomes the subject, {kdmem} the folder ~/.config/kdmem. Exit code 2 denies with the message. Other output is a note. In tool.check it runs when the verdict is ask (the person is needed); nothing waits for it and its output is ignored.
prompt turn.complete starts one follow-up turn with this text. That turn runs no hook, so there is no loop.
protocol "claude" (needs run): the program speaks the hook protocol of Claude Code, so a gate.ts of a project runs as it is. It gets the event as JSON on stdin (PreToolUse, PostToolUse, UserPromptSubmit, Stop, SessionStart, with session_id, cwd and the fields of Claude Code) and runs in the folder of the session. It answers with JSON (permissionDecision deny or ask = refuse with the reason; decision: "block"; additionalContext) or with exit code 2. A Stop block starts one follow-up turn with the reason. Another exit code or a crash never blocks the work.
after true (tool.call with protocol: "claude"): the program runs AFTER the call, as PostToolUse, and also gets tool_response. Its context goes after the result.

event: "session.start" exists for protocol: "claude" only (SessionStart, source is always startup, because the mod cannot tell a start from a resume or a compact). The program runs once at the start. Its context goes with the first prompt of the session.

A hook in a sub agent works too: the sub agent has the session id of its parent and an extra agentId (probe 2026-10-18). The hooks of tool.call run for it. A turn.complete hook ignores the turn of a sub agent. The hooks of the mod run BEFORE the hooks of settings.json. They cannot remove a hook that a repository defines in its own .claude/settings.json.

Prompts. The first prompt of a session carries the visible prompts as context. A prompt that changes (or appears, or goes away) WHILE the session runs reaches the session as a message from the mod, once, within about 15 seconds. The text is not repeated after a /compact: the session keeps the message in its summary only if the summary keeps it. A sub agent does not get the prompts (NOT DETERMINED).

Commands. A row commands/<name>.md becomes a slash command /<name>.

  • The text. The body is the prompt. $ARGUMENTS takes the text after the command; a body with no $ARGUMENTS gets it at the end. A frontmatter block may give description: and argument-hint:. Without a description the first line of the body is used.
  • A run. The mod queues the prompt and submits it as if the user typed it. A timer of 0.5 second submits it, because a direct call in the handler started no turn (probe 2026-10-18).
  • A change or a removal. A changed body counts at once (within 15 seconds). Claude Code cannot unregister a command: a removed command stays in the list and answers that it was removed, until the session ends. A changed description shows after a restart of the session.
  • The name. Letters, digits, _ and -, up to 64 characters. A name of a built-in command is refused by Claude Code and skipped.
  • Old files. A command file that an older daemon made is removed once (a file that the user edited stays).
  • Skills are not changed. A skill is a prompt that Claude follows with its own files, so it stays a SKILL.md file.

Packages (B14)

A package is a named set of signed rows packages/<name>/…: package.json (description, and "enforce": false when no company may enforce it), skills/, commands/, hooks/, prompts/ and files/. The daemon turns the rows of a package that is ON into normal rows: skills/ and commands/ keep their name, a hook or a prompt gets the name <package>-<name>, and a files/<f> row goes to ~/.config/kdmem/packages/<package>/<f> (never into a repository or ~/.claude). A normal row wins over a package row of the same path. The daemon never overwrites or deletes a file in that folder that it did not write, so the .mp3 files of the user stay.

Who decides. The company sets the state of a package in its signed row packages.json, for example {"sounds": "on"}: enforced (on for everyone), on ("default on": on, the person can turn it off) or off ("default off": off, the person can turn it on). A package with no company setting is off. The person chooses on the Packages view of the fleet page: each package has a switch (and "Use the company setting"). The page writes the row <user>|<package> of the table kd_package_choice (owner, pkg, choice = on or off; the server limits the rows by owner). The daemon watches the table, keeps the own rows in ~/.config/kdmem/packages-choice.json, and applies them on every computer of the person. The choice in device.json ("packages": {"sounds": "on"}) still works; the choice of the page wins over it. The choice counts only when the company did not enforce the package. A package with "enforce": false (auto-approve) is never enforced: enforced then counts as on. The last good company policy stays when the company layer is refused.

The company state is set by a company admin on the Packages tab of the /manage page (the fleet page shows it read only). The key never goes into a browser, so the flow has four steps: (1) the page calls the admin service (/api/package-request, a token of an admin of that company); (2) the service stores one row for each company and package in the table kd_company_request (only the service writes it; a member reads the rows of its company); (3) the daemon on the computer that holds the company key (a key file whose public half equals the key that the device pinned: company-<company>.key.pem, or for a company made by a signup the first device key) watches the table, builds the whole new layer (the other rows stay, the version rises) and signs it; (4) it sends the layer to /api/publish-layer. That call needs no token: the service checks the signature against the stored company key and accepts a higher version only. /manage shows "Waiting for the key computer" until the layer shows the request, and has a Refresh button. If no computer with the key is on, the request waits. Make the table once with bun daemon/company-cli.ts <env-file> request-table (a new company gets its grant when it is made). The operator can still set a state by hand with bun daemon/company-cli.ts <env-file> package <company> <name> enforced|on|off|unset, on the computer that holds the key. Make the personal table once with ... package-table.

A package of one company only (a custom package). A package in kd_assets is visible to everyone. A company can put a package into its own signed layer instead: bun daemon/company-cli.ts <env-file> package-publish <company> <name> <dir> reads the folder <dir> (the same layout as kdmem/packages/<name>/) and writes it as company/<company>/packages/<name>/…. It replaces the stored rows of that package and keeps the other rows and the state row. Only the members of the company get the rows. The Packages page lists the packages of kd_assets only, so set the state of a custom package with ... package <company> <name> enforced|on|off|unset. The source of such a package stays in the repository of the company, not in this repository. Run it on the computer that holds the company key.

A custom package extends; it does not replace. Hooks of two packages both run (each has its own name, <package>-<hook>), and prompts add up. Two skills with the same name are one file: a normal row wins, then the first package row, and nothing is logged. So a custom package does NOT ship its own arc or fleet skill. It ships a prompt (for example <project>-arc) and hooks. The generic skills arc and fleet have a section "Extensions of the project" that tells the model to follow such a prompt; where a project rule and a rule of the skill conflict, the project rule wins. A hook of the company that has the same name as a generic hook replaces it in a project layer, and {"off": true} switches it off.

The packages of this repository are in kdmem/packages/: auto-approve (the hook tool.check that allows every ask, except AskUserQuestion, ExitPlanMode and EnterPlanMode) and sounds (play.ts plays a random .mp3 of the folder when Claude is done or needs an answer; a Mac uses afplay, WSL and Windows use PowerShell, Linux uses ffplay) and arc and fleet (below). Publish with bun daemon/assets-cli.ts <env-file> publish kdmem packages.

The package arc (how we work). It holds the generic form of the way we work in kinedb: test first, the issues and merges through forge.ts, a local gate, hooks that enforce the rules, and the /arc skill. It needs bun and git. A project sets the tracker, the id prefix and the gate steps in its own .claude/project.json.

The tracker of a company needs no tool of its own. Set tracker (kind, url, repo) and idPrefix in project.json; forge.ts then allocates the ids (with a re-check against a second session) and checks the PR body. Three additions serve a company. forge.ts issue find <text> lists the OPEN issues whose title holds the text. forge.ts pr state <branch|n> prints open, closed or merged. The setting "pr": {"bodyLines": ["Direction"]} makes pr create refuse (exit 5) a body that closes an issue and lacks a Direction: … line. A line can name paths, like the Docs line: {"name": "Direction", "paths": ["^tasks/direction\\.md$", "^tasks/direction\\.d/"]} refuses Direction: none when the branch changes one of them, and a named answer needs such a change. pr merge checks the body again, with the body and the files from the forge, because someone may edit the body after pr create. pr create also keeps a closing keyword on a line of its own (Closes #N), because Gitea closes or reopens an issue on a keyword and #N anywhere in the body, also after "does not close #3". Docs: none needs a reason of three words or more. With "pr": {"docsPaths": ["^docs/", "^web/docs/"]}, the Docs: line names only docs files that the branch changes, and none is refused when the branch changes one. The changed files are those of the branch against origin/<main> (the local main of a checkout is often old). The merge policy is a setting too: "pr": {"mergeCheck": ["sh", "{packages}/kinedb/scripts/pr-job.sh", "{n}"]} makes pr merge run that command INSTEAD of looking for a local gate PASS (exit 0 merges; any other exit code refuses the merge and is kept; {n}, {head}, {branch} and {packages} are filled in). "uids": {"U": {"dir": "docs/upgrade-paths.d", "pattern": "^U(\\d+)\\.md$"}} names a folder of numbered files, and forge.ts uid next U prints the next free number (U8) over main and every branch or open PR that is not merged, so that two parallel arcs never take the same number.

Row What it is
prompts/rules.md The rules (CLAUDE.md in a generic form). The first prompt of a session carries it.
skills/arc/ The /arc <id> skill and its TRAPS.md.
hooks/gate-*.json Five hooks with protocol: "claude": PreToolUse (Bash, Edit, Write, MultiEdit, AskUserQuestion), PostToolUse (Bash), UserPromptSubmit, Stop and SessionStart. Each runs hooks/gate.ts.
files/hooks/ gate.ts (the rules: a worktree for each arc, no commit on main, explicit git add, a commit that names its issue, protected paths, the question first, a waker for a promised action, the compact dump, and the owner question: issue answer must quote words that a human typed into THIS session, the label owner-question moves only through issue ask and issue answer, and a question that names an issue needs a prior issue ask).
files/scripts/ forge.ts (issues and merges for a local, Gitea or GitHub tracker), gate.ts (the local gate). The texts of the gate name these scripts with their full path in the package folder.

The tests of the package are in kdmem/package-tests/arc/ (not in the package folder, because publish sends every file under it). bun test kdmem/package-tests/arc/ runs them. The mod cannot tell a start from a compact, so gate.ts finds the compact dump file of the session. A project that wants more rules (kinedb itself) adds its own hooks, skills and prompts as rows of its project layer. A row of the project replaces a package row of the same name, and {"off": true} switches a package hook off in that project.

The package memory (the notes of a project). A project keeps its lessons as notes: memory/ holds the rules and archive/ holds the record of each arc. Each note is a Markdown file with frontmatter (description, component, kind, severity). The package delivers the notes that fit the work. It needs bun and git, and it works only in a project that has .claude/kdmem/config.json (the components, the aliases and the triggers of the project). It does not need the package arc. Until now these rules were a part of arc; a company that uses arc must set memory to on to keep them.

Row What it is
hooks/gate-*.json Three hooks with protocol: "claude": PreToolUse (Bash, Edit, Write, MultiEdit), PostToolUse (Bash) and UserPromptSubmit. Each runs hooks/gate.ts.
files/hooks/gate.ts The rules. A note with a bad frontmatter is denied, with the template. The first edit under a component in a session is denied once, with the rules of that component. A command that matches a trigger gets a note. After a new commit comes the reminder to write an archive note. Each prompt gets the notes that fit its ids, components and words; a prompt that a machine wrote gets none.
files/recall/kdmem.ts The store of the files and the recall. It has a command line: check, recall, gate, search and stats.

The tests are in kdmem/package-tests/memory/. Today the notes are files on disk. There is no local database any more.

The notes on the kdmem server (B92, in progress). The table kd_memory holds one row for each VERSION of a note: the whole text of the file (frontmatter included), the project, the id (the path of the file), the version, the sha, the author and the dates. A company reads and adds rows; the grants hold SELECT and INSERT only, so a version is never changed or removed. Every member of a company reads all its notes. The name of the project (the folder of the main checkout) is the namespace of the ids, so two projects may both have memory/never-assume.md; the project "" is for the whole company. The operator makes the table once, and add-company does it for a new company: bun daemon/company-cli.ts <env-file> memory-table.

Seed the notes (once). bun daemon/memory-cli.ts seed [<project-dir>] sends the notes that git tracks today (a file that git ignores or does not track is a private working file, for example a session handoff, and is never sent; --include-untracked sends it). It reads the folders of .claude/kdmem/config.json, or of --config <file> (the older names seedRoots and seedExclude work too). It signs in with the key of the device, as trace-cli.ts does, and uses the company of the device. --dry-run counts and writes nothing. A note that the server holds is skipped, so the command is safe to run again, for example after a restore of the database; a changed file becomes the next version. A note whose frontmatter recall does not accept is reported and sent too: nothing is lost. memory-cli.ts list [<text>] shows the notes of the company, and get <id> prints the newest text of one note. The code is in daemon/memory.ts, the tests in daemon/memory.test.ts.

Work with the notes (B92). memory-cli.ts search <words> finds the notes of all projects whose id or text (any version) has the words, and prints the matching lines of the newest version (--limit <n>, --project <name>). list --all lists all projects. The skill /kdmem tells Claude how to answer a question from the notes: search with several words, read the whole note, answer from the text only, name the note, say when two notes disagree, and say "no note answers this" when none does. The fleet page / has a Memory view: the list of the notes (one project or all), a search on the server (id and text) that runs while you type (250 ms after the last key, 2 letters or more; a search takes 150 to 320 ms; an old answer never replaces a newer one), the versions of a note (newest first) and the text of each, Save as a new version and Remove the note (a new version that marks the note as removed). The page only reads and inserts; the grants hold no UPDATE and no DELETE, and the page text has none. The page checks the frontmatter (description, component, kind, severity) before it saves. The tests are in daemon/page-memory.test.ts.

The notes as files on each computer (B92, stage 3a). A computer that has the package memory keeps the newest version of each note of the company as a file: ~/.config/kdmem/memory/<project>/<id> (a note of the whole company is in _company). The daemon writes a file only when its text differs from the server, and deletes the file of a removed note. A watch on kd_memory starts the sync at once, and the daemon runs it every 5 minutes too. A project reads these files when .claude/kdmem/config.json has "store": "kdmem" (the default is "files": the files of the repo, as before). The name of the project is the folder of the main checkout, or "project": "<name>" in the same file. Each file gets the created time of its note (the add date from git, held by the server) as its mtime: recall ranks undated notes by it, so the order is the same on every computer and the same as in kinedb (a file with the right text and a wrong time is only touched, never fetched again). The hooks read only files, so they work with no network. The code is in daemon/memory-cache.ts, the tests in daemon/memory-cache.test.ts.

Write a note from a session (B92, stage 3b). In a project with "store": "kdmem" the hook of the package memory denies a Write or Edit of a note file (memory/*.md, archive/*.md) and names the command. bun "$D" --note put <id> <file> [--project <name>] checks the frontmatter (the same checks as the hook; supersedes looks in the cache folder), adds a new version on the server and puts the file in the cache folder at once. --note remove <id> adds a version that marks the note as removed. With no network the write goes to ~/.config/kdmem/memory-queue.jsonl; the daemon sends the queue in order BEFORE each sync of the files (so the sync never undoes a queued write), and a write that is sent twice is harmless (an equal text is skipped). --note flush sends the queue now. The code is in daemon/memory-put.ts, the tests in daemon/memory-put.test.ts and package-tests/memory/hook-gate.test.ts.

The package memory has the kinedb rules (B92, stage 1b). The parts of the kinedb memory (kine-memory/lib.ts and the hook) that the package did not have are ported, with the tests of kinedb: lessonSummary and firstParagraph (the summary of a note that has no description), deriveFrontmatter (the back-fill of a note with no frontmatter; a handoff note is a decision of severity 1, so it ranks last; the components may have bodyHints, pathHints and ambient), the stop-word stems (fixed and making are stop words when fix and make are), triggers that match in any letter case and never fail on a bad pattern, the gate memory-md-is-a-pointer (a commit that stages a MEMORY.md over 8192 bytes, with a line over 500 characters, with a bullet for a landed arc or a commit, or with more than 12 bullets that name a note is denied; # gate:ok passes it), and the event log of the hook (~/.config/kdmem/memory-log/kdmem-YYYY-MM-DD.jsonl; kdmem.ts log [--tail n] [--session s] [--event e] prints it; derive <path> [file] prints the frontmatter that a note would get). The labelled prompts of kinedb are in package-tests/memory/recall-labels.ts. labels.test.ts runs them over the real corpus when KDMEM_LABELS_CONFIG names the config of the project and the daemon keeps its notes as files (CI servers have neither, so the test is skipped there). On the kinedb corpus: kdmem recall labels: 24/24 hold.

Parity with the repo (B92, stage 4). package-tests/memory/parity.test.ts answers the question that comes before the note files of a project are removed from its git repo: does the project lose anything? It runs only when KDMEM_PARITY_CHECKOUT (a fresh clone or checkout), KDMEM_PARITY_KINEDB (the repo with .claude/kine-memory/) and the cache folder of the daemon exist. It checks the data (the same ids and the same text in the cache folder as in the repo; nothing else) and the entries (kinedb's entryFromFile and the package parser make the same entry of each note). The first run found three differences, all fixed: the title keeps no backticks (240 notes), a component that is not in the config makes the note invalid (3 notes), and with the store kdmem the created time is the mtime of the file (the git add date, set by the daemon), not the date in the file name. On 2026-10-19: data 2876 = 2876, entries 2838 = 2838, no difference. The seed sends only the files that git tracks (it had sent 17 ignored session handoffs; they are marked as removed).

The package fleet (a queue of arcs in the background). /fleet <id> … makes a session the supervisor of /arc workers. Each worker is a claude --bg session. The supervisor starts them up to a limit that the owner gives, watches them, wakes them, queues the issues that a worker filed and refills the queue. It needs the package arc: the issues come from its forge.ts, the ids from the idPrefix of the project, and the worker is /arc. It needs bun, git and claude.

Several computers (B92, owner 2026-10-20). When the owner has more than one online computer that lists a folder of the project (the mod tool spawn_targets), /fleet asks ONE question: work as a global leader? On yes, the leader starts one manager session on each computer with the mod tool spawn (/fleet --manager <leader session id> <ids>), keeps the owner's orders for each computer (slots, hold) and talks to the managers with send. A manager works as /fleet does on one computer, with its own ledger, queue and slots, and does the local proofs. With one computer nothing changes. The text is section 0a of the skill; there is no new command in fleet.ts.

One session, one issue (B118, owner 2026-10-21). "ONE session = ONE issue, start to merge. A blocker or follow-up gets its OWN issue and its OWN new session. Never send a running worker a second B-id, even if the leader or the worker proposes it." The hook fleet-one-session-one-issue denies a SendMessage or a kdmem send to a worker fleet B<n> whose text gives another B-id as work (a line that only reports a state, such as "B7 merged" or "blocked by B7", passes). The second rule: "A session that waits (waiting list, pr merge --wait, blocked on a merge) holds NO editing slot; count only sessions that edit now." So takenSlots does not count a worker whose state is blocked, and the slot line says "blocked B2 does not count". Both rules are in section 0 of the skill and in the prompt prompts/one-issue.md. The words of the compact prompt stay as they are (owner 2026-10-20).

The worker keeps its issue (B119, owner 2026-10-22). The same rule from the side of the worker: "ONE agent can only work on ONE thing." /arc <id> binds a session to its issue (the id of its first /arc <id> prompt of a human). The hook one-session-one-issue of the package arc denies a commit whose subject starts with another id, issue claim of another id, and a branch or worktree arc/<other id>; it blocks a second /arc <other id>; and it adds a reminder when a prompt gives a second id as work. The kdmem mod sends no transcript_path, so the hook finds the transcript by the session id. fleet.ts wake refuses a message that gives the worker another id (exit 4). A session that did not start with /arc <id> has no bound issue, and no gate judges it. The rule is in the skill arc (start and section 6), in the prompt rules of the package arc and in section 0 of the skill fleet.

The daemon logs in again after a lost connection (B121, owner 2026-10-22). The client SDK has no reconnect (kinedb/kinedb#781, B861). The daemon loop already logged in again after a failed pass, with a fixed wait of 5 s. Now the wait grows: 1 s, 2 s, 4 s, 8 s, 16 s, then 30 s for ever (kdmem/daemon/reconnect.ts). A good pass resets it. A connect or login that gets no answer ends with an error after 20 s, so the loop tries again. Remove reconnect.ts when the SDK reconnects by itself.

The hook judges the entered worktree (B122). After EnterWorktree the hook gets the launch dir as cwd, so no-commit-on-main read the branch of the main checkout and denied a plain git commit in the worktree (kinedb/kinedb#5192, #5365). The gate of the package arc now reads the transcript (by the session id): the last EnterWorktree that no ExitWorktree ended gives the start dir, when it is a worktree of the same repository (enteredWorktree in hooks/lib/transcript.ts). A failed call and the calls of a subagent do not count. The repo's own .claude/hooks/gate.ts is not changed.

An interactive session has its own row (B120). An interactive session has no job id, so its id is empty. All interactive sessions of one computer then shared the row key <user>|<machine>|: the fleet page showed one of them, and the daemon wrote the rows again at every pass. Now the key of a row is <user>|<machine>|<id>, or the sessionId when id is empty (rowKey in agents-publish.ts). The column id stays empty on purpose: the page stops and removes a session by that id, and an interactive session has no job. A job that was resumed as an interactive session keeps its job folder with the same sessionId. offlineSessions now skips such a job, so the page shows the live row only.

bettercompact finds a dump file that Bash wrote (B123). The mod used to see the dump file only when a Write, Edit or MultiEdit named it. A session that wrote it with cp or cat >> in Bash never compacted: 15 of 15 calls with a Write compacted, the 4 calls with only Bash did not (two sessions, 2026-10-23). Now, at each turn end in the phase asked, the mod also reads the time of the file at the path that the tool named. A file that changed after the call of the tool is the dump file (an older file is a leftover). The approval switch now ends after 40 turn ends with no dump file. A give-up and a hook veto (skip) write a trace row (compact-give-up, compact-skipped). A session that has no turn gap (kdmem issue B124) stays as it is, by decision of the owner: the agent decides when it compacts, and a person can type /compact.

A daemon that cannot publish ends itself (B125, owner 2026-10-23). The heartbeat file proves that the loop runs, not that it publishes: a daemon with a lost or hung connection looked alive, so no session started a new one. Now the daemon writes publish-ok.json at each good pass. When no pass is good for 10 minutes, the daemon ends with exit 4 and frees its lock (kdmem/daemon/watchdog.ts). The heartbeat then goes stale, and the next session of Claude Code starts a new daemon with --ensure. The lock also changed: a live pid holds the lock only when it wrote heartbeat.json, or when the lock is less than 60 s old. A program that got the pid of a dead daemon no longer blocks the new daemon (isPublisher in publisher-lock.ts). Not built: a launchd or systemd service. A person must ask for it; it is never a default of the setup.

Each trigger of the mod checks the daemon (B126, owner 2026-10-23). The mod used to read heartbeat.json only at a session start and at a turn end, at most every 5 minutes. A worker with one long turn, and an idle session, were not checked. Now a prompt, a tool call, a turn end and the 2 s message tick call one check (ensureIfDue in register.js). The check runs --ensure when the heartbeat is stale. An in-memory limit of 30 s (checkEveryMs in mod-config.json) keeps the cost at one file read, and decide() still allows one start in 60 s. With B125 a daemon that cannot publish ends itself after 10 minutes, and the next check starts a new one. An arc hook that runs --ensure would repeat this check, so it is not built.

Row What it is
skills/fleet/ The /fleet skill (generic: no tool or id of one company).
files/scripts/ fleet.ts (the commands limit, start, watch, status, wake, next, queue, hold and more), transcript.ts, session-procs.ts, url-path.ts. The ledger and the queue files are in $TMPDIR/kdmem-fleet/.
hooks/gate-pre.json PreToolUse (Bash, AskUserQuestion, SendMessage, the kdmem tool send): a worker with a live supervisor sends its question with SendMessage; a worker on waiting.txt commits and pushes no code; a message to a worker fleet B<n> that gives another B-id as work is denied (B118, below).
prompts/one-issue.md The two rules of the owner (B118) in every session of the package.
hooks/gate-stop.json Stop: the supervisor of a live worker holds a watch, wakes a worker with unread notifications and fills a free slot.

The kdmem mod sends no background_tasks and no transcript_path in a Stop event. files/hooks/fleet-gate.ts reads the background tasks from the transcript of the session (a background Bash call, its id, and the <task-notification> that ends it). When it cannot read them, the fact is unknown and the turn ends: an unknown fact never blocks. The tests are in kdmem/package-tests/fleet/. Three of them (*.selftest.ts) run themselves and call process.exit; they are not named *.test.ts, because bun test would stop at the first of them. run-self-running.test.ts runs each one as a program.

This folder is NOT copied into a project by setup.ts. It is the product, and it lives here so that the kinedb repository stays free of it.

Path What it is
daemon/ The publisher (agents-publish.ts), the device key and login (setup.ts, device.ts), the signed delivery of skills and code (assets-*.ts, release-sign.ts, code-update.ts), the company admin (company-*.ts). bun test kdmem/daemon/ runs its tests.
mod/ The kdmem-usage mod. claude plugin test kdmem/mod runs its tests. The mod writes the plan limits AND the context of each session (tokens and window) to ~/.claude/kdmem-usage/<session>.json. The fleet page shows the percent of the context window from it, so a computer needs no status line script; a personal status line log, when it exists, wins. A tool call writes it too (at most every 20 seconds), because a fleet worker runs one long turn and session.measure fires only when a turn ends. The page shows the percent and the current use (12% · 117k), never the size of the window as a number (owner 2026-10-19). The daemon still writes the size of the window to the column fleet_agents.ctx_window (run bun daemon/company-cli.ts <env-file> agent-window ONCE as root BEFORE the release of the daemon).
.claude-plugin/marketplace.json The marketplace that offers the mod.
skills/ The skills that the database delivers to every machine (/kdmem-setup and /kdmem, both global). Publish with bun daemon/assets-cli.ts <env-file> publish kdmem skills/kdmem skills/kdmem-setup. Remove a row with ... delete <path>.
site/ The fleet page and the admin page. sh deploy-site.sh deploys them.
deploy-*.sh, provision-server.sh, Caddyfile The server side.
q*.ts, q*.sh Probes and end-to-end checks from the design phase.
DESIGN.md The design.

/kdmem (B14) is for a person who wants to ask or change kdmem in chat: /kdmem, /kdmem scope except . or /kdmem why is my session not listed. The skill explains what kdmem is and lists the commands of the daemon file and of the template repository (scope, password, spawn, trace). It reads the status of the computer. It changes only what the person asked, and it never reads a key file or types a password (the person runs that command with !).

The daemon and the skills of a machine come from the database, signed with an ed25519 release key. They never come from a git pull. The private key stays on the owner machine (~/.config/kdmem/release.key.pem).

Details
npm
2026-10-23 10:42:07 +00:00
6
UNLICENSED
latest
187 KiB
Assets (1)
Versions (14) View all
0.1.13 2026-10-23
0.1.12 2026-10-23
0.1.11 2026-10-18
0.1.10 2026-10-18
0.1.9 2026-10-18