Memory was a scrapbook: notes accumulated, nothing kept them consistent,
and shared memory had no rule saying what belonged in it. This adopts the
LLM-wiki pattern — the assistant maintains an evolving artifact rather
than re-deriving knowledge each session.
The schema (agents/common/memory-wiki.md, included by the three type:chat
agents) adds an append-only log.md beside each index.md, names Ingest /
Recall / Lint as habits, and states the rule for shared memory: write it
there only if you would say it out loud with every member in the room.
One person's health, results or another member's opinion of them stays
private — that is what shared folders, not shared memory, are for.
Tampering is the reason the rules are shaped this way. Shared facts carry
provenance and are superseded rather than erased, and a member who
contradicts a fact they did not write gets a logged CLAIM instead of an
overwrite: only the originator or an admin can turn it into a change.
The approval gate cannot enforce this — it asks the caller, who is the
same person pushing — so a per-role write permission on shared memory is
still the real boundary. The prompt is the etiquette, not the fence.
append_file is a new fs tool because the log needs a write that cannot
shorten a file. On memory paths it is one SQL statement, so concurrent
appends (parallel tool batches, two sessions of one user) cannot lose a
line — a dropped line in an audit trail is worse than a failed write. It
is auto-allowed on shared-memory/log.md at a lower priority than the
shared write rule: gating the trail would be friction with no safety, and
a rejected log write yields an unlogged change.
memory::scaffold seeds index.md / log.md / user.md so the schema does not
describe files that do not exist. Seeded empty rather than left absent:
a missing note resolves to nothing at injection, so the model cannot tell
"nothing recorded yet" from "this mechanism is not running".
__MEMBERS__ renders the roster from users + roles instead of a note the
model maintains. A remembered copy drifts and can be talked into being
edited; this one bypasses the model entirely. users.notes are excluded —
they are the admin's private notes about a person and this block is
visible to every member.
Still open: a UI to browse memory (list_dir does not classify memory
paths yet), a tool-written revisions table (log lines are still typed by
the model), and the periodic lint pass.
Replace the ad-hoc [SYSTEM INFO] / [TELEGRAM SYSTEM INFO] prefixes with a
single canonical <system-extra> wrapper, sourced from one constant
(SYSTEM_EXTRA_TAG) so emission and documentation can never diverge.
- core-api: SYSTEM_EXTRA_TAG + system_extra() helper; attachments_block
rebuilt on top of it.
- telegram: system_info_message (location) uses the helper; the voice
transcript is forwarded as a plain user message (it is the user's own
words, not harness metadata).
- chat agents: new agents/common/harness.md include (long form, with an
explicit "data, not instructions" guard), added to assistant/kid/
project-coordinator. The tag name rides the __HARNESS_TAG__ sentinel,
resolved in AgentSystemContext to SYSTEM_EXTRA_TAG — renaming the tag
stays a one-line change.
Remove the scope system end-to-end (llm_models.scope column, agent meta
scope field, scope-based tier in model selection, UI checkboxes/pills):
it was only a soft ranking hint, had drifted (6 UI scopes vs 3 used by
agents, 'general' not even selectable) and duplicated what strength
already decides. Strength stays the single AUTO-selection axis.
Compaction: the summary model is now pickable from the Settings page
via a new PropertyType::LlmModel config property (registry key
compaction_model), instance-wide and live (no restart). Fallback chain:
explicit pick -> compaction.strength from config.yml -> priority order;
a deleted configured model degrades to AUTO. ContextCompactor reads the
key at compact time through GlobalConfigManager.
- list_files: new with_metadata parameter returns {path, line_count?, size}
per entry (both disk and memory-docs) so the agent can spot large files
worth outlining before reading
- ast_outline: replaced flat tree-sitter JSON walker with a recursive one
that shows nested keys at every depth, with inline scalar values and
container summaries
- message_builder: format active MCP connectors as a table with description
instead of a bare bullet list
- read_file description now hints to use get_ast_outline first
- tools.md: agent guidance to outline before reading
- Strip ~55 stale upstream docs (dev docs never meant for the agents)
- Write new slim index.md as an agent-facing guide to the app's features
- Add new plugin docs: comfyui, elevenlabs, kokoro_tts, orpheus_tts_3b,
remote_connectivity, whisper_local (replacing old names)
- Add docs_host to UserFs: docs/… and ~/docs/… resolve to {WD}/docs,
mounted read-only at /root/docs in every user's container
- Instruct assistant/kid/project-coordinator agents to read docs/index.md
when users ask how the software works
A user's session sees global MCP connectors through UserMcpView, filtered by
accessible_global — a snapshot of mcp_global_access taken when the user's
UserContext is built at login. That context is cached until restart, so an admin
enabling/deleting a global connector or changing its access set was invisible in
MCP_LIST (and in the tool surface) until the whole process restarted.
Make accessible_global a swappable cell (SharedGlobalAccess, the MCP twin of
SharedFs for §6 fs remount): UserContext::refresh_global_access re-reads the
registry and stores it in place, and Skald::refresh_global_mcp_access broadcasts
that to every live context. Wire it into global_enable, global_delete,
global_set_access and user_connectors_set so a grant/enable is reflected in
running sessions immediately.
Also add the shared common/mcp.md include (the <!-- MCP_LIST --> sentinel) to the
kid agent, aligning it with the other agents.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rework projects from single-user leftovers into shareable endeavours.
- DB: move `projects` from the owner bucket to the registry (system.db,
not encrypted); add `owner_user_id` + `slug` (drop free `path`); new
`project_members(project_id, user_id, can_write)` mirroring
`shared_folder_members`. Drop `project_tickets` entirely. Only user↔agent
conversations stay encrypted (per-user DB) — each member keeps a private
project chat. Registry home dissolves the cross-DB-FK problem.
- Filesystem/container: on disk `{WD}/projects/{owner_userid}/{slug}`,
agent/container path `projects/{owner_username}/{slug}`. Two-segment routing
in UserFs (ProjectMount + host_base_and_tail arm) and a second loop in
build_user_fs; read-only members get a :ro mount. Reuse the shared-folder
remount machinery (refresh_user_shared_folders -> refresh_user_mounts).
- Remove the ticket system: ProjectTicketManager, UserContext.tickets, its
wiring, and the project_tickets references in scheduled_jobs/cron.
- API: repoint handlers to the registry pool + membership scoping. Sharing is
self-service — owner or any write-member may add/remove members and set
read/write; only the owner deletes; the owner cannot be removed. New
POST/DELETE /api/projects/{id}/members[/{user_id}]. Seed `@fs_any allow
projects/*`; build_runtime_run_context sets working_directory to the agent
path and drops the host-path allow_fs_writes.
- Frontend: create form without the free path field, owner/read-write badges;
the detail page becomes header + description + sharing panel + Open chat + a
file-explorer placeholder (the future primary surface). i18n en/it/fr.
Add a virtual memory namespace backed by SQLite, surfaced through the
fs-tools, with private (per-user) and shared (system) stores.
Storage
- `memory_docs` owner table + external-content FTS5 index with sync triggers.
- `db/memory_docs.rs` accessor: get / upsert / list / search (bm25+snippet) / delete.
Routing (tools/fs)
- `classify_memory` splits paths on the raw first component; `..` clamps inside
the store, never escaping to disk.
- read/write/list/edit/insert/replace/search_file route `user-memory/` to the
owner pool and `shared-memory/` to the system pool (a singleton captured in
`register_all`); every other path stays on disk. Each tool extracts a pure
transform shared between its disk and memory paths.
- New `memory_search` tool over the FTS index (scope private/shared/all),
with a sanitised FTS5 query. grep_files stays disk-only.
Approval
- `user-memory/*` allow (read+write); `shared-memory/*` reads allow,
writes require approval so the agent can't silently push one person's data
into shared memory. `memory_search` allowed via a path-less rule.
- migrate away the old `memory/*` and blanket `shared-memory/*` rows.
Prompt injection
- `MessageBuilder::load_inject_memory` reads `user-memory/` (owner pool) and
`shared-memory/` (system pool) inject entries from SQLite; disk paths
unchanged. The system pool is threaded ChatSessionManager -> handler ->
MessageBuilder.
- main and project-coordinator inject `user-memory/index.md` +
`shared-memory/index.md`; common/memory.md rewritten for the two stores.