Release notes
Celindor ships as one release train: the apps, the server and this site share a version number.
Changelog
All notable changes to Celindor. The version is the release train shared by
daemon, admin, site and flutter — and by the API contract — so each release
section lists what changed in each area. See RELEASING.md.
The format follows Keep a Changelog.
New entries go in changes/unreleased/, not here. See changes/README.md.
[Unreleased]
[0.7.0] - 2026-11-13
The agent searches every folder of an IMAP mailbox (#1603)
- daemon: on an IMAP mailbox without an All Mail folder (iCloud, Fastmail and most others), the agent now searches all of your own folders except the trash and spam, not only the inbox. Ask it to look in one folder by name and it searches only that one.
Due to-dos on your Apple Watch (#1601)
- flutter: the watch app has a to-do list, behind the list button on the talk screen. It shows what is overdue, due today or due in the next two days, the same as Today on the phone. Tap a to-do to tick it off (Undo at the top of the list opens it again), or swipe it and tap Not today to hide it until tomorrow. Adding a to-do is done by saying it on the talk screen; the whole list and editing stay on the phone and the web.
Report an issue from the test builds too (#1525)
- flutter: the test builds (TestFlight, Android and the test web client) have “Report an issue” as a bug button next to Settings in the account row. They don’t get the other test tools (“What’s new in this build”, “What the agent is told”), which stay in the dev builds’ menu.
- flutter: the report form asks for your name. It is shown on the issue and remembered on the device, so the next report starts with it.
- daemon: a test daemon files reports too; prod still doesn’t. Every
report is labelled with the target it came from (
from dev,from test) and names it in the issue’s Environment table. Reports from test get the Urgent priority like those from dev. - daemon: the report request takes an optional
reportername; without it (older apps) the issue names the account’s display name, as before. - deploy: the deploy writes the issue-report tokens for test as well as
dev. Add
ISSUE_REPORTS_GITHUB_TOKEN(and the attachments settings) to thetestGitHub environment.
A message to the agent in a shared chat is shown as one (#1510, #1511)
- flutter: in a shared chat, a message that asked the agent has To … with the agent’s name over it, for everyone in the chat and while your own is still being sent. Before, nothing in the chat told a message to the agent from one to the people, so nobody could see why the agent answered or stayed quiet.
- daemon: a shared chat’s message keeps whether it asked the agent (the Ask … chip, a message naming the agent, or “Answer every message”), and sends it with the message (
to_agent). The agent reads it too, as[Anna, to you], and is told that the rest is the members talking to each other. Messages sent before this carry no mark.
Using the app keeps you signed in (#1500)
- daemon: a session now ends after 7 days without use, and at the latest 90 days after signing in. Before, every device was signed out exactly 7 days after it signed in, however much it was used. Signing out, a password change and an operator’s reset still end sessions at once.
Google Drive documents: only the latest copy is kept (#1489)
- daemon: when a synced Google Drive file changes, Celindor now keeps only its latest copy. Before, every change left the previous copy in storage for 30 days. Earlier versions are in Google Drive’s own version history.
- daemon: a Google Doc whose text did not change (a comment, a rename, a sharing change) is no longer read and learned from all over again.
- flutter: a document from Google Drive has a button that opens it in Drive, where its earlier versions are.
The inbox groups suggestions under who or what they are about (#1488)
- flutter: a new person or thing in To review is one row with its facts and relationships nested under it (a relationship between two new ones under the first, with a pointer on the second). Save on it asks “Also save the N facts about …?” (All, Choose, Only …), Remove asks “Also remove the N facts?” (Yes, all, or No, link to someone else, which moves them to an entry already in memory), and Later puts off the whole group; one Undo takes back the whole decision. A fact’s Save reads “Save (also saves …)” and saves the entity first. Facts about something already in memory are listed under “… (already in memory)”, each with its own Save and Remove.
- daemon: saving a fact about a suggested entity saves that entity too, as saving a relationship already did, so the order is right however the decisions arrive. Facts left about a suggestion that was saved or merged name the entity they wait on (
subject_existing_entity_idbesidesubject_entity_nameinGET /api/v1/user/staging). New routePOST /api/v1/user/staging/entities/{id}/relink: rejects a suggested entity and moves its open facts and relationships onto an entity in its own memory, in one transaction. No migration.
A corrected name follows through the inbox (#1486)
- flutter: correcting a name once, by saving a suggestion under another name or with
rename_entityin chat, now updates the other suggestions still waiting about the same person or thing: the entity suggestion’s name, and facts that name it (“Kristina W starts in May” becomes “Christina W starts in May”). Each suggestion changed this way shows a small grey line, “Name corrected from Kristina W”. - daemon: the correction runs in the background over the caller’s open suggestions (pending and Later) in the same memory, personal or one organization scope. Only whole names are replaced, decided suggestions are left alone, and the old name becomes an alias of the entity, or of the suggestion while it is still one. When another entity or suggestion in that memory has the old name, nothing changes automatically. The Inbox list’s rows carry
name_corrected_from; migration 0253.
An item set to Later is on the Later tab right away (#1484)
- flutter: in the inbox, switching tabs sends decisions still waiting out their Undo at once, so an item you just saved, removed or put off with Later is on the tab it now belongs to. A decision whose Undo runs out while you are on that tab shows up there without reloading.
One search across memory, and a map button that stays put (#1483)
- flutter: Memory has one search field above its tabs. It looks through short-term and long-term memory at once, in your own memory and the shared memories you can see (a household or a company), and labels each hit Short-term or Long-term, with the shared memory’s name. Tapping a hit opens it where it lives; clearing the field brings the tabs back as they were. Short-term and long-term memory are lists, and Show map sits in the same place next to the tabs whichever of them you are on; the map shows long-term memory only and says so. The app remembers whether you last chose the map or the list (the very first time, the map once memory holds ten things), and Show whole map brings a zoomed or moved map back to the full view.
- daemon:
GET /api/v1/user/memory/searchsearches short- and long-term memory at once, own and shared, with the shared memory each hit lives in. Shared rows go through the same access check as every other organization read, so a space you may not read gives no hits.
The inbox recognises what memory already has, under other spellings too (#1482)
- flutter: suggestions memory already has are no longer offered as new ones. They are merged into the entry in memory and listed in one collapsed group at the top of the suggestions inbox, “Merged automatically (N)”, each line saying what went into what, with Undo. Got it clears the group on every device without undoing anything; new merges show it again. A suggested fact about something already in memory says “About X, already in memory”.
- daemon: a suggested entity with the name of an entity in the same memory, or another spelling of it (“S:t Petri kyrka” for “Sankt Petri kyrka”, spaces, hyphens, case), is merged into it when it is suggested, and the spelling is kept as an alias. Facts and relationships about it still wait for review, on the entity in memory. In the background dream, near misses (“Petri kyrka”) are put to the model, and only a sure answer merges. Several possible matches (two people called Kristina) or a different kind of thing still ask with “Same as …”. Undo puts the suggestion back with its facts and takes the alias away again. New routes
GET /api/v1/user/staging/auto-merges,POST /api/v1/user/staging/auto-merges/{id}/undoandPOST /api/v1/user/staging/auto-merges/acknowledge(Got it: stops listing them, the merges stand); migration 0252.
Why a suggestion is high, medium or low (#1481)
- flutter: tap the confidence on a suggestion in To review to see why Celindor is that sure, in one plain sentence, and the short quotes it is based on, each with where it came from (chat, email, document or recording) and when. Suggestions from before say that no explanation was saved.
- daemon: the memory extractors judge each entity, fact and connection on
its own (it used to be medium for all of them), and say why, in the user’s
language, with up to three short quotes from the source. Quotes that are not
in the source are dropped. An imported document is never high. Kept with the
suggestion (migration 0248) and sent with the Inbox list, now a contract
route (
confidence_explanation). - daemon: without an Anthropic key, the dream no longer saves high suggestions on its own: everything waits in To review until the model’s judgements have proven right.
Fewer sign-out and settings races in the app (#1470)
- flutter: signing out while the app is still asking for notification permission no longer leaves this device getting the old account’s notifications.
- flutter: a setting saved right after opening its screen (an API key, the chat provider, a permission) shows the saved value instead of the one loaded before it.
- flutter: the app no longer re-checks your account, or clears its caches again, on every foreground or every press of Sign in.
- flutter: usage statistics (when allowed) are sent in full after a backlog, and retried once a minute while offline instead of on every page view.
Database clean-up from the November audit (#1468)
- daemon: When an owner’s account is deleted, their organization passes to its longest-standing admin, then member, and only then to a viewer, instead of to whoever joined first.
- daemon: Restoring a deleted organization brings back what was waiting to be added to its memory even when the server’s clock runs ahead of the database’s.
- daemon: An expired memory share no longer counts when checking who you may share a document or recording with.
- daemon: Memory searches leave out entries that were replaced or merged, and a division’s agent ranks keyword matches the way personal recall does.
- daemon: About fifty unused database functions, a table and two columns that nothing used are gone, along with duplicate indexes that slowed writes.
Small fixes to the agent loop, sandbox and log masking (#1464)
- daemon: A self-hosted or other OpenAI-compatible service that answers with its reply in parts is read as that reply, instead of as an empty answer that failed the turn.
- daemon: A Claude reply cut off because the conversation filled the model’s window is handled as cut off: a half-written tool call no longer runs.
- daemon: A reminder’s or skill review’s turn shows the agent the chat’s last message even when it went unanswered; a message sent while the agent works no longer loses the turn’s recalled memory if the request has to be shrunk; prompt caching holds after a continued reply.
- daemon: Asked how it is set up, the agent no longer talks of “Supabase tables” whose rows reconfigure it, and its memory rules no longer point at a “Known Entities” section that no longer exists; existing accounts’ stored rules are updated too.
- daemon: Names from an organization, a parent account or a sub-account go into the agent’s instructions as plain names, so such a name can no longer pose as instructions.
- daemon: Log and admin-event masking also covers passwords of several words or in quoted JSON, the session cookie, a bare bearer token, xAI and Brave keys, and any value under a secret’s field name.
- daemon: For a household chat with the memory pre-gate on, the gate’s AI calls count against the organization’s and division’s spend, like the extraction they guard.
- daemon: DuckDuckGo result titles and snippets show characters such as emoji and dashes correctly.
- operators: Sandboxes are named and labelled with the daemon’s configured environment (
envin config.toml), not alwaysdevwhenCELINDOR_ENVis unset; a local daemon’s earliercelindor-dev-sbx-*containers are left for you to remove. A blank or invalidEGRESS_PROXY_PORTorDOCKER_PROXY_PORTnow falls back to the default or stops the proxy with a message naming it.enforce = trueon a decision gate now takes effect on its own for the pre-gates and dream review, as it already did for the agent router. Turns nothing classified (household chats, users without an Anthropic key) no longer leave “parse failure” rows inutterance_classifications.
CLI, migrations and session state tidied (#1463)
- daemon:
celindor initwritesenv = "local"and names the[database.local]and[storage.local]sections, as the setup guides expect. Existing configs are unchanged. - daemon:
celindor startexits with a failure status (128 + the signal) when the daemon is killed by a signal it did not forward, such as the OOM killer, instead of 0. - daemon: CLI commands such as
claim-code,set-secretandexportreturn as soon as they finish instead of lingering for about 10 seconds. - daemon: Two
celindor migrateruns on one database (an ssh retry during a deploy, two worktrees on the local database) now wait for each other instead of one failing on a duplicate key. - daemon: The daemon refuses to start on a session-state file (
state.db) newer than it knows, for example after rolling back to an older image. The error says the file can be moved aside, since it holds only drafts and open-chat state. - daemon: After a restart, an app that reconnects having missed changes now also hears that a send cut off by the restart failed.
Safer deploy retries and one celindor.ai publish at a time (#1461)
- deploy: A deploy whose SSH session drops partway through is no longer started a second time beside the first; it fails and asks for a check of the server. Only one celindor.ai publish runs at a time, and cancelling a deploy run now also stops its celindor.ai publish and web image build.
- ci: A manual Android build (the signed bundle for a hand upload) and the builds for pushes to main no longer cancel each other, and the push builds skip the “What’s new” list nobody could see. The supply-chain scan also covers the embedding sidecar’s Python requirements.
Chats and threads: small fixes from the November audit (#1459)
- flutter: deleting or archiving a chat you are not in, here or on another device, no longer moves the chat your other devices fall back to.
- flutter: a message waiting behind one that could not be sent now goes, instead of waiting until a later reply ends.
- flutter: “Loading earlier messages” shows while older messages are fetched, and a page that arrives after you switched chat no longer moves the chat you are in.
- flutter: a Mac waking from sleep reconnects at once instead of after two seconds.
- flutter: a file refused as one too many uploads once you remove another to make room.
- flutter: the “not kept in your library” note under a file is easier to read.
- flutter: a deadline card due today or already past no longer offers “Remind me”.
- flutter: a tool step that failed while a reply is still being written reads “Using 1 tool… · 1 failed”, not “Using 0 tools…”.
- flutter: the memory picker shows an organization’s divisions also when opened while the organization list was still loading.
- flutter: editing many memory proposals at once scrolls on Android, the web and desktop.
- flutter: long agent, organization and member names end in “…” instead of running off the screen.
- flutter: organization chats: an edit that could not be saved keeps your text and stays open, and what you were writing comes back once the edit is done.
- flutter: organization chats: a reply still being written is no longer wiped by the end of the reply before it, and a reaction that could not be saved no longer removes other people’s reactions.
- flutter: organization chats: a removed “@Ann” is not sent when “@Anna” stays, and names that start alike are shown in bold whole.
- flutter: organization chats: Forward says it failed when the chat list could not be read, a read-only chat no longer offers Reply and React, and search no longer runs for a letter you typed and took back.
Admin console: small fixes from the November audit (#1458)
- admin: the memory log tabs show no count until opened (not “(0)”), and “100+” when a page is full.
- admin: errors on the skills, memory log, memory spaces, API keys and organization key pages show the server’s message instead of raw JSON, and an expired session goes to sign-in.
- admin: Drop session, Revoke permission, and removing a predicate, attribute key or alias ask first.
- admin: the edit and members dialogs close with Escape, take keyboard focus and are announced as dialogs.
- admin: editing a scope with no colour keeps it colourless, and Remove color clears one; a new organization’s parent can only be one you own; the success banners on the organizations and sub-accounts pages are styled.
- admin: the memory graph keeps the kinds you switched off and the nodes you didn’t move in step on Load more; the organization chart no longer cuts off the right-most organization’s buttons.
- admin: signing in from a page with a query (such as
/admin/usage?days=7) returns to it with the query; the sign-in form asks for 8 characters, as every password has. - admin: the Logs page shows up to 1,000 lines, all the daemon keeps.
- admin: browser requests send the client version header, so the server can tell an outdated console.
- operators:
SITE_URLno longer exists in the console; setINTERNAL_API_BASE. Runningpnpm testinadmin/needs Node 22.6 or newer.
Households, agents and missions: small fixes from the November audit (#1456)
- flutter: An agent whose name is only emoji shows that emoji as its avatar, not a broken character.
- flutter: A deleted household’s “restorable until” date is the day in your own time zone.
- flutter: Accepting an invitation to an organization that is full says so, instead of saying you are already in it or to check your connection.
- flutter: Adding someone without email to a full organization says so in the app’s language.
- flutter: Removing a member, changing a role or handing over ownership also updates the pending invitations, which the change may have revoked; a revoke that fails refreshes the list too.
- flutter: When an organization’s members could not be loaded, the Household screen says so and offers Retry, instead of “No members yet besides the owner.”
- flutter: Division asks, sharing and memory history open with the iOS and macOS page transition, like the screens around them.
- flutter: A mission that changes again while the app is reading it is read once more, so its final state is never missed.
- flutter: A mission budget of “NaN”, “Infinity” or less than a millionth of a dollar is refused with the usual message.
The privacy policy names every connection and recipient (#1449)
- site: the privacy policy (English and Swedish) now lists Google Drive and iCloud contacts among the accounts you can connect, and Telegram and the browser push services used by the web app (Google, Mozilla, Apple, Microsoft) among the parties that receive data. The Swedish policy says “aviseringar”, as the app does, and the English one spells “Your data” the same way throughout.
Mac notifications right after closing the window (#1448)
- flutter: on a Mac, a message that arrives after you close Celindor’s window, while Celindor is still the active app, now shows its notification. Before, it was dropped until you switched to another app.
API contract fixes from the 2026-11-10 audit (#1446)
- daemon: the household agent routes declare the 409 they answer when a team’s front desk would be moved, retired or deleted, or an agent with a shared thread would move. Starting a Google Drive connection no longer declares its request body required; the daemon never needed one.
- ci: the contract coverage check fails when fewer routes sit outside the contract than its baseline records, so the baseline is lowered in the same change (now 186).
Daemon runs unprivileged, safer config rendering and operator scripts (#1445)
- daemon: the server process now runs as an unprivileged user instead of
root, and the program that splits uploaded recordings no longer sees the
server’s keys. Configuration values containing quotes, backslashes or line
breaks, and database passwords with characters such as
/or@, no longer stop the server from starting; a yes/no setting written asTrue,1oryesnow counts, and an unreadable one names itself at start. - daemon: in the agent’s sandbox,
pip installworks inside a Python virtual environment. - daemon: operator scripts: the embedding backfill reads users’ own
OpenAI keys again, and the memory clean-up script asks for a user or
--all, confirms test and prod, merges duplicates only one confirmed pair at a time, and no longer loses a fact when the link that should replace it fails. - daemon: contributor and table docs match how things run today: pushes
to
mainmigrate dev, work lands through draft PRs and merge trains.
Drafts go with their chat; the CLI keeps its environment and shows migration notices (#1444)
- daemon: deleting a chat now also removes its unsent draft, read and scroll position and its place among your open chats. The same happens when an idle incognito chat is cleaned up, and when an account or sub-account is deleted. Before, that text and state stayed on the server.
- cli: running
permitorrevokewithCELINDOR_ENVset no longer makes that environment the active one inconfig.toml; onlycelindor env <name>changes it, and it asks first for test and prod. - cli:
celindor migrateprints the notices a migration raises, under its name. Migration 0206 uses one to say which large indexes still need rebuilding by hand.
Safer image cleanup between deploys, payload check covers renamed files (#1443)
- deploy: a deploy holds a host-wide image lock from its pulls until the daemon runs the new tag, and the image cleanup waits for it, so one environment’s cleanup can no longer remove the sandbox image another environment’s deploy has just pulled.
- deploy: the Jev shadow runbook now says it does not work on the compose deployments yet, as the entrypoint rewrites
config.tomlon every start. - ci: the payload check (#142) also scans lines added to renamed or copied files.
Steadier turns, safer tools and fewer false alarms (#1442)
- daemon: A turn no longer ends silently when the agent’s closing reply comes back empty or cut off, and a reply the user already read is kept when the model then answers with nothing. A chat whose history failed to load once loads it again on the next message.
- daemon: A self-hosted or other OpenAI-compatible chat endpoint now times out after 10 minutes even while a turn is running, and an oversized reply from it is cut off instead of read whole.
- daemon: An emoji with joiners (a family emoji), Persian text and Hebrew with direction marks are no longer mistaken for hidden instructions, so such memories and names are saved.
- daemon: Web search results from DuckDuckGo keep their real address, and
web_fetchkeeps everything the fetched server chose inside the untrusted block. - daemon: Sandbox file reads and writes time out after 30 seconds; the sandbox’s outbound proxy closes connections over its per-sandbox limit and closes idle tunnels; the command floor catches destructive commands after
then,do,{or!. - daemon: With
log_for_distillationon, Claude no longer reranks the Today tab on the user’s key, a decision whose shadow is also its fallback asks Claude once, and dream review’s enforce mode sends the items only the Claude fallback answered to the full review instead of acting on them.
Admin console: org memory filters and errors, tool grants, dark mode, sub-account graph (#1441)
- admin: on an organization’s Browse Memory page, the kind filter stays after you pick a kind, so you can go back to All kinds without reloading.
- admin: the organization memory overview, scopes and browse pages, and a scope’s members dialog, show an error with Try again when a request fails, instead of “No scopes yet”, “No entities found” or an empty member list; an expired session goes to sign-in.
- admin: Always-allow tools asks for the full tool name (
mcp__celindor-sandbox__exec_bash), suggests the tools a grant applies to, and refuses a bare name such asexec_bash, which was stored but never took effect. - admin: in dark mode, text on blue, green and red buttons is dark and readable, and error banners, toasts and badges no longer sit on a light pink background.
- admin: the sub-account graph draws each nested account under its real parent.
Chats and threads: fixes from the November audit (#1440)
- flutter: a reply picked up again after a lost connection shows in full and in order, not with a gap where the connection was down.
- flutter: a long reply that keeps writing is no longer cut off after 5 minutes; the app now gives up only after 5 minutes with no sign of the reply, and shows “stalled” when it has gone quiet.
- flutter: a message sent while a reply was being written, then lost when the app closed, now shows as “Not sent” with Retry and Discard instead of “Waiting to send” for good.
- flutter: a proactive message, or another device’s message, finishing in a chat no longer stops the spinner and Stop for your own reply still running there, and its text no longer runs into your reply.
- flutter: messages a delegated agent or a mission step put in a chat keep their label after each reply.
- flutter: opening a sent file from a chat left open for a while no longer opens an expired link.
- flutter: “Remind me” on a deadline card says so when the reminder could not be set.
- flutter: files waiting in the composer move with you to another chat, and adding several files with no chat open starts one chat, not one per file.
- flutter: reopening a chat lands exactly where you left it on the phone and Mac apps too.
- flutter: a delegation to a household agent shows that agent, and the task’s chat opens from its card.
- flutter: editing the chat’s memory proposals no longer offers to reword a relationship (it was saved unchanged), and emptying a proposal leaves it unsaved, in To review.
- flutter: a recording’s player in the side panel keeps showing it is playing after scrolling away and back.
- flutter: a thread open in a hidden window is no longer marked read when the connection comes back.
- flutter: tapping the voice message button twice quickly no longer leaves a second recording running, and a double send no longer fails.
Deleting things takes what it should, and keeps what it should (#1439)
- daemon: The copy Celindor’s records keep of a person or thing you forgot is erased 30 days later, and can no longer be brought back after that.
- daemon: Permanently deleting a document or recording, or editing or unsending a household message, no longer deletes a person or thing that was there before it, that it only added to, or that you have added to yourself since. Two people or things only that document taught, linked to each other, now go with it, and unsending a message that taught a connection no longer fails.
- daemon: To-dos a recording suggested are hidden while the recording is in the Trash and removed when it is deleted permanently; to-dos you accepted stay.
- daemon: A suggested connection from a chat goes when the chat is deleted, like the other suggestions from it.
- daemon: When someone leaves or is removed from a household or team, the documents they shared with it are no longer shared.
- daemon: Deleting a member’s account no longer lowers what the organization has spent this month, and the record that someone opened a Sealed division stays after their account is deleted.
- daemon: A deleted division no longer holds on to its name, and renaming one to a name another division already has says so instead of failing with a server error.
- daemon: The limit of 10 organizations per owner now also holds when restoring a deleted one or taking one over by transfer.
- daemon: The dream worker no longer drops an episode whose title another episode already has; it gets a numbered name.
- daemon: Memory search indexes are re-sized every night as memory grows, so search stays fast on a server that started empty.
Households, agents and missions: fixes from the November audit (#1438)
- flutter: Switching an agent between your memory and its own as often as you like keeps working: a taken name gets a number (“Coach (agent 2)”) instead of failing, and a memory space made for a save that then fails is removed again.
- flutter: Tapping an agent in an organization’s agents list (“N more” or “Manage agents” in its sidebar section) opens its latest chat instead of a new empty one.
- flutter: A division’s monthly budget is set in the same dialog as the organization’s: No limit removes it, and an amount outside 0 to 1,000,000 dollars is refused with a message.
- flutter: Accepting, leaving, deleting or restoring an organization in the app updates the shared threads and the chat list at once.
- flutter: Only the owner is offered “admin” when inviting someone; admins invite members and viewers.
- flutter: “Who may ask” on a Sealed division shows the switches only once the list has loaded, says so when it could not be loaded, and waits for each change before the next, so the stored list is never overwritten from an empty one.
- flutter: A division’s admin who isn’t an admin of the organization is offered a posture change only where the server allows it: taking the division out of Sealed.
- flutter: Leaving an organization while you are the only admin of a Sealed division or space now says so, instead of saying you own the organization.
- flutter: Sharing a division’s fact with an empty wording is no longer possible (it used to share the proposed wording), and a fact that is gone says so.
- flutter: Today no longer counts household missions that another member answers as “waiting for you”.
- flutter: A new mission starts from your saved default budget even when Settings → Missions was never opened.
App fixes from the November audit: library, recordings, staging and more (#1435)
- flutter: The library shows why a document failed, marks a document whose text could not be read as Not searchable instead of Ready, and has a Reprocess button for both.
- flutter: Files whose upload was cut short by the app closing are no longer dropped without a word: one already sent is finished, and the library says which others to add again.
- flutter: A data export or import that loses touch with Celindor keeps checking and says so, instead of spinning until the screen is reopened.
- flutter: Your Data credits your own OpenAI key for indexing when you have one.
- flutter: A failed calendar disconnect says so.
- flutter: The Memory tab’s dream failure notice appears and clears while the app is open, and one missed refresh no longer throws away your place in the memory graph.
- flutter: Recordings waiting to upload are found again after an iOS update moves the app’s files; a microphone that will not start or a refused retry no longer reads as a failed upload; the review screen follows a summary still being made and offers Regenerate again if it stalls.
- flutter: To review: the sidebar badge, Today and a chat’s suggestions keep counting the waiting queue after a look at Saved for later, a suggestion reopened on another device shows up, and Later is no longer offered for what is already saved for later.
- flutter: A refused to-do change keeps its error message on screen.
- flutter: Cmd/Ctrl+N and a chat picked from Cmd/Ctrl+K bring the chat page forward; the web no longer hints at Cmd/Ctrl+N, which browsers keep for a new window.
- flutter: Invitations and household agents are fetched again when you come back to the app or open Settings.
- flutter: The Migration plan screen has its own title again, the OpenAI key no longer claims to pay for Whisper transcription, and several sidebar, voice, review and memory-graph buttons can be pressed with TalkBack, web screen readers and macOS VoiceOver.
- manuals: Cmd/Ctrl+N is for the installed apps; the OpenAI key is for search.
Memory keeps facts apart, and deleted groups are gone everywhere (#1434)
- daemon: two different facts said within a few minutes, such as two phone numbers, “is” and “is not”, or who owes whom, are both kept; one is no longer dropped or written over.
- daemon: saying a relationship again keeps how you filed it and what else is known about it.
- daemon: a summary of events formed from a private or hard memory space stays in that space and is no longer shown in your other chats.
- daemon: a fact about someone whose suggestion is not saved yet waits for your review instead of being saved about nobody.
- daemon: a deleted household or team no longer lends its AI key, and is no longer listed to the assistant as a group you belong to.
- daemon: the Inbox no longer offers relationship suggestions from a document or recording in the Trash, and its counts leave them out too; a household’s suggested relationships show the people’s names, and one you already dismissed or saved is never pulled back.
- daemon: the “may duplicate” hint in the Inbox never points at someone you took back, and a household suggestion no longer stops a personal one for the same name.
- daemon: a document whose file changed (a new Google Drive version, a reprocess) while it was being read is read again from the new file, instead of keeping text from the old one.
- daemon: sharing a document with people works; adding a sub-account to a full or deleted group is refused instead of making it outside the group; a deleted group is announced to its members only once it is really gone.
- daemon: an import archive is checked against the 2 GB limit each time it is read, and one uploaded after its import was cancelled is still deleted.
- daemon: “Record explanations for retrievals” now records them in the Memory Log; the assistant finds a division by the name it is shown, and asks instead of guessing when two of your groups share a name.
The manuals match what the app and server do (#1433)
- manuals: The user manual no longer calls your memory encrypted: it is private to you, but Celindor does not encrypt it separately, as the privacy policy says (your keys and skills are encrypted). The administrator manual says a sub-account can have sub-accounts of its own, sends owners and admins to Organization budget › Monthly budget on the organization’s screen instead of the API, describes Memory history, and no longer lists it, or space admins managing their space’s access, as missing from the app.
Sessions, sign-out and settings errors behave (#1432)
- flutter: a “not allowed” answer from Celindor (for example approving a review item you no longer have write access to) no longer says your session expired, and no longer signs the app out.
- flutter: on the web, signing out while Celindor cannot be reached keeps you signed in and says so, instead of showing the sign-in screen while a reload would sign you straight back in.
- flutter: coming back to the app on the invite-code or onboarding screen no longer starts the chat connection or asks for notification permission.
- flutter: finishing account setup signs you in even if the connection blips right after; before, the used setup link showed an error and could not be tried again.
- flutter: a draft you just typed is no longer replaced by an older copy when the app refreshes from the server.
- flutter: settings screens show their errors in your language instead of in English.
Memory, import and household chat fixes from the November audit (#1431)
- daemon: “my wife” and other relationship names stop naming a person once you mark that relationship not true, and a relationship name you add by hand now works.
- daemon: merging two entries for the same person keeps the nicknames, old names and relationship names of both, and keeps a relationship one of them still had when the other had forgotten it.
- daemon: a fact you corrected can no longer be marked not true and then brought back by Restore, and undoing a forgotten person no longer loses the link between their corrected facts.
- daemon: sharing a person with your household no longer copies the facts you marked not true.
- daemon: long documents are now indexed for search on installations that use OpenAI for embeddings, instead of failing with a message about your API key.
- daemon: reading back an earlier conversation no longer shows the found message twice and drops the one after it.
- daemon: a data export whose files cannot be read from storage now fails instead of staying “in progress” for good.
- daemon: importing an older ChatGPT export with a conversations file over 128 MB now says so instead of showing nothing to import; OpenClaw archives with a file that merely ends in
state.db, a repository inside the workspace, or two workspaces now import every file once. - daemon: a voice message in a household chat whose transcription was cut off by a server restart is transcribed again, and Try again works on it.
- daemon: an owner or admin who opens a Sealed division’s chat through a break-glass can read it but can no longer post, react, pin, unsend members’ messages or change who the agent answers there.
- daemon: looking up a shared person now tells the assistant which division a fact is in and who may read it, instead of saying every member can.
API route fixes from the 2026-11-10 audit (#1430)
- daemon: an operator who deletes an organization through the admin API now makes the same restorable 30-day delete the owner does, and its members are told. The operator’s member and key routes act only on an organization the user in the path owns, and the record of a member or sub-account change names the person it touched.
- daemon: the admin API no longer has routes that set or remove an organization’s or a sub-account’s API key, as SECURITY.md and the manual already said. Owners and parents still manage those keys themselves.
- daemon: wrong passwords sent all at once are counted before they are checked, so they no longer slip past the limit of five. Changing a password and redeeming a claim code have a request ceiling, like signing in.
- daemon: making sub-accounts, setting up an account from its link and asking for a new confirmation link share a ceiling of 30 mailed confirmations a day. The confirmation mail shows the names it quotes on one line. A thread photo or file counts toward the uploads ceiling, as a chat’s does.
- daemon: signing in by password, by link from the apps, or from a setup link accepts only a JSON body, so another site’s form can no longer sign a visitor into an account of its choosing. A spent or unknown sign-in link now lands on the web app’s start page instead of a page that doesn’t exist.
- daemon: a second CalDAV calendar on another server with the same username is added as its own account instead of replacing the first.
- daemon: renaming a space or division checks the name as creating one does: a blank name is refused and a name already in use is a conflict, not a server error. Sealing a space you already read or write in makes you its admin instead of failing and leaving it with nobody to look after it.
- daemon: a malformed organization, recording, document or memory space id in a request is “not found”, not a server error.
- daemon: moving a sub-account from one household to a full one is refused before it leaves the first, instead of leaving it in neither.
- daemon: confirming a document upload again after the server has started reading it succeeds, so an app whose first confirmation got lost no longer reports a failed upload.
- daemon: a watch sign-in code stops working when the phone that asked for it is signed out, so a password change or an operator’s reset leaves no code that can still sign a watch in.
- daemon: a chat whose memory went to a household or team you can no longer write to shows personal memory, which is where its memories then go.
- contract: the org-agent update, delete and retire routes declare the 409 they answer; scope rename declares its 409; the routes with a new ceiling declare their 429.
A reply that finishes while the connection is down no longer leaves the chat stuck (#1428)
- flutter: when the app reconnects or comes back to the front, a reply that finished in the meantime now ends the turn: the spinner stops, messages that were waiting to send go out, the chat is marked read again, and the false “Reply took longer than 5 minutes” error no longer shows.
Database failures answered and logged (#1427)
- daemon: a request that fails in the database (a rejected query or a lost connection) now gets the API’s usual
{"error": "internal error"}answer with its security headers, and the server logs it with the request and the Postgres error code. It used to be an empty 500 with nothing in the log.
The GitHub tools work on the server (#1426)
- daemon: the agent’s GitHub tools (list repos, commits and pull requests, show a commit, PR, diff or file, search code) now call GitHub’s API directly with the user’s saved token. They used to run the
ghprogram, which the server image does not contain, so they always failed with no reason given. A failure now says why, e.g. “401 Bad credentials” or “404 Not Found”.
Programs built or installed in the sandbox run (#1425)
- daemon: the agent’s sandbox can now run what it writes: a compiled
program in the workspace, a script started as
./script.sh, and the command line tools thatpip installandnpm install -gput on its path. Before, the sandbox’s writable folders refused to run anything stored in them. They still refuse setuid programs and device files.
Reconnecting a Gmail or Outlook mailbox takes effect at once (#1424)
- daemon: reconnecting a mailbox that was still connected now switches it to the new sign-in straight away, instead of keeping the old one until the server restarted. The old sign-in can no longer overwrite the new one when it renews its access. The same goes for a new IMAP password.
A reminder set in a chat you later deleted still reaches you (#1423)
- daemon: When the chat a reminder was set in has been deleted, the reminder now arrives in another of your chats instead of being lost. A repeating reminder does so every time it is due.
A follow-up sent while the household agent is answering gets its own answer (#1422)
- daemon: a message to the household agent sent while its turn was still running was queued but never answered: the next turn decided what to answer by where the agent’s last reply sat, and that reply is stored after the follow-up. Each thread now remembers the newest message its last answered turn covered, so everything sent after it (a burst of follow-ups, or any message in an “answer every message” thread) gets one answer that sees all of it, and nothing is answered twice. A failed turn still leaves its messages for the next one.
A team’s front desk can be edited again (#1421)
- flutter: Edit on a team’s front-desk agent no longer crashes the dialog. Owners and admins can rename it and change its persona and image; it shows no memory choice, since a front desk stays outside every division.
Invite codes with digits can be typed (#1420)
- flutter: the invite code field now takes digits as well as letters, so every code your operator issues can be entered. Case doesn’t matter, spaces and dashes are ignored, and I and L are read as 1 and O as 0, as the server does. Continue turns on once all 8 characters are in.
- manuals: the user manual and quick start say the code is 8 characters of letters and digits.
Organization chats keep their composers apart (#1419)
- flutter: switching straight from one organization chat to another in the sidebar no longer carries the composer along: a voice message recorded in the new chat goes to that chat instead of the previous one, and a half-typed message, attached files, mentions and the Ask-agent choice stay behind.
Sub-organizations are deleted first, never swept away with their parent (#1418)
- daemon: deleting an organization that still has sub-organizations is refused with a 409 (
has_sub_organizations): delete those first. It used to delete only the parent, leaving the sub-organizations live until the purge 30 days later erased them with no notice and no way back. A deleted sub-organization can only be restored once its parent is (409,parent_deleted). The database holds both rules (migration 0254), the operator’s immediate delete is refused the same way, and the purge skips, with a warning, any old deleted organization that still has a live sub-organization. - app: deleting a household that has sub-organizations, or restoring one whose parent is deleted, says what to do first.
- admin: the delete confirmation no longer promises to delete sub-organizations, and a refusal says to delete them first.
Long recordings keep every sentence (#1417)
- daemon: a recording over 12 minutes, transcribed in overlapping parts, no longer loses a sentence that follows a short one with the same opening (such as “Okay.” then “Okay, so the budget is…”) anywhere after the first part. Only a sentence heard twice where two parts overlap is removed.
A rate limit that mentions billing is retried, not reported as out of credit (#1416)
- daemon: a 429 or 5xx counts as out of credit only when the provider says so by code (OpenAI
insufficient_quotaorbilling_not_active, Anthropicbilling_error) or answers 402. Gemini’s per-minute limit (“check your plan and billing details … retry in 27s”) and OpenAI’s low-tier limits, which link the billing page, are now waited out, retried and fallen back from, instead of ending the turn with “the account is out of credit”. A failed memory pass shows the same difference.
Outlook mail is learned and read in full, not just its first lines (#1415)
- daemon: HTML mail from Outlook (nearly all of it) was learned and shown to the agent as Outlook’s 255-character preview. Its whole body is now converted to text; the preview is used only for a message that has no body at all.
An incognito chat survives a network blip (#1414)
- flutter: losing the connection for a moment, or changing the AI provider or one of its keys, no longer throws you out of an incognito chat and deletes it. The chat and its messages stay on screen; it is still deleted when you leave it.
A forwarded message can’t be edited (#1413)
- flutter: Edit is no longer offered on a message you forwarded into a shared chat: it still shows whose words they are, so they stay theirs. You can still delete it.
- daemon: editing a forward is refused (
forbidden), both by the thread store and by theedit_thread_messagedatabase function, so a forward’s words and its “Forwarded from” can no longer disagree and an edit never runs extraction on a forward.
A member’s own chats on the organization’s key count against its budget (#1412)
- daemon: a member with no Claude key of their own, viewers included, still runs on the organization’s key, but that use is now the organization’s: their own chats, missions and delegated tasks, and the background work for them (memory extraction, dreams, the message classifier, imported documents and recordings), are recorded against the organization and count against its monthly budget. Before, they were recorded as the member’s own key, so no budget or monthly limit applied and any member could spend the key without end. Once the budget is used up, those chats stop too and say why. No division’s budget counts them: a member’s own chat belongs to none.
Memory work runs on the AI provider you chose (#1411)
- daemon: picking out what to remember from messages, documents, pictures and recordings, and the dream cycles, now run on the provider you chat with (Claude, Gemini, OpenAI or another OpenAI-compatible service), with your key for it. Before, they went to Anthropic whenever any Anthropic key was available, whatever you had chosen. Without a key for your provider that work waits, as before; it is never sent to another provider. The message classifier, the agent’s wake-up check and the decision layer’s Claude provider now run only for people who chat with Claude.
- manuals: the AI provider section says the provider also does the memory work.
A long channel message can no longer stall the daemon (#1410)
- daemon: finding where a forwarded message starts in a mail sent to the agent took minutes on a mail of many blank lines, and held up every user meanwhile. It now reads the mail line by line, in linear time.
- daemon: a message from a chat channel (email, Telegram) reaches the agent cut to its first 100,000 characters, with a note that the rest was cut.
- daemon: reading the From, To and Cc headers of Gmail mail no longer slows down on a header with a long run of spaces or commas.
A page or document full of spaces can no longer freeze the server (#1409)
- daemon: Outside text (web pages, documents, emails, repository files, command output) is checked for forged tags and injection phrases in time that grows only with its length. A “<” followed by a long run of spaces used to take seconds per page, during which the whole server stopped responding.
A Word or Excel file with a hidden second table of contents can no longer slip past the zip-bomb check (#1408)
- daemon: the zip-bomb check now reads a Word or Excel file’s table of contents exactly where the Word reader does, and refuses a file whose table of contents does not add up. A 65 KB .docx with a second, hidden table of contents used to pass the check and then expand to 64 MB in the reader.
- daemon: the size limits (32 MB per part, 100 MB in all, and the expansion ratio) now also hold while the Word and Excel readers unpack a file, counting the bytes as they come out, so a file the check reads differently from the reader still cannot expand past them.
- daemon: files that start like any zip variant the Excel reader accepts are checked too, not only the two most common ones.
The sandbox’s Docker proxy refuses a request body that repeats a key (#1407)
- daemon: The Docker proxy refuses a container, exec or network request whose JSON names the same field twice, or in two spellings Docker reads as one, and sends Docker the request it checked rather than the bytes it was given. Before, a create that sent
HostConfigtwice was checked on the second copy while Docker also applied the first.
A password change stops notifications to signed-out devices (#1406)
- daemon: changing your password, or an operator resetting it, now also stops push notifications to every device it signs out — phones, browsers and watches. Before, a signed-out device kept getting message previews.
- app: the device you change your password on keeps getting notifications.
Deleting a shared memory entry no longer erases other divisions’ facts (#1405)
- daemon: Deleting an entry from an organization’s shared memory through the API now removes it the way the agent’s forget does: it is archived with what was recorded about it, can be restored from Memory history for 30 days, and shows in the history as removed. It used to be deleted outright, together with every fact about it in every division, Members-only and Sealed ones included. An entry linked to a member’s account is refused (409).
- daemon: Removing an entry, through the API or the agent’s forget, is refused while a division the member may not write still records something about it. Nothing is changed then.
- daemon: The delete route is on the API contract now.
A chat’s memory goes only to a division of the organization you picked (#1404)
- daemon: pointing a chat’s memory at an organization now refuses a division that belongs to another organization (404), and the organization memory access checks give no access to a division named under an organization it is not part of, so the owner or an admin of one organization can no longer file memory under another organization’s division.
Confirming an email address takes a button press (#1403)
- daemon: the link in Confirm your address for Celindor now opens a page with a Confirm button, and only pressing it confirms the address. Mail scanners that open every link in a message can no longer confirm an address someone else typed for their own account.
The to-do add button waits for a title (#1402)
- flutter: on To-dos, the plus button next to “Add a to-do…” stays dimmed until you have typed a title, so it no longer looks like it does something with an empty field.
Suggestion filters tell the truth, and a suggestion can be someone and you (#1397, #1398, #1399)
- flutter: On Suggestions for memory, a chosen filter is filled and ticked, so you can see what the list is narrowed by. Each filter’s number is how many you would see with it added to the others, so Organizations (3) no longer leads to an empty list. A chosen filter stays visible even at 0, and Clear filters (also under “Nothing matches the filter”) shows everything again.
- flutter: Same as something else… lets you tick This is me together with the entry you pick, when that person is you. Both become one entry in memory, and the suggestion is merged into it.
Deploys no longer leave tool sandboxes running (#1367)
- daemon: Shutting down removes every sandbox container the daemon started, including those of sessions still being set up, and retries while the Docker proxy cannot be reached instead of taking that for “already gone”.
- daemon: On starting, the daemon removes its own environment’s sandbox containers that an earlier daemon left running, older unlabelled ones included, and never another environment’s.
- deploy: A deploy now stops the daemon before it replaces the Docker proxy, so the daemon’s shutdown can still reach it.
A tool shows as soon as the agent picks it (#1366)
- flutter: while the agent writes out what a tool should do (a long note to remember, say), the line under the reply already says which tool it is using, instead of waiting until it has finished writing. If the reply is cut off before the tool is used, the line moves on as before.
- daemon: Claude’s stream names a tool call when it starts; the new
tool-pendingevent on the user stream carries the name ahead of thetool-call, which still follows and has the last word. OpenAI-compatible and Gemini requests do not stream, so they send none. Older apps ignore the event.
The first message after a quiet spell gets its memories (#1364)
- daemon: An instance that embeds with its own service now keeps that service warm with a short request every minute. Before, the first chat message after the service had sat idle waited 2.5 seconds for its memories and then went on without them, because the service took ten seconds and more to answer. A slow answer to the short request is logged (
[embed-warm]).
Mission steps, reminders, introductions and channel replies no longer lose their chat to a busy moment (#1363)
- daemon: A mission step, a reminder’s work, a new agent’s introduction and a reply to a chat channel now hold their chat before its session starts, as a delegated task does, so another chat starting at the same moment can no longer close that session under them.
Talk to your agent on your Apple Watch (#1360)
- flutter: the watch app’s signed-in screen is now a talk screen. Tap the microphone, speak, and tap again to send (it sends by itself after a minute). The watch shows what it heard and reads the answer aloud sentence by sentence, in the answer’s language, through its speaker or your headphones; the answer is shown as text too. Tapping while Celindor thinks stops the answer; tapping while it reads stops reading. The watch taps your wrist when it listens, sends and answers.
- flutter: everything said on the watch goes into one chat, Apple Watch, the same from all your watches and listed with your other chats on the phone and the web. The agent has the same memory and tools there as in any other chat.
- flutter: when the stream of the answer drops, as watchOS may do, the watch reads the finished answer from the chat instead. The watch also says when there is no connection, nothing was heard, the account has no AI key, the answer failed, or a question has to be answered on the phone.
- daemon:
POST /api/v1/user/voice/transcribereturns the words in a short recording (at most 1 MiB), with the same Gemini transcription as household voice messages, on the user’s key or else the operator’s. The audio is never stored. Rate-limited as “spoken input”. - daemon:
POST /api/v1/user/watch/chatreturns the account’s watch chat, making it on first use.
Sign your Apple Watch in from your iPhone (#1359)
- flutter: on an iPhone with an Apple Watch, Settings has a new row, Set up Apple Watch. Tap it and Celindor on the watch is signed in to your account within a few seconds, with nothing to type on the watch. If the watch app is not open, open it within three minutes.
- flutter: the watch app now shows who is signed in. It has a sign-in of its own, so it also works with the iPhone left at home, and signing out on the phone leaves the watch signed in. Sign the watch out under Settings › Apple Watch on the iPhone. Talking to your agent from the watch comes later (#1355).
- flutter: the watch keeps its sign-in in its keychain, and goes back to the sign-in screen when the server no longer accepts it. Being offline does not sign it out.
An Apple Watch app, to begin with (#1358)
- flutter: Celindor now comes with an Apple Watch app. It installs with the iPhone app from TestFlight and also runs with the phone left at home. So far it shows one screen: the app’s name, its version and the server it talks to. Talking to your agent from the watch comes later (#1355).
- build: the watch app is a SwiftUI target in the iOS Xcode project. It is built, signed and uploaded with the iOS app in the same workflow run, and needs a provisioning profile of its own for each app (RELEASING.md).
The server is ready for an Apple Watch app (#1357)
- daemon: a signed-in device can ask for a one-time code that signs an Apple Watch in to the same account, so nobody types an address and a password on the wrist. The code lasts three minutes and works once. The watch gets a session of its own: signing out on the phone leaves the watch signed in, and the other way round. A sub-account’s watch is signed in as the sub-account. A watch that is used stays signed in for up to 90 days; one left unused for a week is signed out.
- daemon: an account can list the watches signed in to it and sign one out, which also stops that account’s watch notifications.
- daemon: a watch app registers for notifications as its own platform,
watchos, and is sent to through APNs under the watch app’s own bundle id. Migration0247.
A task handed to another agent no longer loses its chat to a busy moment (#1336)
- daemon: A delegated task now holds the target agent’s chat before its session starts, so another chat starting at the same moment can no longer close that session under it.
Attachments of a very large mail open again (#1335)
- daemon: The agent can open the attachments of an IMAP mail larger than 10 MB again. It downloads only the one attachment asked for, never the whole mail, and the usual 20 MB limit per attachment applies.
Shared chats learn what is said in voice messages (#1332, #1014)
- daemon: in a household or team agent’s shared chat, a voice message’s words are learned into the organization’s memory once transcribed, under the sender and with their write rights, as a typed message is. A member who may only read memory adds nothing. A transcription that failed or heard no speech adds nothing, and a forwarded voice message stays unlearned. Unsending the message takes back what was learned from it, and editing its text learns its words again with the new text.
The keyboard no longer covers the email field when setting up an account (#1328)
- flutter: the sign-in, setup-link, invitation and connect pages now end above the on-screen keyboard and scroll the field you are typing in into view, so the email on a setup link is no longer hidden behind the keyboard. On hosted builds, Go on the keyboard now submits the setup form.
A new household can be picked for a chat’s memory straight away (#1327)
- flutter: a household made or joined while the app was running did not show up in a chat’s memory picker until the app was restarted, and someone who made their first household did not see the picker at all until then. The list is now read again each time a chat or the picker opens.
A new household shows in the list straight away (#1327)
- flutter: after making a household or team and going back from its screen, Settings › Household could leave it out until you left and came back. The list is now read again each time you come back from an organization’s screen.
Organizations’ agents gather under Team agents (#1322)
- app: the agents of your households and teams are now one foldable sidebar section, Team agents, below your own agents, with each organization’s name above its agents. Previously each organization was its own section headed only by its name, which did not say it held agents. Folded, the section shows what is unread across all of them.
The iPhone app no longer crashes when it opens (#1319)
- flutter: the app on iPhone crashed straight after opening, including when a sign-in link opened it. It now starts normally.
The web app no longer keeps a sign-in token in browser storage (#1307)
- flutter: a web build that talks to an API on another origin (a development setup; the hosted web app uses its session cookie) keeps its sign-in token in memory in a release build and in the tab’s session storage in a developer build, never in
localStorage, and deletes a token an older build left there.
Reading a very large mail no longer loads it whole (#1295)
- daemon: When the agent reads a mail larger than 10 MB from an IMAP mailbox, it reads only the start, lists the attachments by name, and tells you the mail was too large to read whole. Its attachments cannot be opened.
The agents you use come first (#1294)
- flutter: The sidebar and the agents list put your agents in order of their latest message, newest first, so the ones you talk to most and an agent you just made are on show instead of under “N more”. Agents you have not talked to yet go by when they were made. Organization agents keep their order.
Deleting a division no longer renames an entry for a removed twin (#1293)
- daemon: When a division is deleted, its entries are renamed after the division (for example “Acme (Kitchen)”) only if the shared memory has a live entry of the same name. An entry that was removed from the shared memory no longer counts, so the division’s entry keeps its name and the removed one stays in the trash.
Set a first password from the app (#1274)
- flutter: if you have only signed in with emailed links, Settings › Password now reads Set a password and asks only for the new one, instead of a current password you never had. Accounts that already have a password see Change password as before.
- manuals: the user manual (EN, SV) explains setting a first password.
Connecting a calendar, Google Drive or a mailbox works from the web app again (#1267)
- daemon: a connect started in the web app now comes back to
/#/connect, the address the web app can read. It came back to/connect, which landed on the start page, so nothing was connected and no error was shown. The native apps’ link is unchanged. - daemon, app: a Google Drive connect now says where it started (
return_to), as calendar and mail do (migration 0166).
Outlook sync no longer skips mail or drops attachments after a failed fetch (#1240)
- daemon: when Outlook fails to list a new message’s attachments, the sync now stops just before that message and tries it again on the next run, instead of skipping it for good or storing it without its attachments; a message deleted in Outlook meanwhile is passed over.
To-dos are up to date and can be edited (#1219, #1220)
- flutter: To-dos fetches the list again each time you open it and when you come back to the app, so to-dos added in a chat show at once. Pull to refresh works however short the list is.
- flutter: tapping a to-do’s text opens an editor for its title and due date instead of ticking it off; only the checkbox marks it done.
Agents open their chat from the agents list (#1218)
- flutter: Tapping an agent in the agents list (Manage agents, or “N more” in the sidebar) opens your latest chat with it again, instead of a new empty chat that seemed to have lost everything said before. “New chat with …” still starts a fresh one.
Long press on an organization agent starts a private chat (#1208)
- flutter: A long press (or right-click) on an organization’s agent in the sidebar starts a new private chat with it for members; editing and deleting stay with the organization’s admins. The one-row layout itself is described under #1119.
Undo in the Memory Log takes back corrections and forgets (#1194)
- daemon: Undoing a correction now removes the new fact and brings back the old one, undoing a forget now restores what was forgotten (with its facts, relationships and alternative names), and an older entry that can’t be undone says so instead of reporting success.
Slow memory searches stop when the reply moves on (#1164)
- daemon: When a chat or household turn stops waiting for its memory search after 2.5 seconds, the database now stops the search too, instead of letting it run on and use up connections under load.
Deleting an account also deletes its sub-accounts’ sign-ins (#1163)
- daemon: Deleting an account now deletes the sign-in (email and password) of every sub-account below it in the same step, instead of leaving them behind.
Older chats open again for people with more than 50 chats (#1162)
- daemon: Opening an older chat no longer says “not found” when you have more than 50 chats, and an admin can delete any of an entity’s facts, not only its 500 newest.
Owners and admins see and change the organization’s monthly budget in the app (#1159)
- app: an organization’s screen shows its owner and admins this month’s spend on the organization’s key against its monthly budget, next to the divisions. Tapping it changes the amount, or sets it to no limit, so a household that has reached its limit no longer stays stopped until next month. Members don’t see it. In English and Swedish.
The app asks you to confirm your address, with a resend button (#1158)
- flutter: after you set up an account from a setup link and a confirmation mail goes out, the app tells you to check your inbox. While your sign-in address is unconfirmed, home and your profile say so and explain why sign-in links don’t reach it yet, with a button that mails a new link. It says so when too many links are already open or when the server can’t send email. The notice goes away once the address is confirmed.
- daemon: the session (
GET /api/v1/auth/session) says whether the signed-in address is confirmed (email_verified).
Deleted organizations are erased even when others fail to purge (#1139)
- daemon: an organization whose purge fails now waits an hour before the next try, doubling up to a day, so organizations that keep failing no longer stop newer deleted ones from being erased after their 30 days. A purge is never given up on; after five failures each one is logged as a warning for the operator, with the last error kept on the organization (migration 0183).
Notification settings do what they say (#1138)
- daemon: “Dream cycle completed” now sends a push when a dream has looked at something, saying what it saved and what waits for your review, in English or Swedish. Quiet hours now cover every push, reminders, proactive messages, missions and division answers included, and hold them instead of dropping them: when the window ends you get the latest per chat, or one summary after a busy night.
- flutter: The quiet hours footer says what happens, and tapping a finished dream’s notification opens Memory.
Pushes and emails in your language (#1137)
- daemon: the notifications and emails Celindor writes itself now come in Swedish when your app is in Swedish: “Att granska” after a dream, a mission that is done, failed or needs your OK, a division’s answer to your question, the sign-in link and household invitations. A mission asking for your OK says what it wants to do (“läsa en webbsida”) instead of the tool’s internal name. An invitation is written in the language of the person who sends it, and a sign-in link in the language of the app that asked for it.
- daemon: the reply to someone who writes from a channel that is not linked yet comes in English and Swedish, and names the screens as the app does in each (“Inställningar → Länkade kanaler”).
- flutter: the app tells Celindor its language when it registers for notifications and when it asks for a sign-in link.
Email channel fixes (#1135, #1136)
- daemon: with
CHANNEL_EMAIL_AUTHSERV_IDset, anAuthentication-Resultsheader the sender wrote into the mail no longer passes the sender check. Only the topmost header carrying the server’s name counts. - daemon: unlinking a channel with a malformed id returns 404 instead of 500.
Chat channel replies and turns stay with the person who wrote (#1133, #1134)
- daemon: the email channel’s reply always goes to the sender of the mail it answers. Someone else, such as a person the user CC’d, could reuse the user’s Message-ID while the agent was answering and receive the user’s reply instead.
- daemon: quick messages are merged into one turn only when one person wrote them. Mails from several people in one thread arriving together used to be merged and run as the linked user’s own words (a CC’d person’s instructions included), or the user’s message was dropped.
A hostile link can no longer freeze the server (#1132)
- daemon: Link previews in household threads read a page’s title and description, and find the link in a message, in one pass. A page of unclosed tags, or a link followed by thousands of brackets, used to stop the whole server for 6 to 30 seconds on every send, edit or forward.
Talk to your agent in Telegram (#1123)
- flutter: Settings › Linked channels shows the server’s Telegram bot, with Open in Telegram: it opens the bot, and tapping Start there links your Telegram account in one tap. Each channel has its own icon, and a linked Telegram account shows by name. The screen’s text covers both email and Telegram.
- daemon: a Telegram channel through the operator’s own bot, one per
deployment, set with
CHANNEL_TELEGRAM_TOKEN. Replies stream into one message,/new,/stopand/helpare in the bot’s menu in English and Swedish, and only direct chats are answered. A new route hands the app a one-tap link that works once, for ten minutes; typed codes still work.
Chat channels get /stop, /help and the busy setting (#1122)
- daemon: on the email channel, and every channel after it,
/stopstops the reply being written and/helplists the commands, next to/new. The answers are in the user’s language. A message sent while the agent is still answering follows Sending while Celindor replies, as in the app: it stops the reply, waits, or is added to it. A command mailed from a phone counts even with “Sent from my iPhone” under it. - daemon: each
[channels.<name>]section in the config starts its channel through that channel’s factory, and a channel’s code is loaded only when its section is there. Replies stream into one message on channels that can edit messages. The channel list in the API can now carry a link that opens the channel (url, e.g.mailto:).
Agents without a picture are easier to tell apart (#1121)
- app: an agent without a picture shows two letters instead of one (Sales “Sa”, Support “Su”, HR “HR”), so agents that start with the same letter no longer look alike. The badge that marks an organization’s agent is larger and sits clear of the letters, and a team’s agents get a team badge instead of the house.
Each household and team has its own section in the sidebar (#1120)
- app: an organization’s agents are a foldable section under its name, below your own agents. A team shows its front desk first, as Ask …, then its agents under their division’s name, with a lock on Sealed divisions. Folded, the section shows what is unread in it. Its admins get Manage agents for that organization only, and a long press on an agent edits or deletes it. With the sidebar folded into icons, each household or team is one icon with its unread count.
- daemon:
GET /api/v1/user/household-agentsalso says whether each organization is a household or a team (kind), and lists the divisions the listed agents work in (divisions: id, name, posture). Both are optional and additive.
One row per organization agent in the sidebar (#1119)
- app: an organization’s agent is one row instead of two. Tapping it opens the chat everyone shares with it; the arrow on its right unfolds your private chats with it, with New private chat on top. Its number counts what is unread in both. A team with a front desk and four division agents now takes five rows, not ten.
The sidebar’s agents show without scrolling (#1118)
- app: Recent shows your five latest chats, and leaves out chats with an agent listed under Agents, since they unfold under that agent. In a laptop-height window the Agents are now in view; before, twelve recent chats pushed them below the fold. Show all chats still lists every chat.
Smoother scrolling in chats with pictures (#1097)
- flutter: pictures in a chat no longer make scrolling stutter. Thumbnails keep their size while they load, are decoded at the size shown, and aren’t downloaded again when scrolled back into view.
The agent’s tools say what they really do (#1094)
- daemon: The GitHub tools are only offered once you have set a GitHub token, point to Settings → AI when there is none, and code search says it covers all of public GitHub unless the search names a repo, org or user.
- daemon: Listing your people, projects or documents says when the list stopped at its limit, so the agent no longer treats a cut list as complete.
- daemon: A calendar that lost access is reported as needing to be reconnected, not as unconnected.
- daemon: Removing a relationship forgets it the way forgetting a fact does: it is in the Memory Log and can be brought back. Adding one shows in the Memory Log too, and can be undone there.
- daemon: A to-do or a mission suggestion that the agent sends twice by retrying is added once.
- daemon: Fetching a PDF, an image or another file that isn’t text no longer hands the agent unreadable bytes; it is told what the link holds instead.
- daemon: Tool descriptions are brought up to date: the calendar’s default time range, every calendar and mailbox kind you can connect (Gmail and iCloud included), and what linking a person to a chat address does.
Small app fixes: day labels, buttons, counts and sign-in (#1093)
- flutter: “Today”, “Tomorrow” and “Yesterday” stay right on the days the clocks change; on the Sunday they go forward, Monday’s to-dos no longer read “Today”.
- flutter: The phone’s send, stop and voice buttons answer a tap anywhere in their 44-point area, not only on the small circle.
- flutter: “1 suggestion” instead of “1 suggestions” on a document’s suggestions, and in Swedish “1 vald” when one is selected.
- flutter: Swedish reminders say “Varannan vecka” instead of “Var 2:e vecka”.
- flutter: A chat named after its first message no longer cuts an emoji in half in the chat list.
- flutter: On the web sign-in page, Enter in the email field sends the sign-in link (or moves on to the password where there is one).
- flutter: A very long reply that is picked up mid-way now opens with “…” instead of seeming to start mid-sentence; the full reply appears when it is done.
The site’s language switch, buttons and search listings are fixed (#1092)
- site: “English” on the Swedish home page goes to the English home page instead of reloading the Swedish one.
- site: buttons are readable in dark mode, and the Download button in the header has button-coloured text.
- site: the not-found page is kept out of search results, and its “Svenska” link goes to the Swedish home page. Each page’s language links, default language and Open Graph locale now name the right page and locale.
- site: the header logo and the sharing image are resized and compressed, so pages load several hundred kilobytes less.
- site: the releases page has one main heading, and the product, contact, download and your-data pages no longer skip a heading level.
- deploy: the dev and test app and admin hosts tell search engines not to index them, and the app answers
/robots.txtwith a real file instead of the app page.
Security headers on the web pages, steadier deploys (#1091)
- deploy: The web app, the admin console and celindor.ai now send security headers, so no other page can frame them, and the admin console and the site get a Content Security Policy.
- deploy: Dev and test deploys log in to the registry separately, so one can no longer fail with “denied” because of the other.
- deploy: Every container’s log rotates and every service has a CPU cap, and a deploy warns when the server disk is nearly full.
- deploy: A release builds the web app with the same Flutter version and dependencies that dev ran, and stops if they differ.
Restored households keep their pending memory, and missions stop with their agent (#1090)
- daemon: Restoring a deleted organization brings back what its members had waiting to be added to its memory, instead of losing it for good.
- daemon: When an owner’s account is deleted, an organization they had already deleted goes with the account instead of passing to someone else who could then restore it.
- daemon: Deleting a personal agent cancels its unfinished missions, and retiring one pauses its running missions, so they no longer run on as plain Celindor.
- admin: The sub-account visibility settings, which changed nothing, are gone; the page now says what a parent account can and cannot see.
The agent no longer says it deleted or cancelled something that wasn’t there (#1087)
- daemon: Forgetting a person, removing a relationship or cancelling a reminder now says when nothing matched, the reminder list shows every pending reminder however old, and the agent can find recording and entity ids for the cards it shows.
Memory is only deleted or merged with your OK (#1086)
- daemon: Celindor now asks you every time before it deletes something from your memory for good (forgetting a person or thing, merging two entries into one, removing a link between them), saying what will go, and a yes is never remembered. Merging two entries in a household’s shared memory asks too, and is done in your own chat with the household agent rather than in the shared thread. Missions can no longer delete or merge memory. Calendar event titles, places and attendees, and shared organization memory, now reach Celindor marked as outside text, so an invitation or another member’s note can’t give it instructions.
Unsent messages can be retried, chats load visibly, and old apps are told to update (#1085)
- flutter: A message marked “Not sent” now has Retry and Discard. Retry sends what you wrote again, files included, so nothing has to be typed twice; a reload keeps the message where it was written instead of moving it below newer ones.
- flutter: A chat that isn’t on this device yet opens on a spinner while its messages load, and says so with a Try Again button if they can’t be fetched, instead of looking like a new, empty chat.
- flutter: When the server no longer supports this version of the app, Celindor shows an “Update required” screen with a link to the update (or Reload on the web) instead of retrying in the background for ever. Messages waiting to send are kept until the app is updated.
The chat keeps a reply on resume and scrolls only when it should (#1084)
- app: switching away mid-reply and coming back no longer wipes the part of the answer already written, or its “Using …” lines; the reply carries on from where it was. While you are at the bottom of the chat, the view now follows a reply as it grows; if you have scrolled up to read, it stays put. A message waiting to send (offline, or behind a running reply) no longer pulls the chat to the bottom every second, and no longer reports that the reply took longer than 5 minutes before it was even sent.
Access follows changes to a household (#1080)
- daemon: deleting a household ends every open chat with its agents at once, and so does an operator removing someone from it; the operator removing a member now tells them, as an owner or admin removing them does. Joining through an invite, and an operator adding someone, start their household chats afresh too.
- daemon: an invite now works only while whoever sent it may still send it. Removing or demoting an admin revokes the invites they sent, handing a household over revokes the former owner’s admin invites, and deleting a household revokes all of its invites; restoring it does not bring them back. Accepting an invite checks the same again, and a deleted household takes no new members or invites (migration 0161, which also revokes invites already pending that no longer hold).
A message resent after a lost connection is answered only once (#1079)
- daemon: When the app sends a chat message again because the first response was lost, the server now returns the turn it already started instead of starting a second one, so the agent no longer answers twice or repeats actions like sending an email.
Long chats no longer lose messages or grow without bound (#1078)
- daemon: when a chat is condensed twice at the same moment (once after a reply, once because a request ran out of room), the earlier, shorter condensing no longer overwrites the newer one. Before, the messages between the two could end up in neither the chat nor its summary.
- daemon: a chat that stays open keeps its history within the same budget as one that is reopened. Incognito chats, which are never condensed, no longer resend the whole conversation with every message.
- daemon: photos and files older than the last three messages that carried any are no longer kept in the server’s memory (the model already saw only their names).
Two replies never run at once in one chat (#1076)
- daemon: a message sent while a reply is still running waits for it, as before. If that reply holds the chat for more than ten minutes, it is now stopped, and if it still won’t let go the new message fails with “still busy with an earlier reply” instead of running beside it and mixing the two conversations.
- daemon: freeing room for a new chat no longer closes a chat that is in the middle of a reply.
Stop works on a message that is still waiting (#1075)
- daemon: a message sent while the chat was still busy (another device’s turn, a mission step) could not be stopped, steered or followed until it started, up to 10 minutes later: Stop said there was no such turn, and the reply came anyway. The same was true for the first seconds of every turn. Its turn now exists from the moment it is sent, so Stop, steering and resume work while it waits, and a turn that never gets to run is no longer left marked as running.
No placeholder content on celindor.ai (#1074)
- site: invented testimonials, team members, prices, FAQ answers and office hours are no longer published. They still show in
pnpm dev, and sections with nothing real in them are hidden.
The privacy policy names consent as the basis for usage statistics (#1073)
- site: now that the app asks before it sends any usage statistics, the privacy policy (English and Swedish) gives consent (GDPR Art. 6(1)(a)) as their legal basis instead of legitimate interest, and says the consent can be withdrawn at any time under Your data.
Usage statistics are asked for before anything is sent (#1073)
- app: the app asks once, after you sign in, whether it may send anonymous usage counts, and sends nothing and stores no device id until you allow it. Don’t allow means it never asks again. If you already had the app, you are asked too, and nothing is sent until you answer. You can change your answer under Your data.
- site: the privacy policy says usage statistics are asked for when you first open the app, not on by default.
Incognito chats no longer keep a record of memory searches (#1070)
- daemon: when the agent searches memory in an incognito chat, the search text and its results are no longer stored for ranking or for the Memory Log’s explanations.
A message sent during a reply stays below that reply (#1069)
- daemon: Chat messages are now ordered by the time the server stores them, so a message sent while a reply is still being written, or from a device whose clock is wrong, no longer sorts above earlier messages; when it was written is kept for memory and the agent’s clock.
Older messages in long chats can be scrolled back to (#1068)
- app: scrolling up in a chat loads earlier messages, all the way to the start, instead of stopping at the newest 50; reopening the app no longer drops older messages this device already had.
- daemon: the chat message list takes a
beforecursor (a message’s id and time) and returns the messages just older than it.
Costs count the model attempts a refusal fallback replaced (#1067)
- daemon: When Claude declines a request and another model answers it, the declined attempt’s tokens are now counted in turn usage, background call costs, budgets and token quotas, priced at the model that ran it.
The organization’s key has a monthly budget, kept also while a turn runs (#1066)
- daemon: every household and team has a monthly budget on its own AI key, USD 50 to start, which counts everything the key pays for: every agent’s chats and thread replies, missions, delegated tasks, answers between divisions and background work. A household agent that belongs to no division was not limited before, and any member could spend the key without end. The owner and admins are warned at 50, 80 and 100 percent and can raise or clear it (
GET/PUT /api/v1/user/organizations/{orgId}/budget); once it is used up, its agents say so instead of answering. A division’s budget still applies within it. - daemon: budgets are checked before each model request, not only before a turn or a mission step: a turn that runs out stops at the next request and says why, and turns that start together can no longer all pass on the same reading. A mission step stops once it has spent what was left of the mission’s budget.
- daemon: an organization may have at most 10 household missions live (suggested, running, waiting or paused) at once.
- daemon: an agent’s introduction, a reminder’s work and a chat channel’s turn on the organization’s key now count against its budgets too.
Saved-for-later suggestions follow your access, and people you add can go independent (#1064, #1065)
- daemon: a suggestion waiting to be saved into a household or team is dropped when you leave or are removed, are made a viewer, lose access to its space or division, or the household, team or a non-Open division is deleted, including the ones saved for later. Saving one also checks again that you may still add to that memory.
- daemon: deleting an account no longer deletes the people it added once they sign in themselves: they become accounts of their own and keep their chats, memory and households. Accounts nobody ever signed in to still go with it.
- daemon: someone added under your account (for example with Add without email) and you can agree to make theirs an account of its own. Either of you asks first, the other agrees.
- admin: Settings › Sub-accounts shows the request: Ask to become independent on the sub-account’s page, Let go, Agree or Decline on the parent’s.
Handing over a household or team keeps the new owner in it (#1063)
- daemon: transferring ownership no longer removes the new owner’s membership. They keep their division access, so a Sealed division they look after keeps its steward and the previous owner still needs break-glass to read it. They also keep getting live chat updates, notifications and @mentions. The transfer is all-or-nothing, the other members get a notice, and the owner’s membership can’t be removed, by an admin either, until they hand the organization over. Migration 0090 restores owner memberships lost to earlier transfers.
- flutter: members see “X handed Home over to Y” in the household notices.
The agent remembers the reminders and messages it sent on its own (#1062)
- daemon: A reminder or other message the agent sends unprompted is now part of what it sees on your next message, and one that fails to send no longer leaves the chat stuck as if a reply were still coming.
Stop closes an open permission prompt (#1061)
- daemon: stopping a reply while it waited for you to allow a tool used to leave the prompt open, keep the chat busy for up to a minute, and still run the tool (sending an email, say) if you tapped Allow afterwards. Stop now closes the prompt as denied on every device, the chat is free at once, and nothing runs after Stop.
Changing a key or chat setting no longer silences open chats (#1060)
- daemon: Changing an API key, model or other chat setting keeps every open device getting replies, prompts and questions, lets a reply that is running finish, and applies the new setting from the next message.
Memory search upkeep stays light as data grows (#1058)
- daemon: the background job that fills in missing search vectors now reads only the rows that need one, and one account without an embedding provider no longer holds up everyone else’s; recall logs older than 90 days are cleared nightly (your ratings, corrections and Today-tab dismissals are kept); and the vector search indexes are sized to the data and searched more thoroughly.
Incognito chats leave no trace for operators or usage counts (#1057)
- daemon: an incognito chat’s live session is no longer listed under Admin › Sessions, counted on the Overview, shown in a user’s activity or sent on the admin live stream, as the policy already promised.
- app: opening the chat page while in an incognito chat no longer sends a page view to usage statistics. Your data now says that Celindor does record which provider and model answered each message, which it did all along.
Everyone following a household mission sees it work (#1056)
- daemon: every member who follows a household mission now sees its steps and its plan update live in the mission’s chat, not only the member it runs for.
- daemon: the admin console’s activity feed names a session’s chat by the same id as its messages, and the unused live admin stream and two events nothing read are gone.
- daemon: the API description now lists every error code the server can answer with.
“My wife” names the right person, ChatGPT imports skip bad chats, facts are dated when you said them (#1055)
- daemon: “my wife”, “my child” and other relationship words name a person only while that relationship lasts. Once it is forgotten or ended they no longer point at that person, and when they fit several people (“my child” with two children) memory no longer picks one of them at random.
- daemon: a ChatGPT export with one malformed conversation (an odd title, an impossible date) now imports everything else instead of failing.
- daemon: a fact like “works at Klarna since 2015” is dated from when you said it, not from 2015, so it is not treated as old news.
Sign-in hardening: cross-site requests, first passwords, stored sessions and claim codes (#1054)
- daemon: a request that changes something and carries the browser’s session cookie is refused unless it comes from the app, the admin console or the server itself, so a page on another Celindor host can no longer act as the signed-in user.
- daemon: an account made by a setup link without a password can now set one:
POST /auth/passwordasks for the current password only when there is one, and the session sayshas_password. - daemon: the server’s session file keeps only hashes of session ids, and is rewritten that way on the first start; everyone stays signed in. Reading an expired session no longer rewrites the file.
- daemon:
config.tomlis written readable by its owner only (0600, in a 0700 folder). - daemon: claim codes are kept as hashes, typed codes are read the Crockford way (I and L as 1, O as 0, dashes and case ignored), wrong guesses are limited, and a refused claim no longer uses up the code. The admin console’s code list shows the first two characters only.
Links open the right app, and the app is called Celindor (#1053)
- flutter: On Android, each build now opens only its own server’s sign-in, invite, setup and connect links, so a test build no longer takes a dev link and calls it invalid. A link for another server that still reaches the app says which server it is for and offers to open it in the browser.
- flutter: Android release builds only talk to servers over HTTPS; plain http:// to a local daemon stays available in debug builds.
- flutter: The Android app and the web app are named “Celindor” (it was “celindor”), and the installed web app has a real description.
What we say about your data matches what happens (#1051)
- flutter: Your data → Who else sees your data lists Google Gemini for your recordings and voice messages, even when another service answers in chat: on the service’s own Google Cloud project with its region, or on your Gemini key or the operator’s.
- daemon: The log no longer carries what you wrote or stored: no file or attachment names, entity or episode names, email addresses, snippets of model output about your documents, text quoted by failed JSON parsing, Postgres error details, or the background agent’s free-text reasons. Log lines keep ids, sizes, timings and error codes.
/api/v1/jurisdictionsays where recordings are transcribed. - site: The privacy policy names Google as the transcriber of recordings and voice messages, and says exactly what the logs hold. The site no longer says every chat runs its tools in a sandbox: only accounts given command-line access can run commands, each chat in its own sandbox.
- manuals: The user manual says why Gemini is listed for recordings; the administrator manual says what log lines contain.
Decision model training data only with consent (#1050)
- daemon: With distillation logging on, the text a decision was made on is now kept, and
celindor decision exportreads it, only for users recorded as agreeing (celindor decision consent <user>). Text already stored for everyone else is removed. - daemon: Deleting a recording now also deletes the transcript text kept for it in the decision log.
Privacy policy says when your data trains a model (#1050)
- site: The privacy policy now says that Celindor’s own small decision models are trained on what you wrote only if you agreed, that withdrawing deletes the text kept for it, and that it also goes with your account and with the recording it came from.
Chat turns skip the quota reset; mail and documents are handled for several people at once (#1049)
- daemon: the quota check before a chat turn or extraction only reads. A month or day that has ended counts as unused, and the next use starts the count again, so a user who used nothing no longer sets off an update across every account on each turn. The nightly maintenance now moves every ended reset date along, used or not (migration 0192).
- daemon: mail sync handles up to five mailboxes at once, and checks a fetched batch against what was already synced in one query. One person’s mailboxes still sync one after another.
- daemon: document processing runs its extract, curate and index stages side by side, each on up to four people’s documents at once and taken in turn across people, so one large mail import no longer holds up everyone else’s uploads.
Memory recall stays quick with a large memory (#1048)
- daemon: the personal memory searches every message runs no longer do per-row work for every fact a user has: results are read back by key, the namespace rule is checked inline, keyword matches come from the indexes and each embedding distance is computed once. For a user with 40,000 facts a search takes roughly half the time; what recall finds is unchanged.
- daemon: when a follow-up message needs the names in focus, both memory searches run side by side instead of one after the other.
Faster household thread lists and unread counts (#1047)
- daemon: The chat list, the unread badge and “mark all read” now look only at the households you are in instead of every household thread on the server, and “mark all read” clears your threads in one step.
Large mail no longer runs the server out of memory (#1046)
- daemon: Learning from an IMAP mailbox now reads new mail one message at a time and saves its place after each one, so a batch of large mail is never held at once and a sync that stops halfway carries on from the last mail it stored. Of a mail larger than 10 MB only the start is read; its attachments are listed by name.
- daemon: The agent address reads its mail one message at a time and ignores, without downloading, any mail larger than 2 MB.
Scanned PDFs can no longer stall the document queue, and a bad date no longer wipes lab values (#1045, #882)
- daemon: PDFs, Word .doc and Excel files are all read in a separate worker with memory and time limits. A scanned PDF is read from its first 40 pages, with at most 5 minutes of OCR. A spreadsheet that claims a huge range is read from the cells it really has.
- daemon: a lab value with an impossible date keeps the value and drops the date, and re-reading a document’s values replaces them in one step, so a refused batch keeps the earlier values.
- daemon: the OCR language data ships with the server, and an OCR failure no longer stops the server; OCR tries again later.
A fact and its reverse are no longer treated as the same suggestion (#1044)
- daemon: New memory suggestions are compared with what you already have word for word in order, so “Erik owes Anna 500 kr” no longer hides “Anna owes Erik 500 kr”, and the check is faster for people with many memories.
Memory suggestions no longer expire without a review (#1043)
- daemon: suggestions from a document or recording in the Trash no longer use up the dream’s batch, so newer suggestions behind them are reviewed instead of expiring.
- daemon: when the dream has no AI key for a partition (an organization without its own key, or your own key removed), it can only save high-confidence suggestions. Such a run no longer counts toward letting the rest expire; they wait in the Inbox until a review with a key has seen them.
Duplicate suggestions keep their facts, and a person taken back can come back (#1042, #1041)
- daemon: When dreaming finds two suggestions of the same person, such as “Bo” and “Bo Ek”, it now merges one into the other and moves what was said about it along, so “Bo is allergic to nuts” is saved about Bo Ek instead of about no one, and relationships naming the duplicate are no longer flagged. Dreaming also decides the people before the facts about them.
- daemon: A person or thing taken back from memory (or archived) is suggested again when mentioned, instead of new facts quietly attaching to the hidden entry. Saving that suggestion, or remembering it, brings the entry back.
- daemon: The local decision head for dream review only decides on its own when a
fallback_confidencefloor is set, and no longer saves a fact before the person it is about has been reviewed.
Memory search finds shared facts and skips corrected ones (#1040)
- daemon: A household agent, and shared results in your own chat, no longer miss a shared fact because you have many personal facts on the same subject (a shared “wifi router password” behind a dozen personal wifi notes).
- daemon: Facts you have corrected or said are not true no longer take places in a memory search, so it returns as many current facts as it asked for.
Live-update events are covered by the contract check (#1039)
- ci: the API contract check now compares the events on the live update
stream (
GET /api/v1/user/stream) with the last release, like any response. Removing an event, or removing, renaming or retyping one of its fields, is a breaking change that must raise the minimum client version; a new event or a new optional field is not. - ci: a new value in an enum marked open (
x-extensible-enum) no longer fails the check, as was always intended. - daemon: a test checks that the events the server sends match the published event shapes.
- api: no change to the minimum client version for #664 (a new team
division is readable by the whole organization unless
posturesays otherwise). Divisions first ship in 0.7.0, and the minimum is already 0.7.0 (#875), so no released app creates a division without knowing about posture.
Reading, renaming, deleting and archiving reach your other devices (#1038)
- daemon: renaming, deleting, archiving or bringing back a chat, reading a household chat, and changing its mute or “Answer every message” now send a
state.chatChangedevent on the user stream to your own devices (and “Answer every message” to every member). “Mark all as read” now tells household chats’ members how far you have read, as reading in the chat does. - flutter: a chat you read on one device loses its unread count on the others, a renamed chat shows its new name, and a deleted or archived chat leaves the list instead of staying until a restart (where sending in it failed). Household chats read or changed elsewhere update too.
Permission prompts no longer get lost when two arrive at once (#1037)
- flutter: when the assistant asked for permission in two places at once (another chat, a reminder, a mission), answering one prompt could silently drop the other, so tapping Allow there did nothing and the request was denied a minute later. Prompts now wait in line and appear one at a time, each answers its own request, and each says which chat is asking. A prompt that is answered on another device or closed by Stop disappears by itself, and a late answer to it is no longer shown as an error.
- flutter: saving or removing a standing instruction, merging two memory entries and removing a link from memory now have their own prompt headings.
The team’s operator key needs a proven address (#1035)
- daemon: on test, the operator’s Anthropic and Gemini keys go only to accounts whose
@celindor.aiaddress is proven (signed in by a mailed link, made by an operator, or confirmed). A team address typed for a sub-account or on a setup link no longer runs on the operator’s key until its owner confirms it.
Deleted accounts and password resets sign out every device (#1034)
- daemon: deleting an account — by an operator, or a sub-account by its parent — now signs out every device it was signed in on, and those of its sub-accounts. Before, such a session kept working for up to seven days and could even enroll a new account.
- daemon: when an operator sets a new password for an account, its existing sessions end, as they already did when you change your own password.
The Mac app saves the division audit CSV (#1033)
- flutter: on a Mac, exporting a division’s audit log as CSV now saves the file where you choose. Before, the app was only allowed to read files you picked, so the save failed. If a file can’t be saved, the app now says so instead of blaming the connection.
Choose whether notifications show the message (#1031)
- daemon: A new “Show message previews” setting. Turned off, notifications about messages from your agent, reminders, missions and household chats say only “New message from Celindor” (or “New message”), in English or Swedish, and the text never goes through Apple’s, Google’s or the browser’s push service; pushes held during quiet hours are hidden too. On Android, notifications are now private: the lock screen shows the text only once the phone is unlocked.
- flutter: Settings → Notifications → Privacy → Show message previews, on by default.
Recurring events that started years ago show up again (#1029)
- daemon: a daily or weekly event that started a few years ago, such as a standup, is no longer missing from iCloud, Fastmail, Nextcloud and calendar-link calendars, for the agent and for busy checks. A calendar with an unreasonable number of repeating events can no longer stall the server.
Token and extraction limits count every use (#1027)
- daemon: The monthly token and daily extraction counters now add each chat turn, dream and recording in one atomic update, so usage that happens at the same time is no longer lost and the limits hold.
People with non-Latin names stay apart (#1026)
- daemon: Names written in Cyrillic, Chinese, Arabic, Thai and other non-Latin scripts now keep their own letters in memory instead of all being filed as one entity, so different people no longer merge.
Searching shared memory is fast and keeps private spaces private (#1025)
- daemon: Recall that includes household or team memory no longer shows facts kept in a hard, private or what-if space, and with a large shared memory it answers in under a second instead of 15 to 23 seconds.
- daemon: A household agent’s turn no longer waits on a slow shared-memory search; it answers without that block after a few seconds.
Faster unread counts and badge (#1024)
- daemon: The chat list’s unread bubbles and the app icon badge work out each chat’s read position once per chat instead of once per message, so they no longer take several seconds for long chats.
A malformed email or web page can no longer freeze the server (#1023)
- daemon: HTML emails, HTML attachments and pages the agent fetches are turned into text in one pass. Unclosed tags used to make this take seconds to minutes, during which the whole server stopped responding.
A booby-trapped Word or Excel file can no longer crash the daemon (#1022)
- daemon: Word and Excel files (library uploads, Drive, chat attachments, email attachments) are checked before they are opened: a file that would expand past 100 MB, has a part over 32 MB, holds more than 5,000 parts, or expands far more than a real document does (a zip bomb) is refused with a message saying why. A 300 KB .docx that expanded to 105 MB used to run the daemon out of memory and stop it.
- daemon: the files that pass are read in a separate worker with its own memory and time limit, so one that still needs too much fails on its own instead of taking the daemon down.
An import archive can no longer stop the server (#1020, #1021)
- daemon (security): an archive uploaded to Import from another assistant is now read in a separate worker with its own memory and time limit (5 minutes). A crafted archive used to be able to stop the whole server: a tiny Hermes backup hung it forever, and a small zip bomb crashed it. Now only that one import fails, with a message saying why.
- daemon: a Hermes
state.dbwhose sessions or messages are not plain tables is refused. A file read from an archive may be at most 128 MB (was 512 MB), 512 MB in total (was 1 GB), and a file that expands far more than real exports do is refused. - daemon: an import that was being read when the server restarted now fails (“upload it again”), instead of being read again at every start. Imports cut short while importing still resume.
Sign-in links are shown on screen only on a developer’s own machine (#1019)
- daemon: a daemon without email shows the sign-in link in the response only to a connection from the same machine, and only in a local or dev environment. A request claiming
Host: localhostfrom elsewhere no longer gets a link.
See what operators did to your account; a privacy policy that matches the code (#1017)
- app: Your data has a new section, What operators did. It lists every time someone running Celindor read your profile and your agent’s instructions, set a new password, or changed anything else on your account, with the date. It never names the operator.
- daemon: an operator opening a user’s Workspace in the admin console is now
recorded in the admin audit, like every change already was. The new
GET /api/v1/user/operator-accessgives users the entries about themselves. - site: the privacy policy and terms (English and Swedish) now say what the service actually does. Support access that you grant yourself does not exist yet, so the policy no longer promises it. It now also lists everyone who receives data and what goes outside the EU (push notifications, web search, email delivery, the web app’s fonts, problem reports), and gives the legal basis for each purpose. It also says that deleted files stay in storage versioning for up to 30 days, and names the two things account deletion does not yet remove.
Pinned messages in a shared chat’s info (#1016)
- flutter: a shared chat’s info screen has a Pinned messages row with how many there are. It opens the list of pins; choosing one goes back to the chat at that message. The row is hidden while nothing is pinned.
Voice messages show as voice messages in notifications, replies and pins (#1015)
- daemon: a shared chat’s push for a voice message says “Voice message” (in the reader’s language) instead of the file name, and replies and pins carry a new
voiceflag with the start of the message’s words as the excerpt once they are written out. - flutter: a reply’s quote, the reply bar, the pinned bar and the pins list show “Voice message”, followed by its words when they are ready, instead of the file name or nothing.
A voice message whose words failed is tried again (#1013)
- daemon: a voice message in a shared chat whose transcription fails (an error, a timeout, no Gemini key) is tried again a minute later and then an hour later, and at once when the agent needs its words; after three failures it is marked as failed, and any member can ask once more with
POST /user/threads/{threadId}/files/{fileId}/transcribe. - flutter: a voice message that could not be transcribed says so with a Try again button, and shows “Transcribing…” while it is tried again.
Stricter daemon tests (#1006)
- daemon: the server’s tests are now type-checked in CI, and two tests that passed for any error now check the specific one; nothing changes for users.
Unsent messages leave quotes and pins, and other small app fixes (#1005)
- flutter: an unsent message in an organization chat no longer lingers in the replies that quote it or in the pinned bar until the chat is reloaded.
- flutter: a link with broken percent-encoding (such as
/chat/%E0%A4%A) opens the chat page instead of crashing the app. - flutter: switching several calendars on or off quickly saves them one at a time, so the last choice is the one kept; a failed save only takes back a switch that nothing changed since.
- flutter: “Update the summary” on a recording says so when it fails, instead of doing nothing.
- flutter: an organization’s agents show their bundled face or their letters rather than an image from another server, so an admin cannot learn when members open the app. Your own agents still show the image you gave them.
- flutter: live updates the app cannot read are logged (by type and size only) instead of dropped silently.
Restored items stay restored, and old dates no longer look overdue (#1004)
- daemon: an item you restore while the Trash is being emptied, or while old items are cleared out, is now kept instead of being erased.
- daemon: on the Today tab, a to-do that only mentions an old date, like a birthday, is no longer shown as overdue, and anniversaries fall on your own calendar day instead of the UTC one.
- daemon: a group or member name that reads like an instruction to an assistant is no longer written into your memory when you join or leave a group; the note says the name was withheld.
- daemon: when automatic division publishing is on, a fact is never copied into shared memory twice after an interrupted run.
The agent is told in every chat that outside text is not instructions, and operators can block sites (#1003)
- daemon: every chat’s system prompt now says that fenced text from web pages, emails, documents, calendars, contacts, shared and organization memory and other agents is data, never instructions, on every provider and in household and mission chats too. Before, only the background memory and import prompts said so.
- daemon: new
[web] blocked_domainsinconfig.toml: the agent cannot read a page on those sites or their subdomains, even through a redirect, and its web searches leave their results out. The web tools had a blocklist since #112, but nothing ever set it.
Reminders fire on the right day and hour, and incognito chats only read the calendar (#1002)
- daemon: A weekday reminder whose first time falls on another day now starts on the next chosen weekday, a time given without a zone is read in your own time zone instead of UTC (reminders, their end date and a mission’s next step), and an incognito chat can look at your calendar but no longer book, move, cancel or answer events.
Reply records go with their chat and account (#1001)
- daemon: deleting a chat or an account now also deletes the records of the replies in it, and code that called database functions which never existed is gone, including the unfinished memory-spine switch that broke saving memories when turned on.
Uploaded HTML and SVG files download instead of opening as a page (#999)
- daemon: download links for documents, chat and thread files, recordings and link-preview images say the file’s type themselves. Pictures, PDFs, audio, video and text open as before; HTML, SVG and other active or unknown files are downloaded instead of opening as a web page.
Firmer division walls, capped memory lists and lasting removal history in teams (#998)
- daemon: A division’s admin can now only take down a wall they put up themselves, so a wall raised to keep a division out can no longer be lifted from inside it.
- daemon: An organization’s memory lists return at most 500 entries per page.
- daemon: An organization’s memory history keeps who removed an entry after the entry is erased for good, 30 days later.
Saving retrieval explanation settings no longer resets the dream schedule (#997)
- daemon: Saving the retrieval explanation settings now stores them and leaves the memory-tidying (dream) schedule as it was, and saving either group changes only the fields sent.
Settings cards never pick the mail or calendar server (#995)
- flutter: a settings card in a reply no longer fills in the IMAP server, port or CalDAV server on the form it opens, so a card can’t send your password to someone else’s server. Known services (iCloud, Gmail, Fastmail, one.com) bring their own server; for any other, you type it.
- daemon: the agent’s settings cards no longer accept a
host,portorserver_urlprefill.
The chip showing where a chat’s memories go stays true (#994)
- flutter: the chip reads where the chat’s memory goes each time the chat opens, and again when you change it on another device, so a chat set to the household on the phone no longer says Personal on the Mac. When the app can’t find out (offline, a server error) the chip says “Memory: unknown” instead of guessing Personal, and it no longer disappears for the rest of the session after a failed load: the app tries again on the next chat.
- daemon: changing a chat’s memory scope sends a
chat-write-scope-changedevent on the user’s stream, so their other devices read it again.
The next account on a device, and device backups, get none of your data (#992, #993, #1030)
- flutter: signing out or switching accounts now also resets the open side panel, older chat history, chat settings, notification and permission settings, mission budget, thread activity and an ongoing voice conversation, so nothing of the last person’s stays on screen.
- flutter: saved places and “share my place” now belong to your account. Someone else signing in on your phone starts with sharing off and no places, and yours are deleted from the device when they do. Places saved before this update go to the first account that signs in.
- flutter: the app is no longer copied into Google Drive backups or to a new Android phone, and recordings waiting to upload stay out of iCloud and Mac backups. Your sign-in stays on the device too: after restoring a backup or moving to a new phone, you sign in again and the app fetches your chats from the server. A sign-in that a restore left unreadable now opens the sign-in screen instead of hanging.
Reviewing a suggestion twice says it is already handled (#991)
- daemon: Approving, rejecting, saving or dismissing a memory suggestion that was already decided now answers “already handled” (409), and one that is gone answers “not found” (404), instead of a server error.
Recordings respect the monthly limit, and one person’s recordings no longer hold up everyone else’s (#990)
- daemon: On the operator’s key (Vertex AI included), transcribing and summarising a recording stop once the person’s monthly limit is used up, as chat and ingest already do; the recording says why and can be retried after the limit resets.
- daemon: Recordings are processed in turn across people, so one person with many recordings no longer keeps everyone else’s waiting for hours.
- daemon: Forcing a recording to be transcribed again works once an hour per recording; sooner gets a 429 with
Retry-After. Asking for a new summary now shares the transcription request ceiling.
Quiet hours follow your own clock (#988)
- daemon: Quiet hours now hold pushes during the hours you picked in your own time zone, including across summer time changes, instead of the same hours in UTC; changing one notification setting also no longer switches quiet hours off.
- flutter: The app sends your phone’s time zone with quiet hours, and updates it when you open the notification settings somewhere new.
Handing a task to another agent never uses an incognito chat (#985)
- daemon: when one of your agents hands a task to another, the task no longer runs in an incognito or archived chat you had with that agent, so nothing from an incognito chat can come back into a normal chat or into memory. If you have no other chat with that agent, a new one is started. An incognito chat still cannot hand tasks on, now also refused by the server itself.
Text someone else wrote is never taken as an instruction, and calendar approvals show what invitees get (#983, #986, #989, #987)
- daemon: the names and descriptions a household’s or team’s admins give its agents, and the names of your groups, now reach your own agent marked as someone else’s text, so a persona cannot give your agent orders. A household agent or group name that reads like an attack on other members’ assistants is refused when saved.
- daemon: when a long web page, email or document is read in parts, every part is now marked as outside content, not only the first.
- daemon: in a household chat, shared memories other members recorded go through the same check as your own: one that reads like an instruction is held back. In the shared thread, no member can write a message that looks like it came from someone else.
- daemon: before a calendar booking or change, the approval now shows the event description word for word, and on a change who outside your organization will receive it.
- app: a long approval request is shown in full and scrolls.
Memory is tidied for everyone, even when one account has a large backlog (#982)
- daemon: The dream worker now finds every user with pending memories, so one account’s large import no longer stops everyone else’s memories from being reviewed, and users with only pending relationships are no longer skipped.
Remembered facts get the right dates (#981)
- daemon: Memory extraction from chats, recordings and documents now knows the day the words were written and the user’s time zone, so facts like “I’m 55” or “until March” get dates in the right year, a fact stays visible through the last day it is valid, and impossible date ranges are dropped.
Deleting an account erases every file it had (#979, #907)
- daemon: Deleting an account now also deletes its data exports, import uploads and archived memory files from storage, for the account and every sub-account deleted with it. Before, those files stayed in storage after the account was gone.
- daemon:
celindor remove-userdeletes an account the same way as the admin API: sub-accounts, files and sign-ins go too, and the agents above it are told. It shows which environment it acts on, asks you to type that environment’s name, and says so when no user matched.
Shared memory keeps what is written to it (#977, #978)
- daemon: promoting one of your entities to an organization’s memory copies it with every current fact about it, each now about the organization’s entity. Your own entity and facts stay; before, the entity was deleted with all its facts and only its name reached the organization. Promoting a single fact keeps yours too. Both promote routes are on the API contract now, and the entity’s answer says how many facts were copied.
- daemon: an entry removed from the shared memory stays in the trash, untouched. If the agent learns about the same thing again, it starts a new entry; before, the write landed on the hidden entry and was erased with it 30 days later. Restoring the removed one then folds it, with what was removed with it, into the new entry, and the history says so. Facts, merges, links and publishing never attach to a removed entry, and the 30-day purge only ever erases removed ones.
- daemon: two writes of the same new name at once make one entry instead of failing, and a changed entry is re-indexed for search.
Deleting a division no longer fails on a name the shared memory also has (#976)
- daemon: Deleting a division whose entries could not be merged into a shared entry of the same name now keeps both, renaming the division’s one after the division (for example “Kim (Kitchen)”), and a Members-only or Sealed division with only flagged staged memory or work items is no longer deleted into everyone’s view.
Memory spaces keep loading after the default space is deleted (#975)
- daemon: making another memory space the default and then deleting it no longer breaks the list of memory spaces for good; the original default space becomes the default again.
Deleting a fact that corrected another works again (#974)
- daemon: Deleting a fact or entity that replaced an older one no longer fails with a server error; the older one stays replaced and is not recalled again.
Household connections stay when the member who added them leaves (#973)
- daemon: Connections saved into a household’s or organization’s memory now belong to it, so they survive the deletion of the member who added them and no longer show up in that member’s own data or export.
Mail servers must be public, invite mails are plain, and limits hold under load (#971, #878, #1052)
- daemon: an IMAP mailbox can no longer point at loopback, private, link-local or cloud metadata addresses, or at a port other than 143 and 993. Saving one is refused with one answer that says nothing about what is there, and every sync checks again as it connects and dials the checked address, so a name that changes its answer later gets nowhere. Operators widen this in
[imap]:extra_ports, andprivate_hostsfor a mail server on their own network. Mailboxes already saved with such an address stop syncing and show the error. - daemon: household invite mails show the organization’s and the inviter’s names as text, never as links or markup, and invitations are limited to 50 per person per day; revoking one doesn’t give it back.
- daemon: the limits on household members and pending invites (20), organizations per owner (10), open sign-in links (5), pins per thread (50) and reactions per member (12) now hold when many requests arrive at once, and an account has at most one working setup link.
- daemon: “Allow from now on” on a waiting mission no longer leaves the permission behind when a cancel or another answer got there first.
Uploads have size limits, and recordings are never held in memory (#970)
- daemon: library documents take files up to 100 MB and recordings up to 2 GB. A larger upload is refused at once (413), the upload link only accepts the size that was declared, and the server checks a stored file’s size before reading any of it. Recordings are streamed to disk for transcription. The limits for every kind of upload are in one place.
- flutter: files over 100 MB are left out of the Library with a notice, and recordings send their size when they ask to upload.
Admin console security update (#968)
- admin: Next.js is updated to 15.5.28, which fixes critical advisories in its image optimizer, and the image optimizer is turned off.
Deleting chats, documents and accounts no longer stalls as data grows (#967)
- daemon: every reference between tables now has an index, so deleting a chat, a document, a recording or an account no longer scans whole tables (a 500-message chat took 20 s with 200,000 messages stored; now well under a second).
Deleting a private memory space no longer shares its memories (#966)
- daemon: a memory space that keeps its memories apart (Private, Hard or What-if) can’t be deleted while it still holds memories, because they would then show in every other chat. Make it Soft first, or forget those memories.
- flutter: the app says why such a space wasn’t deleted.
Deleting an account works with a Drive document in the trash (#965)
- daemon: Deleting an account no longer fails when a Google Drive document sits in its trash, and its files, and its sub-accounts’ files, are only queued for erasure once the account is actually gone.
Images in messages are no longer loaded by themselves (#963)
- flutter: an image written into an agent’s reply, your own message, an organization chat message or a mission chat is no longer fetched when the message shows. A web image is a small note naming the site it is on; tap it to open it in your browser. Images from embedded data or files on your device are never shown. Before, a reply steered by a page or mail the agent read, or a member of a chat, could put an image in a message that sent information to another site, or told it who read the message and when. Photos and files sent as attachments show as before.
Missions ask before fetching a web page or forgetting, and every tool is checked on the Claude subscription path (#961, #962)
- daemon: a mission now pauses to ask before it fetches a web page, and before it forgets, corrects or merges something in memory. A fetched address can carry what the mission read (your mail, your memory) to someone else, and nobody is watching a mission, so an instruction hidden in an email could otherwise send it out. Searching the web still runs without asking. Allow “from now on” to let your missions fetch pages without asking again.
- daemon: on the Claude subscription (Agent SDK) path, every tool call now goes through the same permission check as the other providers. Before, the tools on its allow list ran without it, so the prompts for medium-risk tools, a mission’s pause-and-ask and refusals, the reminder turn’s read-only list and the household forget and restore prompts were all skipped there.
Standing instructions are only saved with your OK (#960)
- daemon: Celindor now asks you every time before it adds or removes a standing instruction, showing the exact text, and a yes is never remembered. Missions and reminders can no longer change your instructions, and text that reads like an injected command (from an email or web page, say) is refused rather than saved. You can still add any instruction yourself in Settings.
Undo in the Memory Log no longer deletes people you already had (#959)
- daemon: undoing a “remember” about someone already in your memory now puts back what was there before, instead of deleting that person and everything known about them. Only someone the agent had just added is removed. Older Memory Log entries that can’t tell the two apart are no longer undone by deleting.
Memory works with an OpenAI key for embeddings (#958)
- daemon: with an OpenAI key and no self-hosted embedding service, saving and finding memories no longer fails: OpenAI is asked for embeddings of the size the database stores. A self-hosted embedding service configured for another size now stops the daemon at start with a message saying what to change, instead of every memory being saved without its embedding.
A Sealed space or division always keeps an admin (#957, #1028)
- daemon: the last admin of a Sealed space or division can’t be removed from the organization, leave it, or be removed from the space, also when two admins are removed at the same moment; make someone else its admin first. Before, the owner or an admin could remove its admins from the organization, appoint themselves, and read it without breaking the glass. A Sealed space left with no admin (its last one deleted their account) now gets one only during a break-glass, which tells its members, the owner and the admins. Migration 0098.
An email address typed for someone else’s account must be confirmed before it counts (#956)
- daemon: the address given when creating a sub-account, or chosen on a setup link, is now unconfirmed until a link mailed to it is opened. Until then the account still signs in with its password, but it gets no emailed sign-in links, matches no household invitations sent to that address, and does not stop the real owner of the address from getting an account. Whoever confirms the address first owns it. Opening the link confirms the address and signs nobody in. A signed-in person can ask for a new link (
POST /api/v1/auth/verify-email/resend). Accounts that existed before this change keep their addresses as confirmed.
Small app crashes and sign-out leftovers fixed (#948)
- flutter: correcting a sentence in a recording transcript that has no speakers no longer fails; the dialog opens and a text-only fix leaves the sentence unattributed.
- flutter: changing how a relationship is filed refreshes the list even when the sheet was closed before the save finished; undoing a document delete after leaving the library refreshes the Trash; answering the location permission prompt after leaving the Location screen no longer throws, and a yes still turns sharing on.
- flutter: signing out while the app is still starting up no longer opens a live connection or presence heartbeat for the signed-out account, and no chat messages from it are left on the device.
Error messages in your language (#947)
- flutter: errors in the chat banner (session expired, chats that would not load, a chat that could not start, a reply that stalled, a message that could not be sent, no connection) are now in Swedish when the app is, and the raw exception or server text no longer shows. The same goes for the error banners on To-dos, Trash, Me, Documents and Review, and for the sign-in, invite code, onboarding, change password and report-an-issue screens. A wrong password or invite code now says so plainly. The detail still goes to the log.
- flutter: the “Business” tab under relationships is now “Arbete” in Swedish.
Admin console: stale clicks, hidden errors, confirmations and paging (#943, #944, #945, #946, #1089, #1095)
- admin: the memory explorer and graph always show, and act on, the entity you clicked last; a slow answer for an earlier click is dropped. The same holds for organization members, episodes and the memory log tabs.
- admin: a failed request shows an error with Try again instead of empty data (“No skills available”, “Not set”, an empty memory); an expired session goes to sign-in. The dreaming page waits for your real settings before showing the form.
- admin: the memory explorer pages through all entities, observations and relationships and searches on the server, so entities past the newest 500 can be found, opened and merged. The graph loads more on Load more, and opens entities it hasn’t loaded yet.
- admin: dreaming settings keep every change, and the number fields save when you leave them instead of on every keystroke.
- admin: Forget entity and deleting an observation ask first; the entity counts per kind cover every entity, not the first 500.
- admin: buttons that run an action (Create User, Grant, Revoke, Drop, Save) are disabled while it runs, so a double click can’t do it twice.
- admin: the graph view of organizations offers Edit and Delete only on organizations you own.
- admin: passwords need 8 characters, as the server requires, and are no longer trimmed; clearing a memory space’s description, colour or icon now clears it;
?limit=abcno longer shows “Last NaN turns”; a blankINTERNAL_API_BASEfalls back instead of breaking sign-in. - daemon: the admin entity list also returns the total and the count per kind. Sub-account passwords are no longer trimmed, and a non-numeric
limiton the admin logs falls back to the default.
The admin’s organization memory pages load again (#942)
- admin: the organization memory Browse and Scopes pages no longer always say “Not authenticated”. Scopes and their members can be created, changed and removed from the console again.
celindor.ai opens the right web app (#941)
- site: “Open the web app” on celindor.ai no longer leads to the dev server. It opens the test web app until there is a production app, and the www.dev and www.test previews open their own environment’s app.
Deploys wait for their checks and for dev (#940)
- deploy: A deploy now stops when its checks fail or are interrupted instead of going ahead, and a release is promoted to test and celindor.ai only after dev has run that exact commit, migrations included.
The command line asks before changing test or prod (#938)
- daemon:
celindor migrate,env,restore,scan-memory-instructions --archiveandrotate-master-keyprint the environment and its database, and on test or prod ask for the environment’s name to be typed, unless--yesis given; an unknown environment name gets a clear error instead of a crash. - deploy: deploys pass
--yestomigrate.
Devices catch up when the live stream missed a change (#937)
- daemon: When the live stream drops changes (a slow connection, or reconnecting after the daemon restarted or fell too far behind), it now tells the app with a
state.resyncevent and logs it, instead of staying silent. - flutter: On that event the app reads drafts, open chats, read positions and the review queue again, so another device’s changes are not lost.
- daemon: Stopping a turn now also stops a conversation summary it is waiting on, a summariser that never answers gives up after five minutes, Gemini summaries no longer come back empty when the model thinks for long, and a backup AI provider that cannot start no longer hides the first provider’s error.
The agent can write large files in its sandbox (#936)
- daemon: Writing a file of more than about 96 KB in the Docker sandbox no longer fails; the contents are streamed into the container instead of passed as a command argument.
Linking a channel can’t be blocked by strangers (#935)
- daemon: at most 3 linking codes wait per sender, and 100 per channel as a flood guard. Before, 3 per channel meant three strangers mailing the agent address could stop everyone else from linking for an hour, again and again. Messages arriving together get one code, and old codes are cleared hourly instead of on every message.
Live replies are never compressed on the way through the proxy (#934)
- deploy: Caddy now leaves every event stream uncompressed, including the turn stream a reconnecting app resumes from, by matching on
Accept: text/event-streamas well as the known stream paths.
Replies are marked read only when you see them (#933, #932)
- flutter: a reply or error that arrives while the app, web tab or Mac window is hidden no longer marks the chat read on your other devices or clears its notifications. It is marked read when you come back to the chat.
- flutter: resuming a reply after reconnecting no longer leaves a network connection open each time, and stopping the resume cleans up after itself.
Messages and uploads wait out a bad connection (#931, #996, #1032)
- flutter: a message sent while another was still going out is sent straight after it, instead of waiting for the next reconnect and then reporting that the reply took too long.
- flutter: being offline or meeting a server error no longer counts toward giving up on a message, recording or document. They are tried again, waiting longer each time, and only a server that turns them down makes them fail.
- flutter: a document upload interrupted for more than two hours gets a fresh upload link for the same document, instead of failing and leaving an empty document behind.
- flutter: recordings still on the device are listed on the Record page with Retry and Delete. A recording that could not be uploaded stays listed instead of disappearing, and recordings left on the device are found again when the app starts. A recording cut off because the app was closed is listed the next time it opens.
- daemon:
POST /api/v1/user/documents/{id}/uploadgives a document still waiting for its file a fresh upload link.
A server restart no longer asks for a claim code (#930)
- flutter: if Celindor is restarting or unreachable when the app checks your account, the app keeps what it knew and checks again shortly, instead of showing the claim-code screen and remembering it.
A slow background check no longer holds up a reply (#929)
- daemon: The message classifier and the decision checks now give up after a few seconds and stop when you stop the reply, so a slow or stuck model call can no longer keep a finished answer waiting, and the agent remembers its short “Noted.” replies on the next message.
A reply cut off by the output limit no longer breaks the chat (#928)
- daemon: When the agent’s reply keeps getting cut off by the output limit, the unfinished step is dropped and the turn ends with a short note (or an error when nothing was said), instead of failing the next message or ending silently.
Refusals end the turn on every provider, and AI spend is counted and capped where the service pays (#926, #927, #1088)
- daemon: When Gemini or an OpenAI-compatible model declines (a refusal, a content filter, a safety or recitation block, a blocked prompt), the turn now ends with a refusal message, as it does for Claude, instead of being sent again up to three times and then reported as an empty reply.
- daemon: A Claude request that is stopped, stalls or loses its connection mid-reply, and every attempt that is retried, now counts the tokens it was billed, for chat turns and background work alike.
- daemon: Recordings transcribed and summarized on Vertex AI now have a price, and audio is priced at the model’s audio rate. A model or host without a price is named in the daemon’s log.
- daemon: The operator’s Gemini key now follows the Anthropic key’s rules: every user on dev, the team on test, and never in production. On an operator key, chat (missions, reminders and delegated tasks included) and ingest stop at the user’s monthly limit, and only a chat model with a price can be chosen.
- daemon: The monthly limit now counts what the service’s keys pay for a user, chat and background work, and no longer what users spend on their own key. Every background call records whose key paid.
Memory review follows your dream schedule (#925)
- daemon: The nightly memory review now follows each person’s chosen schedule (nightly, every few hours, when enough has piled up, or both), and a nightly review that comes due while another run is still going now runs right after it instead of waiting a day.
Syncing a mailbox by hand no longer adds its mail twice (#924)
- daemon: A mailbox now syncs one sync at a time, so tapping sync while the regular sync runs no longer turns the same email into two documents, and a failed manual sync now shows its error like a regular one.
One Drive file that can’t be imported no longer stops Drive sync (#923)
- daemon: a Google Drive file that fails to import (for example a Google Doc too large to export) is set aside and tried again after a growing wait, while the rest of your Drive keeps syncing; after eight failures it waits until the file changes in Drive.
A restart no longer leaves tool sandboxes running (#922)
- daemon: Shutting down now closes the API and stops chat sandboxes first, then stops every background worker at once and closes the database last, all within 25 seconds instead of being cut off after 5.
Chats keep their tools when two messages start one at once, and a database blip no longer turns AI features off (#920, #921)
- daemon: when a chat’s session started from two places at once (a message and a mission step, or the same message from two devices), the second start removed the first one’s sandbox and its tools failed mid-turn. The chat now starts one session and both share it.
- daemon: if the account’s keys could not be read once, for example during a short database outage, memory review, entity extraction, the classifier and the decision router stayed off for that account until the server restarted. They now try again on the next use, and a changed key reaches the classifier and decision router right away instead of the old key being kept.
Documents no longer stay stuck in processing after a restart (#919)
- daemon: A document whose reading or tidying was cut short by a crash or update now goes back in the queue after 30 minutes, and one that is interrupted three times shows as failed with a “Reprocess to try again” message instead of processing forever.
One person’s broken embedding key no longer stops everyone’s documents from being indexed (#918)
- daemon: A document whose embedding fails is retried later with growing waits and marked failed after eight tries, and the index queue now takes each person’s documents in turn, so other people’s documents keep getting indexed.
The server survives a dropped database connection (#917)
- daemon: a database restart or a dropped connection no longer stops the server and the replies in progress. The lost connection is logged and replaced.
A reply that fails partway remembers what it already did (#916)
- daemon: when a reply failed after the agent had used a tool (set a reminder, sent an email) and the AI provider then gave up, nothing of it was saved, so asking “did it work?” could make the agent do it again. The chat now keeps what the tools did, with a note that the reply ended in an error, and the agent sees it on the next message. A permission check that fails now refuses just that one tool instead of ending the whole reply.
A passing network problem no longer turns a mailbox off (#915)
- daemon: when a mailbox sync fails for a reason other than the sign-in (a timeout, a server error), the mailbox stays connected and is tried again after a growing wait. It only needs reconnecting after about ten failures in a row. Mailboxes that were turned off this way start syncing again, and reconnecting a mailbox restarts its sync.
Sub-accounts keep their reminders after the main account changes its keys (#914)
- daemon: when an account changed its AI keys, the accounts below it lost their background agent until the server restarted, and every reminder that came due in that time was thrown away. Those accounts now get a fresh agent on the new key right away, and a reminder that comes due while an agent is missing waits and is delivered on the next check instead of being dropped.
Synced emails show their real file size (#913)
- daemon: An email saved to the library now records the number of bytes it actually takes, so mail with å, ä, ö or emoji no longer shows a size that is too small.
Scrolling back in a thread no longer skips messages (#912)
- daemon: Loading older messages in a household thread or mission chat no longer skips messages sent at the same moment as the oldest one already shown.
Deleting a folder keeps its subfolders (#911)
- daemon: Deleting a document folder now moves its subfolders up a level along with its documents, instead of deleting the subfolders and dropping their documents to the top level; if a subfolder’s name is already taken there, the delete is refused and nothing changes.
Cancelling an import can no longer break one that has started (#910)
- daemon: Cancelling an import just as it starts now leaves the started import alone, instead of deleting its archive under it.
One data export at a time, and a finished export stays finished (#909)
- daemon: Tapping export twice at once no longer builds two exports.
- daemon: A problem removing an older export no longer marks the new one as failed and leaves its file behind.
Gmail sync no longer skips mail after a big backlog or a failed fetch (#908)
- daemon: a Gmail mailbox with more than 10 000 new messages is now synced from its oldest new message onwards, so none of the older ones are passed over. A message that fails to download stops the sync just before it and is tried again on the next run, instead of being skipped for good; a message deleted in Gmail meanwhile is passed over.
Moving a chat to another memory space takes effect at once (#906)
- daemon: After a chat is moved to another memory space, what the agent saves from it lands in the new space and what it reads keeps to that space’s walls, from its next step on, instead of keeping to the old space for as long as the agent’s session lasted.
- daemon: If the server cannot tell which memory space a chat is in (a database blip), the agent’s memory step fails and nothing is saved or read, instead of saving without a space and reading beyond the chat’s walls.
A fact saved in one memory space no longer lands in another (#905)
- daemon: When the same fact is saved in two memory spaces a few minutes apart, each space now keeps its own copy, and the second save no longer changes the first one.
Mining a recording again keeps suggestions flagged for you (#904)
- daemon: when a recording is mined again (after you link its speakers, say), a suggestion flagged for your review that names a person proposed from that recording now stays, and so does the person. Before, the person was withdrawn because only unflagged suggestions counted as still using them, which deleted a flagged relationship with them and left a flagged fact without its subject.
A name merged in from a suggestion is found again (#903)
- daemon: Merging a suggested person or thing into one already in memory now records the other name as an alias, keeps the attribute history in step and refreshes the search embedding, so the agent finds the entity by either name.
Merging two memory entries no longer loses links when something fails (#902)
- daemon: Merging two people or other entries now happens all at once, so a failure part-way leaves both entries as they were instead of dropping their relationships and channel identities.
A memory suggestion is saved once, even when you and dreaming save it at the same time (#901)
- daemon: Saving a suggested fact or person now claims it first, so when you approve a suggestion while dreaming promotes the same one, long-term memory gets one copy instead of two, and the second save is refused.
Rules and Soul save with an empty field (#900)
- admin: “Save rules” and “Save soul” no longer fail when a field is empty, and a field can now be cleared. A cleared agent name goes back to “Celindor”.
Notifications and sign-in links follow the account you are signed in to (#899, #964)
- flutter: signing out now stops this device’s notifications for that account, so on a shared iPad or Mac the next person no longer sees the last one’s replies. Whoever signs in next gets notifications straight away, without restarting the app.
- flutter: opening a sign-in link while you are already signed in no longer switches accounts by itself. The app shows both email addresses and asks whether to stay or switch. Switching signs you out in full first. A link that has already been used or has expired now leaves you signed in, and keeps any messages waiting to send.
A network blip no longer signs you out on the web (#898)
- flutter: in the browser, waking a laptop before Wi-Fi is back, or a server hiccup during an update, no longer sends you to the sign-in screen and drops your unsent messages. Only the server saying your session has ended signs you out; otherwise the app keeps you signed in, and on launch it keeps trying until it can reach Celindor.
Signing out keeps one account’s recordings and data from the next (#897)
- flutter: recordings and documents still waiting to upload now belong to the account that made them. They pause when you sign out and continue if you sign back in. If someone else signs in on the device, they are deleted and never uploaded to that account. Signing out also clears the cached profile, measurements, suggestions, library and missions, and resets your to-dos, missions, trash and settings in the app, so the next person starts with a clean slate.
Secrets saved right after a key rotation stay readable (#896)
- daemon: after
celindor rotate-dekororg-rotate-dek, the running server no longer keeps encrypting new secrets (provider keys, mail, calendar, contacts and Drive sign-ins) with the replaced key for up to five minutes, which left them unreadable for good. It checks for a newer key before every encrypt, and reads secrets the rotation re-encrypted without waiting for its cache to expire.
Rotating an encryption key can no longer lose secrets (#895)
- daemon: rotating a user’s or organization’s data key re-encrypts every stored secret (provider keys, mail, calendar, contacts and Drive sign-ins, own skills) and swaps the key in one transaction. A failure part-way leaves everything as it was, where before every secret not yet rewritten became unreadable for good.
- daemon: a secret saved while a rotation runs, or by a daemon still
holding the old key, stays readable: the replaced key is kept until the next
rotation, which re-encrypts what it opens. A daemon whose cached key is out
of date reads the new key again instead of failing. Migration
0092_dek_previous_key.sql;rotate-master-keyre-wraps the kept keys too.
Pinned build tools and narrower CI tokens (#894)
- deploy: the pandoc and typst downloads are checked against their sha256, and the oasdiff, aws-cli and site base images are pinned by digest.
- deploy: only the jobs that push images can write packages, and the contract check revokes its write token before installing dependencies.
API hardening: password spraying, forwarded headers, body size, error detail, session ids in URLs (#893)
- daemon: one address can no longer try passwords across many accounts: after 30 wrong passwords in 15 minutes, whatever the email, sign-in is refused from it for the rest of the window, and
/auth/logintakes at most 60 attempts per 10 minutes. - daemon:
X-Forwarded-HostandX-Forwarded-Protocount only when the connection comes from the proxy, so sign-in links and calendar or Drive connect links can’t be built on a host someone else chose. - daemon: the sign-in routes (
/api/v1/auth/*) refuse a body over 64 KiB with 413. - daemon: errors from the database or a calendar or Google Drive provider reach the app as a plain sentence; the detail stays in the server log.
- daemon: a session id in the URL (
?token=) is accepted only on the live streams, not on every route. - daemon: manual downloads and division audit exports carry the usual security and version headers.
- admin: signing out works only from the console’s own pages, and the daemon’s cookie clearing is passed on intact.
Operators can no longer see an incognito chat’s messages (#892)
- daemon: The admin view of a chat’s messages now answers “not found” for an incognito chat, as the admin chat list and activity feed already hid them.
Tighter limits on the sandbox’s network and Docker proxies (#891)
- daemon: The sandbox egress proxy now refuses the same private and reserved addresses as the daemon’s own web fetches, caps each sandbox at 32 open connections and closes connections idle for two minutes, and the Docker proxy refuses a detached exec start on every path, not only on upgraded ones.
Measurement lookups treat document text as untrusted (#890)
- daemon: When the agent looks up measurements from your documents, their labels and the document title are now marked as document text, so instructions hidden in a document cannot pass as part of the conversation.
Memory and membership changes check who may make them (#889, #951, #969, #972, #952)
- daemon: From a chat in one memory space, the agent can no longer add a fact to, or link an email address or chat account to, a person or thing in a space that chat can’t see. It answers as if that entry didn’t exist, the same answer it gives for one that doesn’t.
- daemon: A fact added to a household’s or team’s memory can only be about that organization’s own entry, in the fact’s own division or its shared memory. Naming anything else, such as another organization’s entry or someone’s personal one, answers “not found”. A personal fact moved into an organization no longer keeps a link to a personal entry.
- daemon: Merging a suggested entry into an existing one in an organization now needs write access there at that moment. A member can no longer overwrite an entry in a Sealed division this way, and someone who has left can’t use an old suggestion to change anything.
- daemon: Only the owner can make someone an admin. Admins can still invite people and add their own sub-accounts, as members or viewers. An admin invitation an admin sent earlier makes its person a member when they accept.
- daemon: Changing which organizations a sub-account is in now uses the same rules as the member screens: the member limit, invitations still waiting included; the owner can’t be removed; only the owner removes an admin; and a Sealed division keeps its last admin. The sub-account’s open household chats pick up the change, and it is told when it is taken out.
Calendar feeds no longer change each other’s time zones (#888)
- daemon: Time zones in calendar links and CalDAV calendars are read per calendar, so a zone one calendar defines no longer shifts event times in another user’s calendar, and a calendar that names a standard zone without defining it gets that zone’s real times.
Manual deploys accept only real image tags (#887)
- deploy: A hand-started deploy now refuses any image tag that is not
vX.Y.Zorsha-<commit>, and the commands sent to the server quote every value, so the tag field can no longer run commands on the server.
Admin sign-in only returns to admin pages (#883)
- admin: after signing in, the console only follows a
?next=link to one of its own pages. A link that points to another site, such as/login?next=https://evil.example, now lands on the start page instead.
Search text can no longer change the database query (#881)
- daemon: searching your documents (and the entity lookup used when no embedding provider is set) treats the search text as text. Before, specially crafted search text could become part of the database query and show documents that belong to other accounts. Searches with commas, brackets, quotes or
%now also find what they say.
A custom AI endpoint must be a public address (#879)
- daemon: the endpoint for an OpenAI-compatible provider (
chat_base_url) can no longer point at loopback, private, link-local or cloud metadata addresses. Saving one is refused, and every chat and summary request is checked again as it connects, after DNS, so a name that changes its answer later still gets nowhere. A refused endpoint says so instead of being retried. - daemon: operators who run a model on their own network (Ollama, vLLM) list its host in
[chat] private_base_url_hosts, e.g.["localhost:11434"]; users cannot. Metadata addresses stay refused there, and its redirects are not followed.
Sign-in links tell nobody who has an account, survive mail scanners, and live streams end at sign-out (#877, #1036, #880)
- daemon: asking for a sign-in link gets the same answer, at once, for every address. Whether it has an account, has asked for too many links or the mail server is down no longer shows, and asking for links on someone else’s behalf can no longer lock them out.
- daemon: opening a sign-in link in a browser shows a Sign in button instead of signing in on the spot, so mail filters that open every link no longer use it up. The apps sign in straight from the link as before.
- daemon: a password sign-in with an unknown address takes as long as one with a wrong password.
- daemon: live updates stop within 15 seconds of signing out, changing your password, the account being deleted or losing admin rights, and a connection dropped while opening no longer leaves work running on the server.
Rate limits follow the signed-in user (#876)
- daemon: the limits on chat turns, uploads, transcription and sign-in links count per signed-in user, else per client address. A made-up
Authorizationheader no longer gets round them, and several sessions of one user share one budget.
Connecting a calendar, Google Drive or a mailbox finishes in the app (#875, #1018)
- daemon: the Google and Outlook Calendar, Google Drive, Gmail and Outlook mail sign-in callbacks no longer connect anything. They check the one-time
stateand send the browser on to the app’s/connectwithstateandcode(or the provider’serror): the web app’s origin when the connect started there, else the server’s own. Before, a consent link someone started from their own account and sent to someone else put that person’s calendar, Drive or mailbox on the sender’s account. - daemon: new
POST /api/v1/user/oauth/complete({ state, code }), where the signed-in app finishes a connect. The state is used up whatever the outcome; another account than the one that started it gets 403 and nothing is connected. The answer says what was connected (calendar,driveoremail, and the provider). - app: a
/connectpage finishes the connect after the browser sign-in, says “Calendar connected”, “Google Drive connected” or “Mailbox connected”, or why not (refused access, an expired link, or a connect started from another account), and opens the matching Settings screen./connectand the five provider callback addresses are app links on iOS, Android and macOS, like/invite, so the app opens straight from Google’s or Microsoft’s consent page. - api: the minimum supported app version is now 0.7.0 (
MIN_SUPPORTED_CLIENT_VERSION): older apps can still sign in, but can no longer finish connecting a calendar, Google Drive or a mailbox with Google or Microsoft. Update the app.
GitHub tools can no longer read files on the server (#874)
- daemon (security): the agent’s GitHub tools now check every repo, path,
ref and commit they are given, and run
ghwithout the daemon’s own environment. Before, a craftedrefingh_show_filecould make the server read a local file, such as its master key, and send it to GitHub. - daemon: reading a file at a branch, tag or commit with
gh_show_fileworks again. It used to answer Not Found.
Forwarding between shared chats (#855)
- flutter: Forward in a shared chat message’s menu sends it to another shared chat you’re in, marked “Forwarded from …” with who wrote it; its files go along. When people there can’t see the chat it came from, the app says how many and asks first; a Sealed division’s message, or one across a wall, is refused. A division’s admins can export a new Forwards log.
- daemon:
GET/POST /user/threads/{id}/messages/{messageId}/forward. A forward that widens who sees a message needsconfirm_wideningand is logged inthread_forwardsfor the source division (audit exportlog=forwards); out of a Sealed division or between walled divisions it is refused (403). The forward is the sender’s, carriesforwarded_from(also on thethread-messageevent), copies the files under the destination, starts no turn and is never extracted into memory. Migration 0087.
Link previews in a shared chat (#854)
- flutter: a shared chat message’s first link shows a preview under it, with the page’s title, picture, site and a line of description; tapping it opens the link. Previews arrive and go live.
- daemon: after a member sends or edits a message, the daemon fetches its first link’s preview in the background through
safeFetch(public addresses only, checked as dialed and after every redirect; 3 redirects, 5 s, 512 KB of page, a JPEG/PNG/WebP/GIF image of at most 1 MB; no cookies, credentials or Referer). It keeps the image under the organization and serves it as a short-lived signed link, so apps never load anything from the linked site.ThreadMessage.link_previewand athread-link-previewevent are new; unsending drops the preview and its image. Migration 0086. The boundary is recorded indaemon/SECURITY.md.
Voice messages in a shared chat (#853)
- flutter: a microphone beside (+) in a shared chat records a voice message, up to five minutes, to send or discard. A voice message plays in the chat, and its words show under it, live, when they are ready.
- daemon: a shared chat takes audio (M4A, AAC, MP3, Ogg, WebM, WAV, up to 10 MB) as the thread’s file. The daemon transcribes it in the background with the recordings’ Gemini transcription, on the sender’s key or else the operator’s. The agent reads the words, waiting for them when it answers a voice message still being transcribed, and search finds a voice message by them.
ThreadAttachment.transcriptand athread-transcriptevent are new; personal chats still take no audio.
Chat info for a shared chat (#852)
- flutter: tapping a shared chat’s name, or the new (i) in its bar, opens its info. The info screen shows the agent, everyone in the chat with their role, the photos and files, and switches to mute the chat and to share read and typing. The photos-and-files button moved from the bar into the info screen. A shared chat keeps its agent’s name and picture.
- daemon:
GET /user/threads/{threadId}/membersgives each member’srole(owner, admin, member or viewer).
Pinned messages in a shared chat (#851)
- flutter: Pin and Unpin in a shared chat message’s menu. The latest pin shows at the top of the chat; tapping it goes to the message, and with more than one pin, All … lists them with who pinned each, to go to or unpin. Pins update live for everyone in the chat.
- daemon: a thread’s pins are the thread’s. Anyone in it pins or unpins any live message, up to 50 per thread, and unsending a message drops its pin. A pin stays when its pinner deletes their account. New routes
GET /user/threads/{threadId}/pinsandPUT /user/threads/{threadId}/messages/{messageId}/pin, and athread-pinsevent. Pinning is never a message: no turn, nothing unread, no push. Migration 0085.
Search in a shared chat (#850)
- flutter: a magnifier in a shared chat’s bar searches its messages by their words or the names of their files, newest first. Tapping a match goes back to the chat and brings that message into view.
- daemon: new route
GET /user/threads/{threadId}/search?q=, a case-insensitive substring search over the thread’s messages and file names, newest first and paged withbefore. Only people in the thread now can search it. Unsent messages are never found, and an edited one is found by its current text. The daemon never logs the query.
To review says where a suggestion goes (#849)
- flutter: a suggestion noticed in a household or team chat says where saving puts it (“Goes to Sales (Acme Inc)”). The same name suggested for your own memory and for a household, or for two divisions, is now two suggestions, so removing one no longer removes the other. Those suggestions no longer offer “Same as something else…”, which only searched your own memory.
- daemon: each row of
GET /api/v1/user/staginghas ahome(organization and division, or null for personal memory). A row bound for an organization no longer comes with matches from the user’s own memory, which could not be merged into anyway.
Division budgets count everything; undecided Sealed questions expire (#841)
- daemon: a division chat’s compaction summaries and memory extraction now count against the division’s monthly budget when the organization’s key pays.
- daemon: a question to a Sealed division that no admin decides within 14 days expires. The member’s card says the division didn’t answer in time and they get a notification; their agent tells them so.
- flutter: the chat card and the admins’ list of questions show an expired question as such.
Known vulnerabilities in dependencies fixed (#839)
- deps: the daemon’s mail libraries move to current releases (nodemailer 10, mailparser 3.14, imapflow 1.7), Hono to 4.13, and transitive packages with published fixes (node-forge, xmldom, protobufjs, gRPC, js-yaml and others) are lifted, closing most of the 153 advisories the OSV scan found.
- deps: the admin console moves to Next.js 15.5.29 with a patched sharp and PostCSS; the site moves to Astro 7.3.8 with patched undici, sharp, zod and fast-uri.
- deps: the unused npm lockfile in the daemon’s email package is gone.
The five advisories still open (xlsx, zod 3, uuid, a serveStatic flaw in
@hono/node-server) are listed in
osv-scanner.tomlwith the reason each one does not apply or has no fix yet.
Review says when saving a connection also saves someone new (#832)
- flutter: saving a suggested connection, such as “Anna works at Acme”, also saves Anna or Acme when they are not in memory yet. The suggestion now says so (“Also saves Anna”), both in the list and when you open it.
Ask the household agent to bring something back (#826)
- daemon: in your own chat with a household or team agent, ask it to bring back an entry removed from the shared memory in the last 30 days. It asks you to confirm, then restores the entry with what was removed with it.
- flutter: the confirmation asks Bring this back to the shared memory?
Memory history for households and teams (#820)
- flutter: a household’s or team’s page now has Memory history, showing who added, changed or removed what in its shared memory, newest first. Everyone in it can open it; spaces you can’t read are left out, and an entry in one is shown only as “An entry you can’t see”.
- daemon: new route
GET /user/organizations/{orgId}/memory/contributions. Migration 0080.
Restore what was removed from a household’s memory (#819)
- flutter: a removal in Memory history now has Restore for 30 days, for members who may add where the entry was. It brings back the entry with its facts and connections.
- daemon: new route
POST /user/organizations/{orgId}/memory/entities/{entityId}/restore. After 30 days the trash purge deletes a removed entry for good. Migration 0082.
Link a division’s entries to the shared memory’s (#813)
- flutter: on a team division’s screen, those who manage it see Linked to the shared memory. Entries with the same name and kind as one in the shared memory are suggested, to link or turn down for good. Any entry can be linked or unlinked by hand. A Sealed division gets no suggestions.
- daemon: link suggestions and turned-down suggestions for a division’s entries, under
/user/organizations/{orgId}/scopes/{scopeId}/entity-links. Migration 0083.
Web search: what failed, and who receives your searches (#804)
- flutter: a lookup that failed, such as a web search the search service refused, no longer counts as a source checked. The activity line says “Checked 2 sources · 1 failed”, or that every step failed, without the check mark. A failed step is marked when you expand it.
- flutter: Your data lists the search service that searches when your own fails, and says how long Brave keeps search queries (up to 90 days).
- flutter: the Web search screen says where to add a search key: Settings, under AI provider → Other keys.
- daemon: a
tool-resultevent carriesok: falsewhen the tool failed. - manuals: the user manual (EN, SV) covers the fallback, the shared search allowance and failed lookups; the admin manual covers the operator search key, the fallback order and the search limits.
Recent and local web results (#803)
- daemon: the agent’s web search can ask for results from the past day, week, month or year, and from a given country and language. For news and anything that changes, it now searches recent news reporting instead of answering from an encyclopedia (#624).
- daemon: if Brave doesn’t support the requested country or language, the search runs again without them rather than failing.
One way to search the web (#801, #802)
- daemon: the agent’s web search and the app’s Web search space now go through one search service. A provider that refuses this server, or a key whose plan is used up, is skipped for a while, and the next provider answers in its place. Each person gets one search budget across both.
- daemon: a failed web search or page read now counts as failed. The agent is told whether searching again can help, and says so instead of answering from older sources as if they were current.
- daemon: with an operator Brave key, dev and test search with Brave, as DuckDuckGo refuses the servers’ searches.
- daemon: on the Claude Code (subscription) path, Claude’s own WebSearch and WebFetch are switched off, so every search goes through Celindor’s.
- cli:
celindor web-search-checkruns one search the way the daemon does. Test deploys run it after the health gate.
Correct what Celindor remembers from a person’s page (#796)
- flutter: on a person’s or thing’s page in Memory, each observation has a ⋯ menu with Not true, Correct… and Forget, and each relationship one with Not true and Forget. Forget and Not true can be undone right away from the message that appears.
- flutter: facts you marked as not true are listed in a folded Not true section at the foot of the page, where Take back puts them back.
- flutter: Rename… (the pencil next to the name) renames a person or thing; the old name still finds them. If something of the same kind already has that name, the app says so.
Correct what Celindor remembers (#796)
- daemon: you can ask Celindor in a chat to forget one fact (“forget that Anna likes tea”), to correct one (“that’s wrong, Anna lives in Malmö, not Lund”), to stop using something that isn’t true, or to rename someone. A corrected fact is replaced on the same person or thing, and the old wording is not suggested again. Something you said isn’t true is kept out of Celindor’s answers and is never suggested again, from any chat, email or document. A forgotten fact can be learned again if it comes up later. A renamed person or thing is still found by the old name.
- daemon: facts that were replaced or marked as not true no longer show up anywhere Celindor recalls from: its answers, the frequently used memories, the memory browser and a person’s page.
- daemon: new API routes to forget, correct, mark as not true and take back a fact, and to rename an entity, for the app’s memory pages.
Photos and files in a shared chat (#784)
- flutter: the (+) in a shared chat’s message box adds files and, on a phone, photos. They can be sent with words or on their own, and show on the message for everyone in the chat. A button in the chat’s bar opens a gallery of everything sent there. Unlike your own chats, nothing is saved to a document library.
- daemon: a shared chat’s file belongs to the chat, not to its sender. Who can open it follows who is in the chat: a member who joins later can open earlier files, and one who leaves loses access. A file stays when its sender deletes their account, and unsending its message deletes it. Files are never in a member’s data export. The agent sees the files of the messages it answers. New routes:
POST /user/threads/{threadId}/attachmentsandGET /user/threads/{threadId}/files, andattachment_idson send. Files open throughGET /user/attachments/{id}/download, as in a personal chat. Migration 0083.
Mention someone in a shared chat (#783)
- flutter: typing @ in a shared chat suggests its members and its agent. A mentioned member’s name shows in bold, and a message that mentions you is highlighted. Picking the agent turns on Ask …. A reply that arrives live now shows its quote at once.
- daemon: a thread message can carry
mentions, the user ids of members who can read the chat; reads give their names now, so a renamed member still resolves. A mentioned member is notified even if they muted the chat. Mentioning a member never makes the agent answer, even one who shares the agent’s name.GET /user/threads/{threadId}/memberslists who may be mentioned.
Seen by and typing in shared chats (#782)
- flutter: under the newest message in a shared chat, Seen by … names who has read up to it, and … is typing… shows over the message box while someone writes. A message that arrives while the app is in the background is no longer counted as read.
- flutter: an eye button in the chat’s bar turns sharing on or off for you in that chat. It’s on to begin with in a household and off in a team. It’s reciprocal: when yours is off, nobody sees when you’ve read or that you’re typing, and you don’t see theirs.
- daemon:
GET /user/threads/{threadId}/reads,PUT .../activity,POST .../typing, andthread-read/thread-typingevents. They go only to other members of the chat who share. Typing is never stored, and a member’s typing events are limited to one every 3 seconds.
Edit and delete messages in shared chats (#781)
- flutter: edit your own message in a shared chat for 15 minutes after sending, from its long-press or right-click menu. Everyone sees it marked Edited, and the agent doesn’t answer an edit.
- flutter: delete a message from the same menu, after a confirmation that says what deleting does. It shows as This message was deleted for everyone, and quotes of it say so. The household’s owner and admins can delete anyone’s message.
- daemon:
PATCHandDELETE /user/threads/{threadId}/messages/{messageId}, and athread-message-updatedevent. A deleted message’s text is blanked for good, leaves the agent’s context and the unread count, and the household facts learned from that message alone are removed. An edit’s new text is learned in place of the old. Neither starts a turn or sends a push.
Replies and reactions in shared chats (#780)
- flutter: reply to a message in a shared chat by swiping it right, or from its long-press or right-click menu. The quote shows above the reply, and tapping it finds the original. Replying to the agent turns on Ask …, which you can turn off.
- flutter: react to a message with an emoji from the same menu. Reactions show under the message with who reacted, update live for everyone, and a tap on your own takes it back.
- daemon: a thread message can quote another of the same thread (
reply_to_message_id, returned asreply_to), and the agent reads the quote with the reply. Reactions have their own route,PUT /user/threads/{threadId}/messages/{messageId}/reactions, and athread-reactionevent. They are never messages: no turn, nothing unread, no push, nothing for the agent.
Shared chats send reliably and read like a messaging app (#779)
- flutter: a message in a shared chat shows the moment you send it. If it can’t go, it waits and goes by itself when the connection is back, with Send again and Remove. After a lost connection, the open shared chat catches up without being reopened.
- flutter: shared chats show the day and time, group one person’s messages under one name, and show members’ initials. Other members’ messages can be selected and copied.
- flutter: tapping a link in any chat message opens it.
- daemon: sending in a shared chat takes an optional
client_msg_id, so a retry never posts a message twice or starts a second agent turn.
Search the web without using AI (#763)
- flutter: a new Web search space searches the web and opens results in your browser. It uses no AI, so it costs no tokens.
- daemon:
GET /api/v1/user/web/searchanswers from the user’s search provider, at most 60 searches per 10 minutes. - daemon: when DuckDuckGo refuses a search or returns a page Celindor can’t read, the agent now gets an error telling it that searching again won’t help. Before, it could be told “0 results, try rephrasing” and spend tokens retrying.
Household missions in the app (#762)
- app: Missions lists your households’ missions with your own, with which member each runs for. Tap one to see how it stands and read its steps; nobody writes in a mission’s own chat. The member it runs for, and the household’s owner and admins, answer, pause, resume or stop it there. Everyone else sees what it asks and who can answer. A tool is allowed once only, with no “Allow always”.
- app: New mission lists your households’ agents with the household’s name, except where you are a viewer. A mission you start with one runs for you, on the household’s key.
- daemon:
GET /api/v1/user/organizations/{orgId}/missions/{missionId}/messagesreads a household mission’s chat, read only, for everyone who may follow it. Amission-updatedevent carriesorganizationIdfor a household mission.
Household missions (#762)
- daemon: a household agent can do a mission for its organization. A member (not a viewer) starts it, or the agent suggests one in the shared thread or a member’s own chat with it (
start_mission). Its steps run in a chat of the organization’s, for that member, on the organization’s key and within its division’s monthly budget, using household memory only. Every member who may read the agent’s memory follows it; the member it runs for and the owner and admins answer, pause, resume or cancel it, and an allowed tool is allowed once only. - daemon: what a household mission asks, and how it ends or why it paused, is posted in the agent’s shared thread, with the thread’s push. If its member leaves or loses the agent’s space, it pauses until an owner or admin resumes it and takes it over; a retired agent’s mission pauses, a deleted agent’s fails.
- daemon: new routes under
/api/v1/user/organizations/{orgId}/missions; aMissioncarriesorganizationId,actingUserId,actingUserNameandcanActfor household missions only. Migration 0086 adds mission chats (an agent keeps one thread) and the acting member.
Personal agents can ask a household agent (#761)
- daemon:
delegatenow reaches the household agents of the user’s organizations, named with their household (“Butler (Home)”), alongside the user’s own agents, and the chat’s prompt lists them. Only agents the user may already chat with are offered: in service, and for one tied to a space or division, only when the user reads it. It never goes the other way: household agents have nodelegatetool. - daemon: the household agent is told which member’s agent asks, answers in the member’s own chat with it, and runs on the organization’s key within its division’s budget, not the member’s quota. Its answer marks the asking chat as holding that organization’s memory, so it gets the source chip and its replies are not learned into personal memory. A household agent takes no missions (
wait: false).
Imported and Google Drive documents stay linked to their source (#757)
- daemon: finishing ingest erased what a document knew about its source. Google Drive sync then no longer recognised its own documents and imported the same files again, and re-running an import from another assistant created duplicates and brought back documents you had deleted. Documents ingested from now on stay linked. Copies already made stay until they are cleaned up.
The admin console counts all LLM spend, not only chat (#755)
- admin: the Usage page shows what background calls spend, per kind of call, and a true total with chat. The old total is now labelled as chat.
- daemon: extraction, dreams, ingest, transcription, compaction and the other calls outside chat turns are recorded with their tokens, time and cost, never their content. Monthly limits are unchanged.
See how fast and reliable turns are in the admin console (#754)
- admin: the Usage page has a Performance section: time to first text and whole-turn time, where the time goes, failures and stops, cost per turn and per user, prompt size, and breakdowns by day, model and tool.
- daemon: a new admin route serves those numbers. It holds counts, timings and money, never content.
Turn counts are right when a chat falls back to another model (#753)
- daemon: a turn that fell back to another provider counted as two turns in the usage views, in the app and on the admin page. It now counts as one.
- daemon: each finished turn’s timings and error kinds are now stored for 90 days, never its content, ready for the admin page to show.
See and cancel upcoming reminders in the app (#751)
- flutter: Settings › Reminders lists what is still to come, soonest first, with the next time and the schedule, and each one can be deleted. Until now the only way to see them was to ask the agent.
- daemon: new routes list a user’s upcoming reminders and cancel one.
The data export is more complete, and leaves out incognito chats (#748)
- daemon: the export now includes your agents, the skills that are on by default, each skill’s full details and reference files, the saved output of long tool results and archived memory files.
- daemon: incognito chats no longer appear in an export, along with everything attached to them.
- daemon: the zip’s
format_versionis now 2.
Restore an export into an empty account (#748)
- cli:
celindor restore --user-id <uuid> <bundle> [--dry-run]brings a Celindor export back into an empty account, on this deployment or another: memory, personal chats, documents, recordings and their files, to-dos, agents, settings and the user’s own skills. Stored keys, connected accounts, shares, organization membership, always-allow permissions and missions do not come back, and timers stay paused. Restoring from the app comes later. - daemon: the embedding backfill now also fills recording segments, which a restore brings back without vectors, and any others left without one.
A household agent can tell which memory entry is which member (#745)
- daemon: when a household or team agent searches its shared memory, an entry linked to a member’s account now says so, by the name members already see, and says when it is the member writing. So the agent can tell duplicates apart and answer who someone is, instead of guessing.
A household agent can remove an entry from shared memory (#744)
- daemon: a household or team agent can remove a wrong or unwanted entry from the shared memory when a member asks, in that member’s own chat with it. The member confirms every time. The entry and what was recorded about it are archived, not deleted, so they can be restored, and the removal is recorded as that member’s. An entry linked to a member’s account is never removed.
- flutter: the confirmation reads “Remove this from the shared memory?”.
The Mac app keeps running when its window closes (#735)
- flutter: closing the window on a Mac leaves Celindor running, so replies and notifications keep coming. A menu bar icon opens it again or quits it, and so does the Dock icon or ⌘Q. Settings has a new Open at login switch, off by default, that follows a change made in System Settings.
- flutter: the app notices a dead connection to the server within 45 seconds on every platform, and on a Mac it reconnects as soon as the Mac wakes from sleep.
Organization memory stats and contributions respect restricted spaces (#724)
- daemon: the stats and contributions of an organization’s shared memory now cover only the spaces you may read. Before, any member could get a restricted space’s contributions, with their ids, contributor names and notes, and counts that showed how much the space held. The owner, admins and members with a grant see what they did before.
Scan paper into one PDF (#716)
- flutter: on a phone, (+) in a chat or thread and the Library’s uploads have Scan a document. The phone’s own document camera finds the edges and straightens each page, and every page scanned becomes one PDF. A scan attached in a chat is saved to the Library like any other file.
Changelog entries no longer conflict between pull requests (#696)
- Repository: a change records its entry in its own file under
changes/unreleased/instead of editingCHANGELOG.md, so pull requests no longer conflict with each other there.scripts/release.shgathers the files into the release’s section, andCHANGELOG.mdkeeps the same layout.
Division budgets and audit export (#669)
- daemon: a division can have a monthly budget on the organization’s key. Its agents’ chats and thread replies, its answers to other divisions and its publish pass count against it. Its admins and the organization’s are warned at 50, 80 and 100 percent, and once it is used up the division stops until next month, saying why.
- daemon: a division’s disclosure and break-glass logs export as CSV for a date range, for those who may read them.
- flutter: Budget and audit on a division’s page shows this month’s spend against the budget, lets the owner and admins set it, and exports the logs.
- docs:
daemon/SECURITY.mdanddaemon/ARCHITECTURE.mddescribe the division model and what is enforced in the database and what a model judges.
Walls between divisions (#668)
- daemon: a team can wall two divisions off from each other. Neither division’s agent then answers the other one, or anyone in it, through any agent, and the database refuses to record such an answer. Only Members only and Sealed divisions can be walled off, and a walled division can’t be made Open.
- daemon: adding someone to both sides of a wall warns by default. An organization can block it instead, once nobody is on both sides.
- flutter: a Walls section on a team’s screen lists each wall and who is in both divisions. The owner and admins put walls up, take them down and choose whether to block joining both sides. Adding someone across a wall says so.
Sealed divisions answer selected divisions, with approval (#668)
- daemon: a Sealed division lists which divisions may ask it: their agents, and their members through any agent. Its agent drafts an answer at once, checked against its policy, and the question waits until one of its admins sends the draft, cuts it, or declines. The database refuses a request from anyone not on the list, lets the agent read only while drafting, and refuses an answer on the record without an admin’s decision.
- daemon: when an admin decides, the asker’s card changes and they get a notification; their agent hands the answer over with a new
division_answerstool. A Sealed division is never asked from a shared thread. - daemon: one person can ask any one division at most 5 questions an hour, and a division’s admins get a notice when someone asks it much the same thing several times in a day.
- flutter: a Sealed division’s page has Who may ask and Waiting for approval, where admins read the question and the draft and send or decline. The chat card says “Waiting for HR” until they do.
Choose what a division shares, in the app (#667)
- app: a team division’s screen has Sharing with everyone for those who manage it: the three lists, the facts waiting for approval (share, in fewer words if you like, or keep), and what the division has shared, which can be taken back.
Divisions share with the whole team through their own lists (#667)
- daemon: a background pass sorts each division’s new facts by its stewards’ three lists, always share, ask first and never share, and copies what may be shared into the team’s shared memory, noting the division it came from and who approved it.
- daemon: facts that ask first wait for the division’s stewards, who can share them, in fewer words if they like, or keep them. A steward can unpublish a shared fact.
- daemon: a fact about a person always waits for a steward, an answer kept from another division is never shared, and a Sealed division shares nothing by default.
- daemon: “always share” waits for a steward too until the operator sets
CELINDOR_AUTO_PUBLISH=on.
A name can be kept in more than one part of an organization (#667)
- daemon: each division or space can keep its own entry for the same name, so Sales and Support can each have their own Acme, and the shared memory one too. What one holds stays there.
- daemon: a division’s entry can link up to the shared entry for the same thing, and its agent then sees both. It never sees another division’s.
- daemon: deleting an Open division that keeps a name the shared memory also has merges the two.
- daemon: extraction in a household or team chat only lists entries from that chat’s own memory for the model, so a Sealed division’s names no longer reach it.
One division can ask another (#666)
- daemon: in a team, an agent can ask another division a question for the member it is helping. That division’s agent answers from its own memory, and the person asking gets no access to that memory.
- daemon: every draft answer is checked against the division’s own rules, and may be cut or declined. Who asked, what, what was read and what was said go in a log those who manage the division can read. If it can’t be logged, it isn’t answered.
- daemon: answers aren’t learned from or saved, unless the asked division allows keeping them.
- flutter: a card in the chat shows “Asking Sales…”, then how it went. A division’s page has its rules and its log, for those who manage it.
Every team has a front desk agent (#666)
- daemon: every team gets one front desk agent, made automatically, first in its list and renamable. It reads the team’s shared memory and Open divisions, and asks other divisions for you.
- daemon: the front desk knows the names of things each Members only division keeps track of, so it knows who to ask. It never sees their content, and never mentions a Sealed division.
- daemon: in an existing team, the oldest agent outside any division becomes the front desk, and from then on reads only shared memory and Open divisions. A team without such an agent gets a new one.
- flutter: a front desk can be renamed, but the app no longer offers to retire it, delete it or move it into a division.
The app says why a household agent can’t move (#665)
- flutter: when a household agent can’t leave its space because its shared thread has messages, the app now says so and suggests creating a new agent, instead of only saying the agent could not be saved.
Agents say where an answer came from, and keep a space’s memory in that space (#665)
- flutter: a reply that read a household’s or team’s memory shows where from under it: “Shared” or a space’s name, and in a personal chat also which organization.
- daemon: recall names where each memory is kept, so an agent can say so.
- daemon: a fact from one space no longer moves or changes an entry kept in another space or in the shared memory.
- daemon: the dream’s review of the shared memory no longer reads what spaces hold, and a fact can only be about an entry its own readers may see.
- daemon: once a chat has read organization memory, Celindor stops learning from that chat’s replies, so none of it is copied into your personal memory later. What you say yourself is still learned. The same holds when an unscoped household agent reads a space: its replies stay out of the shared memory.
- daemon: a household agent can’t be moved out of its space once its shared thread has messages, since that would show them to everyone in the new place. Create a new agent instead.
Choose who reads a space or division: Open, Members only or Sealed (#664)
- flutter: each space and division shows who reads it, and those allowed can change it under Who reads it. Widening asks first and says who will be able to read. On a Sealed one, the owner and admins get Break the glass instead of its people, and its admins get a notice saying who did it and why.
- daemon: a scope has a posture. Open: everyone in the organization reads it. Members only: those with access plus the owner and admins, which is what every existing scope stays. Sealed: only those with access; the owner and admins can’t read it, grant themselves access, open it up or put an agent in it, and read it only through a logged, expiring break-glass. A new team division starts Open and a new household space Sealed. A Sealed scope is left out of personal chats and is only shown to its members, the owner and the admins.
- daemon: a scope that isn’t Open can no longer be deleted while it holds memory. Before, its memory moved to the shared memory, which everyone could read.
- api:
postureon scopes (isolation_levelis deprecated and ignored), newGETandPOST …/scopes/{scopeId}/break-glass, and abreak_glassnotice kind withscope_nameandreason.
The message classifier runs on Claude Haiku 5.5 (#656)
- daemon: the classifier that labels each user message (shape, threshold,
permission) moves from Claude Haiku 4.5 to Claude Haiku 5.5, with thinking off
at low effort. On 100 real dev messages it cost about a seventh as much
($0.00046 against $0.00338 per message), answered faster (median 1.3 s against
1.8 s, p95 2.5 s against 3.7 s) and parsed every answer. Its labels agreed
with Haiku 4.5 on 90% of shapes and 78% of thresholds; Haiku 4.5 agreed with
itself on all of them. The classifier still only logs unless a user turns on
dispatch.
scripts/compare-models.mjsin the classifier package repeats the comparison inside a daemon container.
Agents in the app: clearer memory labels and fewer dead ends (#650)
- flutter: Manage agents says what each agent remembers, in the words of its edit dialog, instead of “Own memory” for both kinds.
- flutter: an admin can bring back or delete a retired household agent from a menu, not only by swiping.
- flutter: the sidebar shows “3 more” past the agents it lists, says so when agents can’t be loaded, and keeps an Agents icon when collapsed. An organization’s screen has a Manage agents row for its admins.
- flutter: picking a bundled face no longer writes an internal address into the image address field. An untitled chat is named in the app’s language, and screen readers say when an agent is a household one.
- flutter: a memory space made for an agent that then couldn’t be created is removed again, and agent lists reload when you switch account.
Household agents get their own introduction and don’t touch a member’s key (#649)
- daemon: a household agent’s first message to a member no longer says the member just created it.
- daemon: a household agent’s prompt describes the shared memory and tools it actually has. In a private chat it is told it records nothing, which is what its tools already enforced.
- daemon: a household agent’s turn no longer runs the member’s classifier, dispatch or decision router, so it costs nothing on the member’s key and can’t answer without the agent’s persona.
- daemon: starting a chat with an agent no longer hands back an archived chat, or an empty one bound to memory the agent has since moved away from.
- daemon: two agents created at the same moment can’t go past the limit of 50, and looking up a chat is always scoped to its user.
Memory keeps up while an import is running (#622)
- flutter: Memory showed its counts, lists and graph as they were when first opened, so during a Google Drive import it could say “(1)” on every box, with an empty graph, until the app was restarted. While it is on screen it now checks every 15 seconds and when you come back to it, and fills in as memory grows. A list you have scrolled down stays where it is until you are back at the top.
- flutter: long-term memory says how much is still waiting in short-term memory. What an import finds is saved there first, usually overnight, so long-term could honestly show “(1)” for hours, which looked broken.
Tell Celindor what kind of thing something is (#591)
- daemon: you can ask Celindor in a chat to change what kind of thing it remembers something as (“Taligent is a company, not a project”). Everything it knew about it stays, and it is still found under the old kind.
Claude subscription tokens refused in production (#495)
- daemon: A production server refuses Claude chats that would run on a subscription token, with a clear error instead of an unsupported path, and does not start with
[chat] claude_api = false.
Several lookups at once no longer wait for each other (#491)
- daemon: when the agent asks for several memory lookups or web pages at the same time, they now run together, so the reply waits for the slowest one rather than for all of them added up. Anything that changes something still runs on its own, in order.
Top bars use one title size (#482)
- flutter: a chat’s title was 15 points, a page’s 17 and a sheet’s 18, so the title in the top bar kept changing size as you moved around the app. They are all 17 now. On Android, pages’ titles follow too.
Toasts stay above the keyboard on iPhone and iPad (#413)
- flutter: a toast shown while you were typing landed behind the keyboard, so you could miss it, along with its Undo. It now floats above the keyboard and the message box.
A Drive file you deleted can be fetched again (#412)
- flutter, daemon: a Drive file permanently deleted in Celindor could never come back: the picker still let you tick it, and sync quietly skipped it. The picker now marks it Deleted in Celindor, and ticking it fetches it again, even under a chosen folder. Re-ticking the folder alone doesn’t, so your deletions in it stay deleted.
Sandboxes are kept apart and within their limits (#408)
- daemon: Each chat’s sandbox now has a network of its own that only the egress proxy shares, a command that times out stops everything it started, and the workspace and home directory are capped at 512 MB and 256 MB.
- deploy: The shared sandbox network is gone; the egress proxy has a fixed container name (
celindor-<env>-egress-proxy) that the daemon and docker-proxy are given.
Supply-chain checks (#142)
- ci: every third-party GitHub Action is pinned to a commit, with its version in a comment. Dependabot proposes the updates in one PR a week.
- ci: a new Supply chain workflow scans every lockfile for known
vulnerabilities (OSV) on each PR, on
mainand weekly. The ones already present are listed inosv-scanner.tomlwith an expiry date (fixes: #839), so only new ones fail. - ci: PRs are checked for added code that decodes a hidden payload and
runs it (
scripts/check-diff-payloads.sh). - The release-age gate for new package versions needs pnpm 10 and is #838.
A reply can’t get stuck on a silent connection or a stuck tool (#141)
- daemon: a tool that never returned used to hold the reply for good, and Stop couldn’t end it. Stop now ends the turn within 10 seconds, and a tool that runs for 6 minutes becomes an error the agent can work around. A Claude reply that sends nothing for 3 minutes now counts as a timeout, so it is sent again, or moves to the chat’s fallback model, rather than waiting on the connection.
Hermes conversations import as you saw them (#140)
- daemon: turns with an image keep their text and never bring in the image
data. Turns that compaction archived are kept, and the summaries the model
wrote in their place are left out. A
/branchor a reset stays its own conversation, and kanban workers and delegated runs are left out.
Talk to your agent by email (#135)
- daemon: a server can now have an agent address. Mail it from your own
address and you get a pairing code back; once the code is entered in the
app, every thread you start there becomes a chat in the app, and the agent
answers by replying in the thread. Only mail your mail server vouches for
(DMARC or DKIM) is answered. What you forward is read as someone else’s
text, never as instructions. Anything that needs your approval is turned
down, with a note to open the chat in the app. Operators set it up with
CHANNEL_EMAIL_*(see the administrator manual).
Settings › Linked channels (#135)
- flutter: a new screen under Settings › Linked channels shows your server’s agent address (tap it to copy it) and the email addresses linked to your account. Enter the code the agent address mailed you to link your address, or tap Not me to turn it away. Unlink an address there too; its chats stay. English and Swedish.
Save a chat as a skill in one step (#119)
- daemon: a message that starts with
/learnasks the agent to draft a skill from the chat so far, or from a link or notes written after the command. The agent proposes it withskill_manage, so it waits for the user’s yes like any other proposal. The stored message keeps the user’s words; only the model sees the expanded prompt. The turn always goes to the agent, never a dispatch reply. Where no skill can be proposed (incognito, mission and household chats) the agent says so. - flutter: a chat’s menu (chat list, sidebar) has Save as a skill,
which opens the chat and sends
/learn.
Celindor suggests skills on its own, after a chat it learned from (#117)
- daemon: every 10 completed user turns in a chat, or 30 tool calls, the
agent looks back at the chat in the background and may propose skill
changes, above all where the user had to correct it. It only proposes: each
proposal waits for the user’s yes like one asked for in a chat, and is marked
as the agent’s own. The review replays the chat with the same system prompt
and tools, so the provider’s cache serves it; only
skill_viewandskill_managemay run, at most 16 requests, and nothing it says is stored. Its tokens count against the user’s monthly quota, and it is skipped once 90% of them are used. A turn whose agent already saved a skill or a memory starts the count again. Not after a stopped or failed turn, nor in incognito, mission or household chats, nor on the Agent SDK path.[chat] skill_review_every_turnsandskill_review_every_tool_callschange the triggers; 0 turns one off. - flutter: Settings → Skills marks a proposal the agent made on its own.
Celindor learns skills you ask for, and asks before saving them (#116)
- daemon: a new
skill_managetool lets the agent propose a skill of your own, or a change to one: create it, change part of it, edit it, add or remove a reference file, or turn it off. Nothing is saved until you approve the proposal; all its changes are saved together or not at all. The agent must have loaded a skill in the same turn before it may change it, pinned skills are left alone, and a proposal that reads like an injected instruction is refused. Changing a built-in skill makes your own copy, used instead of it for you only. Your own skills and proposals are encrypted with your key, rotate with it, and come with the data export as SKILL.md files. Not in incognito chats or missions. Needs migration0071_skill_manage.sql. - flutter: Settings → Skills lists the changes waiting for your yes, with what each would write, and your own skills.
- api: new routes
GET /api/v1/user/skills,GET /api/v1/user/skill-proposals, andPOST /api/v1/user/skill-proposals/{id}/approveand/reject.
A reminder that asks for something now does it (#107)
- daemon: a reminder such as “daily email digest” used to announce the digest and stop there, because a fired reminder could only say something. When its note asks for something to be found out, the chat’s agent now reads your mail, calendar, memory, contacts, to-dos and earlier chats, and sends the result. It can’t change or send anything or go on the web, and the result isn’t saved to memory. If it can’t finish, you get the plain reminder and a note that it wasn’t done. Reminders like “drink water” work as before.
Finding files that deletes left behind (#79)
- daemon: Once a day the server lists stored files older than a week that nothing points at any more, such as files from deletes before the deletion queue; it only logs them unless the operator turns deleting on.
Linking a meeting’s speakers no longer loses suggestions (#38)
- daemon: if the model call fails while a recording is mined again after you link its speakers (an expired or rate-limited key, say), the suggestions from it still waiting in review now stay. Before, the failed call counted as “nothing found”, so they were replaced with nothing. A failed part of a first mining now also shows up in the recording’s note.
- flutter: “Who is this?” no longer jumps right after it opens. It used to search for the speaker’s name by itself and push the suggestions down, so a quick tap could miss. Matches for what you type now come after the suggestions, without repeating anyone already suggested.
What Celindor forgets, and when, now matches what it says (#710)
- daemon: something flagged for your review disappeared from the Memory tab after 30 days, though it never expires and stays in To review. It now stays in both, and no longer claims a date it will be dropped.
- daemon: what was dropped is now erased, not just hidden: 30 days after it expired or the dream let it go, and with the dream journal’s note about it. What you turned down yourself is kept for a year, so it is not suggested again, and then erased.
- daemon: a fact past its end date (“lived in Oslo until 2022”) ranks lower in recall and the agent is told it ended, instead of treating it as current. It can still be found. Old facts now weigh less in every chat, not only in chats outside a namespace or household.
- daemon: a dream cut off by a restart is closed as failed instead of showing as running forever.
- admin: removed what did nothing: the setting for how soon pending items expire, and the whole Memory policies page, with its API routes.
- manuals: flagged items wait for you, when dropped items are erased, and the Trash holds documents and recordings, not messages.
Choose what a memory suggestion is the same as (#679)
- flutter: a suggestion for memory could only be merged into the one entity Celindor guessed. A new Same as something else… choice lists the other likely matches and searches everything in memory by name, also when Celindor found no match at all.
iCloud contacts (#712)
- daemon: an Apple app password given for iCloud mail or calendar can now
also read that Apple ID’s contacts, only after the user says yes. The agent
can look people up with a new
search_contactstool. Contacts are read live and read-only; the address book is not stored, and the tool is not available in a household’s shared chats. - flutter: a new Settings › Contacts screen connects, disconnects and browses them, and connecting iCloud mail or calendar offers them too.
- api: new routes under
/api/v1/user/contacts.
Household agents follow permission and agent changes right away (#645)
- daemon: a chat with a household agent kept what was true when it started until it had been idle for a while. A member made read-only, or taken out of a space, could keep saving to the household’s memory, and an admin’s changes to an agent (its persona, its space) did not reach chats that were already open. Now every save to the shared memory checks again whether the member may still write there, and refuses if not (or if that cannot be checked). Changing, retiring or deleting a household agent, and changing a member’s role, their spaces, or removing them, ends the affected chats’ sessions, so the next message starts one with the new settings.
Agent chats open where you start them, and retired agents behave (#648)
- flutter: a chat started from New agent, from Manage agents or from the “+” in the agents sheet now opens: the sheet closes, a phone’s sidebar closes and the chat page comes forward, even from Settings or Missions.
- flutter: in a retired agent’s chat the bar no longer offers Edit agent (which did nothing) or a new chat with the agent (which the daemon refuses); a short line says the agent is retired and the chat goes on. Edit agent from the chat list finds retired agents too.
- flutter: missions by a retired agent keep its name and face, and it stays in the agent filter; while the agents load, a mission names no one instead of briefly saying “By Celindor”. Celindor’s missions show Celindor’s face.
- flutter: Open chat on a delegated task opens the chat the task ran in, not just the agent’s newest chat.
- daemon: the delegate tool’s result names the chat the task ran in, and
the stored reply keeps it (
toolCalls[].chatId), so the app can open it. A chat’s agent says when it is retired (agent.retired).
Add someone to a household without an email or password (#714)
- flutter: household owners and admins can Add without email: only a name, then a setup link shown as a QR code and a link to copy. The person opens it on their own device, chooses the email address they sign in with (and a password, except on the web), and is signed in. Until then the member list says they are not set up yet, and their row offers a New setup link.
- admin: Settings › Sub-accounts creates an account with a setup link by default; setting an email and password at once is still possible. Such a password now needs 8 characters, as the server always required (6 or 7 failed with an error).
- daemon:
POST /api/v1/user/sub-accountsis on the contract and takes email and password as optional, together; without them the response carries a single-use setup link valid for 7 days. New:POST /api/v1/user/sub-accounts/{id}/setup-link, and the publicPOST /api/v1/auth/setup/previewandPOST /api/v1/auth/setup. Members and sub-account lists carrysetup_pending. Adding a sub-account to an organization now respects its limit of 20 people.
Send a picture from the camera or Photos (#716)
- flutter: on iPhone and Android, (+) in the composer now also has Take a photo and Choose a picture. A message can be just the picture. Pictures are scaled down to send, and aren’t saved to the Library.
The same fact is not suggested for memory again (#703)
- daemon: a fact found again in a new email, document or chat no longer shows up in Suggestions for memory a second time. It is left out when it says the same thing in the same words (order, case and punctuation aside) about the same person or thing as a fact already waiting there, saved for later, in memory, or removed by you. A fact that differs by a single word (“tea” or “coffee”, “sold” or “bought”) is still suggested, and so is a reworded one. This covers your personal memory; household suggestions are unchanged.
Link a meeting’s speakers to people Celindor knows (#38)
- flutter: tap a speaker in a recording’s review to link them to a person Celindor knows, or to yourself. It suggests people by the name heard in the recording, the names the summary gives the speakers and the people mentioned, and you can search by name. Linked speakers are marked.
- daemon: a transcript speaker can carry the person it is
(
entity_id, additive), andGET /api/v1/user/recordings/{id}/peoplesuggests and searches people. What a linked speaker said is attributed to that person when the recording is mined for memory. Linking after the recording was processed mines it again, replacing its suggestions still waiting in review; reviewed ones stay, and nothing withdrawn this way counts as removed.
Browsing shared memory keeps restricted spaces private (#708)
- daemon: the organization memory lists (entities and notes) now show only the spaces you may read. Before, a member without access to a restricted space could list its entities and notes by naming the space, or see them in the listing of the whole organization. Naming a space you may not read, or one that is not the organization’s, now answers “not found”.
Household agents keep to their space (#646)
- daemon: a member’s chat with a household agent now remembers where the agent works now. Before, it kept writing to the space the agent had when the chat was started, so after an admin moved the agent its chats remembered in the old space while its tools wrote to the new one.
- daemon: a space can no longer be deleted while an agent works in it, retired or not: the answer names the agents. Deleting it turned such an agent, and its shared chat with all its history, into one for the whole household.
- daemon: an agent that works in a space is listed, and can be chatted with, only by the people who may read that space.
- flutter: deleting a space an agent works in says so, and what to do.
Listing shared memory keeps restricted spaces private (#702)
- daemon: when the agent lists an organization’s shared memory for you, it now shows only the spaces you may read. Before, a member without access to a restricted space (a household’s “Parents” space, say) could see that space’s entities and notes in the full listing, or by naming the space.
Mission steps know today’s date, and a stalled step pauses the mission (#699, #700)
- daemon: a mission step that stalls for two minutes or runs out of time now pauses the mission and says why above its chat (“The step stopped after 2 minutes without progress. Resume to try again.”), and is not counted as a step taken. Before, the mission moved on as if the step had finished, and the chat showed only how the step began.
- daemon: mission steps and tasks handed to another agent now get the current date and time in your timezone, as your own messages do. Before, a daily digest could not tell what day it was.
Pausing or stopping a mission stops its spending (#643)
- daemon: pausing or stopping a mission now ends the step it is running within moments. Before, the step ran on for up to 10 minutes and kept spending.
- daemon: a mission step can no longer hand work to another agent. That agent’s costs were not counted against the mission’s budget, and its tools were refused instead of waiting for your OK.
- daemon: on an AI model with no known price, a mission now pauses after each step and says why, since its spending can’t be checked against the budget. Resume runs one more step.
- daemon: a step’s own cost now counts toward the budget as soon as the step ends, so the next step doesn’t start once the budget is spent.
Delegated tasks and mission reports keep saying where they came from (#644)
- daemon: a task one of your agents hands to another now reads as handed over by that agent when the chat is picked up again later, not as something you typed yourself, and it is never read for memory about you.
- daemon: the answer a delegated agent sends back, and a finished mission’s report in the chat it came from, now reach the agent marked as another agent’s output, to be read as information rather than obeyed, like a web page or an email. A mission’s report no longer reads as the agent’s own words. The chat shows both as before.
Recordings keep going with the screen locked (#717)
- flutter: a recording on iPhone no longer stops when the screen locks or you switch apps (the app now declares background audio). On Android a foreground service with a silent Recording notification keeps the microphone running while a capture is open.
A long sidebar scrolls all the way to the top (#698)
- flutter: with a folded section (such as Spaces) and a sidebar longer than the window, scrolling back up kept throwing the list back down, so its top could not be reached. The sidebar now scrolls smoothly to the top and stays where you leave it.
Quotes in replies are readable in dark mode (#697)
- flutter: quoted text in a reply, such as an email draft, sat on a pale blue box in light grey text and was almost unreadable in dark mode. Quotes now have a faint background with a coloured line on the left and use the normal text colour, in light and dark mode. The same goes for quotes in your own messages, household threads, the manuals and What’s new. Links in replies use the app’s link colour.
Household agents’ replies are remembered on the household’s key (#638)
- daemon: what a household agent says in a member’s chat is now read for memory with the household’s key and entities. Before, it used the member’s key, so the member paid for it, and the member’s own names and entities went into the request.
A household agent never falls back to the wrong memory (#637)
- daemon: if a household agent or its space can’t be loaded, the turn now fails with an error. Before, a brief database error could run a household chat on the member’s personal memory and key, or send a space’s memories to the whole household, until the chat’s session ended.
A reply no longer gets stuck until the chat is opened again (#681, #682)
- flutter: the app reads the live connection to the daemon more carefully. Å, ä and ö no longer turn into “��” when they fall on the boundary between two network packets, which happened often now that replies stream. One event the app cannot handle no longer stops every event after it, so a reply that finishes while you watch now shows up without leaving and reopening the chat.
Your question stays in view while the reply comes in (#670)
- flutter: a reply that streams in follows down only until your message reaches the top of the screen, then stops there, so a long reply no longer pushes what you asked out of sight. Scroll on to read the rest.
A connection you save for later can be saved (#683)
- daemon: merging two entries in memory also moves the suggestions that mention them. Before, the merge could fail, or leave a connection under Saved for later that named a person but could only answer “one of the people in it is no longer available”.
- daemon: a connection to a person Celindor folded into another suggestion is saved with that one.
- daemon: a connection whose person you removed leaves the suggestions on its own, instead of waiting for a Save that cannot work.
Suggestions you removed stay removed (#684)
- daemon: a person, place or organization you remove from Suggestions for memory is not suggested again by the next email, document or chat, and neither are facts or connections about it. A connection you remove is not suggested again either.
- daemon: something named only in a connection (“works at Rutin”) joins the suggestion already waiting for it, instead of a new one per document.
Marketing email is not turned into memory (#677)
- daemon: with Learn from this mailbox on, an email that reads as marketing (newsletters, adverts, offers, app promotions) is still searchable in the Library but no longer produces Suggestions for memory. Most of it was already skipped by its headers; this catches what the headers don’t show.
An email in the Library opens (#680)
- daemon: tapping a synced email in the Library opened a blank page in Safari. Emails, Markdown, CSV and JSON files now open as readable text, and plain text keeps its å, ä and ö.
Long missions keep using their tools (#641)
- daemon: each mission step, task handed to another agent and new agent’s introduction now starts with the full amount of tool output a turn may use. Before, these turns shared one allowance per chat, so after a few busy steps every tool result in a mission said its limit was reached and the mission could no longer search or read anything. Memories and to-dos saved during these turns now point at the right message instead of an earlier one.
Organization memory stays out of your personal memory (#662)
- daemon: your personal agent can still read your organization’s shared memory, but when a reply draws on it, that reply is no longer turned into personal memory. Before, a fact from the organization that the agent repeated could end up in your own memory and stay there after you left the organization. What you write yourself is remembered as before.
A household thread keeps restricted spaces private (#639)
- daemon: in a household thread, an agent that is not tied to a space now
recalls only the household’s shared memory, never what is kept in a space.
Before, when a member who can read a restricted space (say “Parents”)
addressed the agent, what it found there could be answered into the thread,
where every member reads it. In your own chat with the agent nothing changes.
Needs migration
0068_thread_recall_unscoped_only.sql.
One turn at a time in a chat (#642)
- daemon: a message you send to a chat while the agent is still working there (on a task another agent handed it, a mission step, or your message from another device) now waits for that work to finish instead of running alongside it, so the two no longer mix up the chat. Stop the running work to go first.
- daemon: when one agent hands a task to another that you are talking to right now, the task goes to a new chat with that agent instead of the one you are in.
- daemon: a message you send to a mission’s chat no longer follows the mission’s rules for tools: it asks you as usual, and no longer pauses the mission or denies your tool.
Editing an agent keeps its memory setting (#640)
- flutter: saving an agent, for example after renaming it, no longer changes how it uses your memory unless you changed that yourself. Before, an agent set to “Starts from scratch” could be switched to “Knows you” if the app could not read its setting when you opened it.
Replies appear as they are written (#489)
- daemon: on Claude, the reply reaches the app while the model writes it, instead of all at once at the end. OpenAI and Gemini still show it at the end.
- daemon: if you stop a reply, what you had read so far is kept. If the connection to the model drops partway through a reply, you get an error rather than a second, different reply after the first.
- flutter: a reply that fails or is declined partway is taken off the screen, with the error in its place, since it was not saved. The same in household threads.
The number on the app’s icon counts chats
- daemon: the icon badge (iPhone, and the Dock on a Mac) is now the number
of chats with something new, not the number of unread messages, so “3” is
three chats you can find in the app. Archived and incognito chats no longer
count. Household chats you are in now count, unless you muted them. A chat
with no read position counts only replies from the last week, so an old
chat nobody opened no longer keeps the icon badged. Archiving, deleting or
muting a chat, or reading a household chat, updates the badge. Pushes that
don’t carry a count, such as “Review needed”, leave the badge alone instead
of setting it to 1. Needs migration
0067_icon_badge_counts_chats.sql. - daemon: each Android notification is tagged with its chat, so a new message replaces the chat’s previous notification instead of stacking up.
- contract:
GET /api/v1/user/unreadreturns the badge’s number (unreadChats), andPOST /api/v1/user/unread/mark-readmarks every chat and household chat read (additive). - flutter: the sidebar shows “3 chats with something new” with Mark all as read. The app sets the icon badge itself when it opens or comes back, instead of waiting for the next push. On Android, reading a chat removes its notification, and marking everything read removes them all. An installed web app shows the number on its icon where the browser supports it.
Proactive messages stay out of shared and private chats (#647)
- daemon: the agent’s own messages and reminders now go only to plain chats and chats with a personal agent. They could land in a household agent’s chat, where they could reach shared memory, or in an incognito or read-only chat. A reminder set in one of those goes to another of your chats.
Saving what Celindor suggests now saves it (#528)
- daemon: saving a suggested relationship also saves the people in it who were still suggestions. Before, it was quietly put under “Saved for later” and nothing reached your memory. If it can’t be saved, you are told.
- daemon: Save and Remove now work under “Saved for later”. Before, they failed and the suggestion came back.
- daemon: a relationship already saved for later is not suggested again.
An ordinary chat looks in a household’s shared memory
- daemon: the agent in an ordinary chat is told that the groups the user
belongs to (a household included) keep shared memory, that
recall_entitiesandrecall_observationssearch it, and to search it before saying it does not know something about one of them. - daemon:
get_entitylooks in the shared memory of the user’s groups when the user’s own memory has no such entity, and says which group the record belongs to. Scoped entries and observations show only to members of the scope.
Mailboxes are connected only in the app (#634)
- admin: the Email Integration page and its Gmail and Outlook sign-in callback pages are gone; Settings → Email in the app replaces them.
- contract: the deprecated
GET /api/v1/user/email-accounts/oauth/{provider}/startandPOST …/oauth/{provider}/callbackroutes are removed (only the admin console used them), and so issync_enabledonPUT /api/v1/user/email-accounts/{id}(uselearn_from). UsePOST …/oauth/{provider}/connect. - deploy:
GOOGLE_OAUTH_REDIRECT_URI,MICROSOFT_OAUTH_REDIRECT_URIandADMIN_ORIGINare no longer read; the admin callback URLs can be removed from the Google and Microsoft apps.
Fixed: a household agent couldn’t merge two entries in shared memory (#633)
- daemon: a household or team agent can now merge two entries in its shared memory that are the same person or thing, e.g. “Stefan” and “Stefan Crafoord-Wiklund”. The surviving entry keeps everything recorded about both, keeps the other name as an alias, and stays linked to the member’s account. The agent won’t merge entries linked to different accounts.
Gmail with an app password, read-only (#627)
- flutter: Settings → Email has a row for Gmail: its form has the server filled in, with steps and a button for making an app password. Sign-in with Google waits until Google has verified the app (#628).
- flutter: iCloud gets a row and a form of its own too. Fastmail, one.com and the rest are under Other mailbox.
- daemon: the agent’s email search over IMAP now looks in the whole mailbox where the server has an All Mail folder, so archived Gmail is found. Other servers still search the inbox.
- daemon: the agent can open the form for Gmail from chat.
Fixed: the shared chat missing after joining or switching account
- flutter: the household or team’s shared chat now shows up in the sidebar right after accepting an invite or signing in to another account. Before, the app kept the thread list from when it started, so it only appeared after Celindor was quit and opened again.
Cards under the agent’s replies in the shared chat (#614)
- daemon: in a household or team’s shared chat the agent can put a card under its reply, as it does in your own chats: a person or thing from the household’s shared memory, or a date that matters. Everyone in the chat sees it, so a card only ever points at the shared memory, never at what one member keeps for themselves; documents, recordings, to-dos and settings are not offered there.
- API:
cardsonThreadMessageand thethread-messageevent, andGET /api/v1/user/threads/{threadId}/entities/{entityId}for what an entity card shows (additive). - flutter: the shared chat shows the cards under the agent’s replies.
Notifications in the web app and on Android (#616, #617)
- flutter: the web app asks, once you are signed in, whether it may send notifications, and then shows Celindor’s notifications like the iPhone and the Mac. Clicking one opens what it is about in the open Celindor tab, or in a new one.
- flutter: the Android app asks for notification permission once you are signed in and gets Celindor’s notifications, under “Messages” in the system’s notification settings. Tapping one opens what it is about.
- daemon: sends web and Android notifications; before, the parts that deliver them were never installed.
A household agent no longer sees members’ personal memories
- daemon: a household or team agent’s memory search also returned personal memories of the member who asked, though it is meant to see only the shared memory. In the shared chat everyone reads its replies, so it could repeat one member’s personal memories to the others. It now only ever finds the shared memory. The personal agent’s “shared memory only” search had the same fault and is fixed with it.
Long shared chats are summarised, not forgotten (#615)
- daemon: when a household or team’s shared chat grows long, the agent folds its oldest messages into a summary, as it does in your own chats, instead of losing sight of everything before its last 200 messages. The summary keeps who said, asked and decided what, by name, and it is the household’s: every member’s next question to the agent starts from the same one. It is written on the household’s key, never a member’s (migration 0066). Everyone still sees every message in the chat.
The shared chat’s agent remembers what it says there too (#613)
- daemon: in a household or team’s shared chat, the agent’s replies now go into the shared memory as well, not only what the members write. They count as written by the member who asked, so a viewer’s question adds nothing.
Notifications from the shared chat, and an agent that can answer everything (#608)
- daemon: a message in a household or team’s shared chat, a member’s or the agent’s, notifies everyone else in it who has not muted it, unless they have Celindor open or are in their quiet hours. The notification names the author.
- flutter: on the iPhone and the Mac, tapping a notification opens what it is about: the shared chat it came from, the chat an agent wrote in, or the review queue. Before, it only opened Celindor.
- daemon: “Answer every message”: with it on, the agent answers every message in the shared chat, not only the ones that ask it. Off by default; the owner and admins turn it on (migration 0065).
- API:
answer_allonHouseholdThread, andPUT /api/v1/user/threads/{id}/settingsfor the owner and admins (additive). - flutter: owners and admins get the switch in the shared chat’s bar.
One chat the whole household or team shares (#606)
- flutter: each household or team agent’s shared chat is in the sidebar as “Everyone with {agent}”, above your own chats with the agent, with what is unread there. Everyone in it sees every message, under its author’s name, as it is written; a deleted account shows as “Former member”.
- flutter: a message is for everyone. Start it with the agent’s name, or tap “Ask {agent}”, and the agent answers there for everyone, its reply appearing as it writes; it learns from what everyone says. The reply can be stopped, and the chat muted.
Skills in the SKILL.md v2 format, and four that come with Celindor
- daemon: skills take the SKILL.md v2 format: frontmatter with a
category, tags, related skills, required tools and config, a body in four
sections (when to use, procedure, pitfalls, verification), and optional
reference files that
skill_viewloads one at a time. Four skills now ship with Celindor and are on for everyone until they turn one off: meeting prep, day prep (“prep me for tomorrow”), trip planning and inbox triage. They are synced fromdaemon/skills/at startup, without overwriting a skill an admin edited or bringing back one an admin deleted. The prompt’s skill list is grouped by category and shortens to names when it gets long. Each skill records who owns and wrote it, its state, and how often the agent loads it (migration 0065). Skill descriptions are one sentence of at most 60 characters. - admin: the Skills page lists the catalog with category, source (builtin or admin), state, uses and when each was last used.
Stopping a reply before it starts now says “Stopped”
- daemon, flutter: a reply stopped before the agent wrote anything or used a tool left the message with no answer, as if it had vanished. It now shows “Stopped”, also after a reload.
Manuals for users and administrators, for every release
- manuals: a user manual and a quick-start cheat sheet in English and Swedish, and an administrator manual in English: part 1 for whoever runs a household or team, part 2 for operators. Each release ships its own, built as Markdown and PDF, and earlier releases’ stay readable.
- flutter: Settings › Manuals shows the manuals of the running version in the app’s language, every language, and earlier versions. On the web a manual also opens as a PDF or downloads as Markdown.
- admin: a Manuals page lists every version’s manuals as PDF and Markdown.
- API:
GET /api/v1/user/manuals,…/{version}/{kind}/{language}and…/download?format=pdf|md(additive). The administrator manual is listed and served only to operators and to owners and admins of an organization.
Connecting Google Drive looks like connecting mail and calendars
- flutter: in Settings → Google Drive, connecting and reconnecting is now a row in a list, as for mail and calendars, instead of a button across the whole screen.
Team members add to the team’s memory; viewers only read it
- daemon: a member of a team now reads and writes its shared memory, as in a household; before, team members could only read it, so a colleague could add nothing. The new viewer role reads the shared memory and adds nothing, in teams and households alike. Admins and division grants work as before. Existing team members become writers (migration 0064).
- API:
vieweris a new value ofOrganizationRoleand of the member, invite and household-organizationrolefields, and is accepted when inviting, adding or changing a role (additive; the response enums are nowx-extensible-enum). - flutter: the invite dialog offers admin, member or viewer, with a line on what each may do; the owner changes anyone’s role to either of the other two from the member’s menu.
Invitations reach the people invited
- flutter: the link in an invitation mail now opens the installed app on iPhone, Mac and Android, which accepts it at once. Before, it opened the browser, which asked for a sign-in whose own link then opened the app, so the invitation was never accepted.
- flutter: a pending invitation shows at the top of the sidebar (“Invitation to …”); tapping it opens Settings › Household, where you accept or decline.
flutter: the profile shows which account you are signed in with (#598)
- Settings → Profile starts with “Signed in as” / “Inloggad som” and your email address.
- Holding your name at the bottom of the sidebar shows the email too (hover with a mouse).
A shared thread for each household agent, groundwork (#592)
- daemon: every household agent now has one thread that the household owns, which everyone in the household can read and write in, with each message carrying its author. For an agent bound to a space or division, only the members with access there are in it, and leaving or being removed takes a member out at once. When an author deletes their account their messages stay, shown as from a former member. The thread is in no member’s chat list, search or export.
- daemon: new routes list the threads, page through a thread’s messages, write in one, and set each member’s read position and mute. The agent does not answer in the thread yet, and the app does not show it yet.
What is said in a household chat stays with the household (#587)
- daemon (security): memory from a household chat was matched against the
member’s own personal memory, so a household fact could be hung on one of
their personal entities,
self:meincluded. A stub for a relationship’s end went into their personal memory. It now resolves in the household’s memory: “me” is the member’s person there, and nothing of their personal memory goes into the request. Promotion also refuses a subject or a relationship end outside the memory the fact lands in. Migration 0062 clears such links already made: the fact keeps its text, and the relationship goes. - daemon: that extraction runs on the household’s key, as its chat does, not on the member’s own.
- daemon: a household chat’s turns are recorded for the household
(
turn_usage.organization_id, key sourceorganization). A turn paid with the household’s key no longer counts against the member’s monthly quota. - daemon: purging a deleted household after its 30 days takes its agents’ chats with it. Before, they became plain personal chats that still held the household’s conversation. The app’s delete confirmation says so.
The household agent answers in its shared thread (#596)
- daemon: the agent answers in the thread when a message starts with its name or names it as @Name, or when the app says the message is to it. Everything else is members talking to each other, which the agent reads on its next turn. One turn runs at a time; messages to the agent during a turn get one more turn, which answers all of them. The agent sees who wrote each message, runs on the household’s key and memory, and records what it learns with the rights of the member who asked. Every member’s message is learned from under its author’s rights, addressed or not.
- daemon: every member in the thread sees each new message as it lands and follows the agent’s reply as it is written. The member who addressed the agent, and the household’s owner and admins, can stop the reply.
- API:
address_agentonPOST /user/threads/{id}/messages,turn_idon its response, thethread-messageevent on the user stream, andGET /user/turns/{id}/streamfor any member of the thread (additive).
Connect iCloud mail and calendar with one app password (#582)
Celindor asks with choices you tap (#125)
- daemon: a new
clarifytool lets the agent ask one to five short questions, each with two to four choices (and the app’s own “Other”; some take several picks), and wait for the answer. Its reply goes on once you answer or skip, after 5 minutes, when you stop it, or at once if no app of yours is open, and the agent is told which, so it says what it assumed. Whichever of your devices answers first counts; the others close the question. Not offered to background work: missions, a task another agent handed over, or a household agent’s chat. - API:
POST /api/v1/user/clarify/{requestId}and theclarify-requestedandclarify-resolvedevents on the user stream (additive). - flutter: the questions show over the composer as chips, with Send and Skip; “Other” opens a field for your own words. The chat no longer counts a reply as stuck while it waits for your answer.
Security: memories that read like instructions stay out of the agent’s tools too (#409)
- daemon: when the agent looks something up in memory itself (a person, facts, episodes, household memory, measurements), any stored line that reads like an instruction to an assistant is replaced by a note that it is waiting in the Memory tab, as the prompt has done since #110. Before, such a line came back as written, so a memory saved before #110 could still steer the agent. The Memory tab shows the raw text as before.
- cli:
celindor scan-memory-instructions [--user-id <id>] [--archive]lists stored people, things and facts that read like instructions, such as ones saved before the checks existed, and with--archivearchives the personal ones.
Connect your iCloud calendar along with iCloud mail (#582)
- flutter: after an iCloud mailbox connects, Celindor offers to connect that Apple ID’s iCloud calendar as well, with the same app password, so no second one is needed. After an iCloud calendar connects, it offers the mailbox the same way, when the Apple ID is an iCloud Mail address. Nothing happens unless you say yes, and there is no offer when the other one is already connected.
- flutter: an iCloud mailbox or calendar connected earlier shows a row, “With a password you already gave”, on the Calendar or Email screen, which connects the other one in a tap.
- daemon: new
POST /api/v1/user/calendar/accounts/icloud-from-mailandPOST /api/v1/user/email-accounts/icloud-from-calendarconnect one from the other. The password is reused on the server and never goes back to the app. Each account keeps its own copy, so either one can be disconnected alone. A mailbox made this way signs in to iCloud Mail first, like any other: the agent may read it, and learning from it is off. Another user’s account answers 404.
Pictures from tools reach every AI provider (#574)
- daemon: a picture a tool returns, such as one in your mail (#573), now
reaches the model on every provider. Before, it reached only Claude through
the Agent SDK.
- Claude over the Messages API gets it inside the tool’s result.
- OpenAI-compatible providers get it in a message right after the results, named with the tool it came from.
- Gemini gets it after the function’s response.
- daemon: a model that takes no pictures no longer fails the turn. The provider’s refusal is recognised, the pictures become a note saying they were left out, the request is sent again, and later pictures in the turn are left out too. The same happens for a picture that is too large.
Security: sharing and suggestions check every id (#407)
- daemon: sharing a recording checks that it is yours; someone else’s answers 404. Before, only the people it went to were checked, so another user’s recording could be shared to your own sub-account and read there.
- daemon: sharing a document into a household or team requires that you are in it and may write to its memory, and that the scope is its own; an organization you are not in answers 404.
- daemon: approving or promoting a suggestion with a chosen subject takes only one of your own people or things (or, for a household’s memory, one of the household’s); another user’s answers 404.
- daemon: the database now refuses a recording share by someone who does not own the recording, and a document share into an organization the owner is not in or a scope of another organization. Shares like that already stored are removed. Migration 0061.
- daemon: sharing a recording, and sharing a document into an organization, work again. Both failed with an internal error for everyone: the database client refused how they read back the new rows, and the document upsert named no index it could use.
- The organization scope routes already checked that the scope is the organization’s (#562); a test now pins it.
Private can be switched off again (#431)
- flutter: the “Memory on” and “Private” chips in the composer each switch off when tapped again. Before, only the chip that was off reacted, so Private could be turned on but not back off.
Teams and their divisions in the app (#581)
- app: a new organization is a household or a team. A team says so in the list and calls its memory areas divisions; a household calls them spaces.
- app: the organization’s owner and admins create and delete spaces and divisions and choose who is in each, and how much: read, add or manage. Deleting one moves what it remembered into the organization’s shared memory, and the confirmation says so.
- daemon: the space and division routes are on the API contract, so the app and the admin console get typed clients. Their paths and behaviour are unchanged.
Household agents can be retired; their chats remember for the household (#577)
- daemon: a household agent can be retired and brought back, like a personal agent: it leaves the members’ lists and takes no new chats, while its chats go on.
- daemon: deleting a household agent keeps its chats readable, marked as such. Before, they turned into plain personal chats that kept the household’s history and still wrote to its memory. Migration 0061.
- daemon: a household agent’s chat remembers where its agent does; its memory can no longer be pointed elsewhere (409). The prompt in such a chat no longer calls memory “strictly private”.
- daemon: a new household or team starts with one agent named after it.
- app: a household chat shows “Remembered for Home” in place of the memory picker; admins retire and bring back household agents by swiping; a chat with a deleted agent says so in place of the message box.
People you remember, connected to people in your household (#572)
- daemon: joining a household puts you in its memory as a person linked to you, so its agent knows its members. Existing members are added by migration 0060.
- daemon: a person in your own memory who looks like someone in your household (the same email address, or the same name) is suggested as them. Nothing is linked without a yes, and a no is never asked again. Only people you share a household with are ever matched.
- daemon: when the agent ties a person you remember to an email address that is a household member’s sign-in address, they are linked at once.
- app: Household asks “Is Anna in your memory Anna Berg from Home?” with yes and no. A link lets Celindor recognise the person; it shows them nothing of what you have told it about them.
Celindor can look back through your earlier chats (#120)
- daemon: two new agent tools.
search_conversationsfinds what was said in earlier chats, by meaning and by keyword, with the best match shown in context; without a query it lists recent chats.read_conversationreads one chat around a message, or its beginning and end. So “what did we say about the trip?” gets an answer instead of a request to repeat it. Incognito chats are never searched and have no such tools; a household agent’s chats stay out of personal search; a chat in a memory space only sees the chats it may read by that space’s rules (#506). Tool results are left out. Migration 0059. - daemon: the unused admin
searchMessages(a bareILIKE) is gone.
A deleted household can be restored for 30 days; people are told (#570)
- daemon: deleting a household no longer removes it at once. It is gone for everyone straight away (lists, agents, recall, access), and its owner can restore it with its members, memory and agents for 30 days; after that a worker purges it. What was about to be added to it is dismissed when it is deleted. Migration 0058.
- daemon: people get notices: someone removed from a household is told who removed them, and members are told when a household is deleted or restored.
- app: Household lists those notices, each dismissable, and a “Recently deleted” section where the owner restores a household until its 30 days are up. The delete confirmation says it can be restored.
Celindor sees the pictures in your mail (#573)
- daemon:
read_email_attachmentshows a picture (JPEG, PNG, GIF, WebP; attached or in the body) to the model as a picture, with any writing in it beside it. Before, a picture only gave its OCR text, so a photo or a chart came back as “no text was found”. At most five pictures a turn. A picture over 5 MB or 8000 pixels a side, and BMP, TIFF and HEIC, keep the text-only path. - daemon: tracking pixels, logos and signature icons are no longer listed among an email’s attachments.
- daemon: Gmail now keeps the Content-ID of a picture in the body, so it can be told from one that was attached.
- Pictures reach the model in chats that run on Claude through the Agent SDK. Other providers get the writing and a note until #574.
Leaving a household, and an owner’s account going (#567)
- daemon: someone who leaves a household, or is removed from it, no longer has their pending suggestions added to its memory afterwards; they are dismissed. Deleting a household dismisses what was bound for it too. Before, those suggestions went into the person’s personal memory instead. Migration 0057.
- daemon: when an owner’s account is deleted, each household they own passes to its longest-standing admin, else its longest-standing member. Before, the households were deleted with the account.
- app: a chat with the agent of a household you’ve left stays to read; in place of the composer it says the chat can’t go on. The daemon refuses a message to it with 409.
Household members add to its memory; households and teams (#564)
- daemon: every member of a household now writes its shared memory, so what a member tells a household agent is remembered for the household. Before, only the owner, the admins and members with a write grant could, and a plain member’s household chats recorded nothing.
- daemon: an organization is a household or a team (
kind). In a team, members read the shared memory and write where a scope (a division) grants it. A grant now counts only for its own scope: a write grant on one scope no longer gave write access to the organization’s whole shared memory. Migration 0056. - daemon: writing to an organization’s memory over the API checks the scope written to, and that the scope is the organization’s.
- daemon: at most 20 members per organization, pending invites counted, and at most 10 organizations owned per user.
- app: the owner makes a member an admin, or an admin a member, from the member’s row; admins can rename the household. The owner is listed among the members as owner, and nobody gets actions on the owner’s row. An admin no longer sees Remove on another admin.
Household boundaries hold (#562)
- daemon: sharing a memory with a household only takes the caller’s own memory. Before, the share routes looked the item up by id alone, so anyone who had the id of another user’s entity or observation could copy it into their household and delete it from its owner’s memory.
- daemon: someone removed from a household, or who leaves it, loses their access to its spaces (scopes) as well. Before, their scope grants stayed and still counted, so a removed member could keep writing to household memory. Migration 0055 also deletes grants already left behind.
- daemon: only the owner and admins create, change or delete spaces, and a space’s members are managed by them or by the space’s own admins. Space routes answer 404 for a space that belongs to another household, and a grant can only be made for someone in the household.
- daemon: an admin can no longer remove the owner or another admin; only the owner removes an admin. Handing over ownership no longer fails when the owner holds an admin row.
What Celindor decided on its own can be taken back (#560)
- app: each decision in a dream now names the suggestion it was about, and a saved, let-go or merged suggestion has an Undo. Undo puts the suggestion back in Suggestions for memory for you to decide; Celindor doesn’t decide it again. A suggestion that was saved has what the save created archived, unless it was already in memory before. A person or thing that other memories have since been saved about is refused until those are taken back; a merge of a person or thing can’t be undone.
- daemon:
POST /api/v1/user/dreams/{id}/decisions/{extraction_id}/undo, andtitle,statusandundoableon each dream decision (additive).
Suggestions for memory say what Celindor decided on its own (#559)
- app: when Celindor has saved or let go of suggestions since you last opened Suggestions for memory, a note at the top says how many, with a link to those decisions in the dream journal. Before, the list just changed between visits, which looked as if the suggestions had been swapped for others.
The agent reads the files attached to an email (#553)
- daemon: a new
read_email_attachmenttool gives the agent the text of a file attached to an email: PDF (tickets, invoices, contracts), Word, Excel, text and HTML, and the writing in an image (OCR). Before, the agent only saw the file names, so a seat or an amount that existed only in the attached PDF was out of reach.read_emailnow numbers the attachments, and in a conversation each message that has attachments shows its own ref. Works for IMAP, Gmail and Outlook. Files over 20 MB are not read.
The keyboard can be put away in a chat (#554)
- flutter: on a phone or tablet, tapping the conversation (or anywhere outside the composer) hides the keyboard. Before, nothing did, because Return adds a new line. Tapping the composer’s own buttons, such as send, attach or the microphone, keeps it open.
A suggestion that may be a duplicate can be merged from the app (#555)
- app: a suggestion for memory that may duplicate something already there now offers “Same as “…””, which merges it into the existing entry, and Save reads “Save as new”. When the existing entry is you, the card says “This may be you.” and the button reads “This is me”. Before, the only choices were Save, Remove and Later.
Help with app passwords when adding a mailbox (#550)
- flutter: Settings → Email → Add a mailbox explains, for iCloud and Fastmail, how to make an app password, and links to the page where it’s done. For one.com it says the mailbox’s own password is the one to use.
- flutter: a Paste button fills the password from the clipboard when you come back from that page.
- flutter: for iCloud, a password that isn’t shaped like an Apple app
password (16 letters, e.g.
abcd-efgh-ijkl-mnop) is flagged before Add is tapped. The server still has the final say (#548).
A wrong mailbox password is caught when the mailbox is added (#548)
- daemon: adding an IMAP mailbox signs in to the server once first. A rejected username or password answers 422 and a server that can’t be reached answers 502, and nothing is saved either way. Before, a wrong password was saved and the mailbox looked connected while every search failed. A space or newline pasted along with the password is trimmed.
- daemon: IMAP errors carry the server’s own reason (“Authentication Failed”, …) to the agent and the logs instead of a bare “Command failed”. When the server turns down the login of a connected mailbox, the mailbox is marked as sign-in failed, so Settings → Email shows it.
- flutter: the IMAP form says when the server didn’t accept the username or password (and that most services want an app password there), or when it couldn’t be reached.
- contract:
POST /api/v1/user/email-accounts/imapdocuments the new 422 and 502 answers (additive).
IMAP mailboxes recover after a connection drops (#543)
- daemon: after one failed or dropped IMAP connection, every later email search and sync for that mailbox failed with “Can not re-use ImapFlow instance” until the daemon restarted. Each reconnect now uses a fresh connection, and the sync worker and the agent’s email tools share one connect instead of racing each other.
Archive chats, retire agents, and delete both from the sidebar (#469)
- flutter: on an iPad, a Mac or the web, a long press or a right-click on a chat in the sidebar’s Recent list opens its menu: rename, its agent, its memory space, archive, delete. The same on a personal agent offers a new chat with it, edit, retire and delete. Before, rename and delete were only in the full chat list and the agents sheet.
- flutter: an archived chat leaves the chat list but keeps its messages and memory. The chat list’s Archived button lists them; tapping one brings it back and opens it. A retired agent leaves the sidebar and takes no new chats, while the chats it has go on; the agents sheet lists it under Retired, to bring back or delete.
- flutter: swiping a chat in the chat list or the sidebar now shows Archive and Delete buttons instead of deleting at once, and swiping an agent in the agents sheet or the sidebar shows Retire and Delete; on a Mac, drag with the mouse or swipe with two fingers. In the archived chats and under Retired, the swipe offers Unarchive or Bring back, and Delete. The buttons look like Mail’s.
- daemon:
POST /api/v1/user/chats/{chatId}/archiveand/unarchive,POST /api/v1/user/agents/{id}/retireand/unretire. The chat and agent lists leave archived chats and retired agents out unlessarchived=true/retired=trueasks for them, and carryarchivedAt/retiredAt. A message the user sends in an archived chat brings it back; a retired agent cannot start a chat (400), be delegated to or be given a mission. Migration 0054. Additive: older apps simply do not see archived chats or retired agents.
A new agent introduces itself (#537)
- daemon: the first chat with a new agent opens with the agent saying hello and what it thinks it was brought to life for, going by its persona, or asking what you have in mind when the persona doesn’t say. It streams in as any reply does, and not if you write first.
- app: an empty chat with an agent shows the agent’s picture and name instead of your home greeting and starter prompts.
A Settings button is offered once per chat (#536)
- daemon: the agent no longer adds the same Settings button to every reply. Once it has shown a screen in a chat, it points back to that button instead of adding it again, and it calls the button “below” the reply, which is where the app draws it.
A mailbox connected mid-chat can be read at once (#535)
- daemon: connecting a mailbox the agent may read, removing one, or switching “Agent can read” ends the user’s open chat sessions, so the next message rebuilds them with (or without) the email tools. Before, a chat that was open when the mailbox was added kept saying it had no way to read mail.
A reply no longer waits for the message to be embedded (#530)
- daemon: a chat message is saved at once and embedded right after, instead of the turn waiting for the embedding service first (up to 10 s when it was slow). Ambient recall reuses that embedding instead of making its own. A message whose embedding failed is now filled in by the backfill sweep, so it becomes searchable once the service is back; before, it never did.
Chat replies no longer stall on a cold embedding model (#529)
- deploy: the shared embedding sidecar gets 4 GB instead of 2 GB. Under the old cap the model was paged out while idle, and the first turn after a quiet spell waited up to ~12 s for it (and went out without memory context).
Agents hand off longer work without waiting (#511)
- daemon:
delegatetakeswait: false: the task becomes a suggested mission for the other agent, which you start with a tap, and the agent that handed it off goes on at once. Not from a mission’s own chat or from a task another agent delegated. When a mission started from a chat finishes or fails, its result is posted there as a message from the agent (that message is the push), so the agent in that chat can pick it up;mission_statussays how one is going in the meantime. - flutter (#526): a task handed off that way shows in the chat as its mission card, with Start, Change budget and Dismiss, instead of as a delegation that already answered.
Celindor fills in your email, calendar and AI key setup; you add the secret (#520)
- daemon: a Settings card can now carry the form’s details (
prefill): the mail server, port and address, the iCloud or CalDAV login, a calendar link, or which AI key row to open. Celindor works them out from what you say or a file you give it. Passwords, app passwords, tokens and keys are never accepted in a card. The tool refuses any field outside the screen’s list, and any value that looks like a secret. Celindor never asks for one, and if you paste one in chat it tells you to change it. - flutter: tapping the card opens the IMAP, iCloud/CalDAV or calendar-link form filled in, or the AI screen with that row open. You type the password or key and connect; it goes straight to the server, never through the chat. The card says which account it set up.
- contract:
ChatCardgains an optionalprefill(additive).
daemon, flutter: To-dos and deadlines the agent manages from chat (#17)
- The agent can add, change, tick off, remove, list and find the user’s
to-dos with new tools (
add_todo,update_todo,complete_todo,delete_todo,list_todos,find_todos). A due date is a whole day or a time, read in the user’s zone. An incognito chat can only list and find. schedule_remindertakes atodo_id: the reminder is tied to the to-do and cancelled when the to-do is done or removed.- A
deadlinecard can carry the to-do behind it (workItemId), so the app can tick it off from the chat. - A recording’s action items become proposed to-dos, waiting for the user to accept or dismiss them. A dismissed or accepted one is not proposed again.
- New routes under
/api/v1/user/work-items: list, add, get, change, remove, set a reminder, snooze, and/duefor Today (overdue, today and soon). - Migration
0053puts the oldwork_itemstable to use: it drops the RLS-era helpers and the kanban view, fixes the status to proposed/todo/done, and adds all-day due dates, the source recording, the linked reminder and Today’s snooze. To-dos are plaintext at rest, like other content (daemon/SECURITY.md).
Missions: your agents work on longer jobs in the background (#509)
- daemon: a mission is a job one of your agents does without you
watching, in its own chat, a step at a time (at most 10 minutes each), and
you see every step there. An agent can only suggest one (
start_mission); it runs once you start it. Low-risk tools run; anything else pauses the mission and asks you, and your answer lets it go on (once, or from now on). A mission stops to ask when its budget (USD 1.00 by default, settable), its step limit (30) or its deadline (7 days) runs out, and when your monthly limit is reached. It ends when the agent hands over the result, gives up or you cancel it. You get a push when one waits for you, finishes or fails. - flutter (#510): a mission an agent suggests shows as a card in the chat, with Start, Change budget and Dismiss; once started it follows the mission live. A mission’s own chat has a bar on top with its status, steps, plan and spending, Pause, Stop and Raise budget, and the question or permission it waits on, answered right there; its step messages are marked as such. A Missions space in the sidebar (with a count of running and waiting missions) lists them by agent and starts new ones. Today shows missions waiting for you and those that finished since you last looked. Settings → Missions sets the default budget.
- contract: the Dart client generator now makes a field nullable when
the schema it points to is (
Mission.pending, and four older fields with the same problem:DataImport.preview/summaryand two recording summary fields). - daemon: API under
/api/v1/user/missions(list, start, get, pause, resume, cancel, change limits, answer) and/api/v1/user/mission-settings, and amission-updatedevent on the user stream. Migration 0052; additive contract change.
An agent’s memory: shares yours, knows you, or starts from scratch (#506)
- flutter: the agent editor’s “Keeps its own memory” switch becomes three choices: Shares your memory, Knows you, remembers on its own (it reads your memory, what it learns stays with it) and Starts from scratch (only what was said in its own chats). Agents that kept their own memory before are now Starts from scratch, as their switch promised. Memory spaces get a fourth level, Private.
- daemon: memory spaces keep to their isolation level in both directions.
Before, a hard space still read the rest of your memory, and Celindor’s own
chats read every hard and what-if space. Now only soft spaces and memory in
no space are seen outside their space, and a hard space reads nothing else.
New level
private: reads the rest of memory, nothing of it leaves. The memory tools that go by name or list (get_entity,list_entities,recall_episodes,forget_entity,merge_entities,relate,unrelate,remember) follow the same rules, so a chat can neither see nor change memory it may not read. Migration 0050 changes the recall and episode search functions; additive contract change.
Learning from a mailbox misses nothing and skips the noise (#504)
- daemon: the learning sync no longer loses mail when more than one batch arrives between syncs: Gmail and Outlook take the oldest new mail first and only move past what was fetched, and IMAP now really counts in UIDs and starts over if the server renumbers a folder (UIDVALIDITY).
- daemon: the first sync, or turning learning on again, reaches back 30 days, a batch at a time.
- daemon: newsletters and mailing lists, automatic mail, no-reply senders, Gmail promotions and Outlook’s “Other” folder, and very large messages are not learned from. The agent can still search them.
- daemon: HTML mail is read as text with paragraphs, lists and link addresses kept.
Settings → Email in the app (#503)
- flutter: a new Settings → Email screen. Connect Outlook with its sign-in, or any other mailbox (iCloud, Fastmail, one.com or your own IMAP server) with an app password. For each mailbox, choose whether Celindor may read it and whether it learns from it, sync it now, see when access stopped working, and disconnect it, keeping or forgetting what Celindor learned. Gmail is not offered yet.
Celindor can search and read your mail when you ask (#502)
- daemon: the agent gets
search_email,read_email(one message or the whole conversation) andlist_email_accounts, searching Outlook, IMAP and Gmail mailboxes live. Only mailboxes with “Agent can read” on are seen, and turning it off holds at once. Mail comes back marked as untrusted data, long mail is cut at 20 000 characters, and nothing is copied into memory. - daemon: an IMAP connection the server dropped is reopened instead of failing until restart, and an IMAP error no longer risks taking the daemon down.
Mailboxes get two consent switches, and Outlook connects without admin (#501)
- daemon: each connected mailbox now has Agent can read (on by default) and Learn from this mailbox (off by default). Only mailboxes that learn are synced into documents and memory; mailboxes that synced before keep doing so (migration 0051). Disconnecting can keep or forget the documents made from a mailbox’s mail.
- daemon: Outlook (and Gmail, where offered) can be connected from any
client:
POST /api/v1/user/email-accounts/oauth/{provider}/connectreturns the sign-in page, and the provider sends the browser back to the daemon, not to the admin console. Access stays read-only. - contract: the email-account routes are on the contract (additive;
agent_access,learn_from,providers,?documents=keep|forget). The admin console’s older start/callback routes are kept and marked deprecated. - daemon: connecting Outlook or Gmail always shows the account picker, so a second account can be added while another is signed in to the browser.
You can stop a task one agent handed to another (#508)
- daemon: a task handed to another agent can be stopped and steered from that agent’s chat, like any reply. It counts against the monthly limit, a task that goes quiet for two minutes is stopped, and what it wrote before it was stopped or ran out of time now stays in the chat. The turn runs through a shared runner that missions (#327) will use too.
You can see the agent’s plan for longer work (#126)
- daemon: a new
update_plantool lets the agent keep a checklist for work that takes several steps. The plan is stored with the chat (migration0049_chat_plans), sent to the app as it changes (plan-updatedon the user stream,GET /api/v1/user/chats/{chatId}/planwhen a chat opens), and shown to the agent again in each later turn while steps are still open, so it carries on after a compaction. Missions (#327) will build on it. - flutter: a chat with a plan shows it over the messages, “Plan · 2 of 5 done”, which opens to the steps.
Celindor helps you set things up in Settings (#505)
- daemon: Celindor knows the app’s Settings screens and what each one
sets up. Asked how to add a key, connect a calendar or quiet notifications,
it explains and shows a card that opens the right screen (
show_cardkind=settings). It never asks for keys or passwords in chat. - flutter: tapping that card opens the screen in the Settings tab, with Back leading to Settings. A screen this app does not know opens Settings itself.
- contract:
ChatCardgains kindsettingsand ascreenfield (additive; older apps skip the card).
Celindor knows your agents and passes tasks on (#473)
- daemon: every chat that can delegate (Celindor and each personal agent, not incognito or household chats) is told the user’s other agents, by name and the first sentence of their persona, so it hands a task to the right one instead of guessing names.
- flutter: a task handed to another agent shows in the chat as a card, “Asking Coach…” then “Asked Coach”, with what was asked and a button that opens that agent’s chat.
Tapping an agent opens your chat with it (#472)
- flutter: an agent in the sidebar opens its chat instead of starting a new, empty one each time. With several chats it unfolds them below it, newest first, with New chat on top. In a chat with an agent, the bar’s new chat button starts another with the same agent, and an empty agent chat shows the agent’s face and name.
- daemon: starting a chat with an agent hands back its empty chat when it has one, with the agent’s name and avatar. Migration 0048 deletes the empty agent chats the old sidebar left behind.
Dialogs look like iOS (#459)
- flutter: on iPhone, iPad and Mac, alerts and confirmations are iOS 26 style: a frosted glass card with large corners and capsule buttons, the main one filled. Editors with several fields (agents, spaces, invites, instructions, transcript segments, memory proposals) open on iPhone as a page sheet with Cancel and Save in a bar on top. Bottom sheets float on glass. Android and the web are unchanged.
Claude turns finish what they start (#488, #490)
- daemon: the Claude, OpenAI and Gemini chats now share one tool loop, so Claude gets the guardrails the others had. A turn that runs out of steps, or keeps repeating or failing the same calls, ends with a summary of what it found instead of stopping without a word; a reply cut off by the output limit is continued, and a cut-off tool call is asked for again instead of failing the turn. OpenAI and Gemini now check a call’s arguments against the tool’s own rules before it runs, as Claude did.
A tighter sidebar (#471)
- flutter: the sidebar’s rows (Spaces, recent chats, agents) are 36 pt instead of 44, and its sections sit closer together, so more fits without scrolling. The collapsed icon rail keeps its full-size targets.
The agent no longer says it runs over Telegram (#468)
- daemon: accounts created while Telegram was still a channel kept “personal AI agent over Telegram” as the agent’s description, and a memory rule about Telegram user ids, in what the agent is told. Migration 0047 replaces both with today’s defaults, leaving any wording a user wrote themselves alone.
Memory’s type switch fits on a phone (#467)
- flutter: the Entities / Relationships / Observations switch in Memory keeps each label, count included, on one line and shrinks it to fit instead of wrapping into an oversized pill. The same goes for every other labelled switch in the app.
Fold away sidebar sections (#433)
- flutter: the sidebar’s Spaces, Recent and Agents headings fold their section away with a tap, and open it again the same way, so a long list of chats or agents no longer crowds the menu. Each section remembers whether it was folded. English and Swedish.
A chat’s field on a phone, the iOS way (#458)
- flutter: on a phone a chat’s field is a slim capsule on a hairline with a small send (or voice) button inside it and (+) to its left, as in iOS Messages. The scope chip and the microphone moved to the row above, beside the place chip. Wider screens keep their pill, now drawn the same way.
Paste a screenshot into an issue report (#460)
- flutter: “Report an issue” has a “Paste a screenshot” button next to “Add a screenshot”, so a screenshot just taken on a phone goes straight in. On a keyboard, Ctrl/Cmd+V anywhere in the form does the same when the clipboard holds an image; text still pastes into the field. English and Swedish.
Fixed
- daemon: a reply made only of tool calls and cards, with no text, is now saved. It used to be dropped, so it showed while it streamed and was gone when the chat was opened again. This holds for every provider (#462).
- flutter: such a reply shows its tool lines and cards and no empty reply under them (#462).
- flutter: the text fields in “Report an issue” no longer have their rounded corners cut off by the card around them (#470).
Recent chats back in the menu (#463)
- flutter: with a few agents, the Agents section pushed Recent chats out of the phone menu. Recent now comes first and the agents follow below it, in one list that scrolls.
Agent images (#299, #455)
- flutter: an agent can be given one of eight images drawn for Celindor (spark, leaf, wave, flame, book, compass, heart, moon): the agent dialog shows them above the image address field, and a tap picks one. A household agent carries a small house badge wherever its face shows. A chat with no agent shows Celindor’s own face beside its title. English and Swedish.
- daemon: an agent’s
avatarUrlmay becelindor:agent/<name>for one of the bundled images, besides an https address.
Persona delegation (#337)
- daemon: an agent can hand a task to one of your other personal agents
with a
delegatetool and get the answer back as a tool result. The task runs as a real turn in that agent’s own chat, with its persona, memory and tools, so you see it there and can continue it; the row is marked as a task from the delegating agent. Bounds: a delegated turn cannot delegate further, at most two delegations run at once per chat, two minutes per delegation, and the reply handed back is capped at 4,000 characters (the whole of it stays in the chat). Not in incognito or household chats. - flutter: a task another agent handed to a chat is headed “Task from …” above its bubble. English and Swedish.
Dream worker: organization memory like personal memory (#346)
- daemon: what a household or organization chat says is now dreamed the way personal memory is. Each organization scope is reviewed against what that scope already remembers, so a duplicate merges into the existing entity or observation instead of being added again; relationships between organization entities are promoted into the organization’s memory, in the chat’s scope; and the review runs on the organization’s API key, falling back to rule-based promotion when it has none. Episodes stay personal and form only from what was promoted into your own memory. Migration 0046.
Household invites (#345)
- flutter: Settings has a Household entry: the invitations waiting for you, with accept and decline, and the organizations you own or are in with your role. Create one by name; open one to see its members, and, as its owner or admin, invite by email (member or admin), see and revoke pending invitations, remove members, or hand the organization to a member. The bar’s menu renames or deletes an organization you own and leaves one you don’t. The link in an invitation mail lands in the app and accepts the invitation once you are signed in. English and Swedish.
- daemon: nobody joins an organization without consenting. An owner or
admin invites an address at
POST /api/v1/user/organizations/{orgId}/invites; the address gets a mail with a link, and a signed-in user with that address sees the invite atGET /api/v1/user/invitesand accepts or declines it (or accepts through the link,POST /api/v1/user/invites/consume). Invites expire after 14 days and can be revoked. Adding a member directly is now only for the caller’s own sub-accounts. A member can leave an organization; the owner transfers it to a member first (POST …/leave,POST …/transfer). The organization, member and invite routes are on the contract, andGET /api/v1/user/organizationslists the organizations the user is in, not only the owned ones, each with the user’s role. Migration 0045. - admin: the Organizations page shows rename and delete only for organizations the user owns.
Household agents in the app (#448)
- flutter: the sidebar’s Agents section lists each organization’s household agents under the organization’s name, below your own; one tap starts a chat. “Manage agents” shows every organization you are in as its own card: owners and admins create (“New agent for …”), edit and delete its agents, choosing the whole shared memory or one of its scopes; members see the list and who runs it. A chat with a household agent names the organization in its bar in place of “Memory on” and after the agent’s name above each reply, and offers no “Edit agent”. English and Swedish.
- daemon:
GET /api/v1/user/household-agentsreturns every organization the user owns or is in, their standing in it and its agents, in one call; a chat’s agent carries its organization’s name.
Household agents (#322)
- daemon: an organization can own agents. Its owner and admins create,
change and delete them at
/api/v1/user/organizations/{orgId}/agents(a name, how it behaves, an image address, and one of the organization’s memory scopes or its whole memory); every member lists them and starts a chat with one throughPOST /api/v1/user/chats, like a personal agent. Such a chat runs on the organization’s API key, never the member’s, and on the organization’s shared memory alone: the agent recalls, remembers and observes there (only recalls for a member who may not write), what it is told is extracted into the organization’s scope, and the member’s own memory, identity, standing instructions, skills, documents, calendars, reminders, GitHub and sandbox stay out. Chats list the agent with the organization that owns it. Migration 0044.
Memory spaces in the app (#335)
- flutter: the Memory Space has a Spaces tab: your memory spaces with their isolation level and how much each holds, a Default mark, and the way to make one (name, description, soft, hard or what-if isolation, colour and icon), edit it, make it the default or delete it. A chat is bound to a space from “Memory space…” in its menu in the chat list, and the chat’s bar names the space in place of “Memory on”. English and Swedish.
Personal agents in the app (#326)
- flutter: an Agents section in the sidebar and the phone’s drawer lists your personal agents: one tap starts a chat with one, “New agent” makes one with a name, how it behaves, an optional image address and a “Keeps its own memory” switch, and “Manage agents” opens the full list with edit and delete. A chat with an agent shows its face and name in the chat list, the sidebar, the chat’s bar (with “Own memory” in place of “Memory on” when it keeps its own) and above each reply, and the bar offers “Edit agent”. The memory switch creates a hard-isolated memory space named after the agent; turning it off unbinds the agent and keeps the space. English and Swedish.
Long tool results no longer flood the chat’s context (#127)
- daemon: every tool result the agent gets is limited in size, on every chat
provider: 100,000 characters (less for a model with a window under 200k
tokens), and 200,000 characters across all results in one turn. A longer
result is saved with the chat, and the agent gets its first 1,500 characters
plus a new
read_tool_outputtool to read on as far as it needs. Saved results are deleted with their chat. A tool’s error is cut to 2 KB. - daemon: migration
0043_tool_outputs.sqladds thetool_outputstable.
“Saved to memory · Undo” goes away again (#430)
- flutter: a toast with an Undo button (“Saved to memory”, “Deleted”) once more disappears after its few seconds. Flutter 3.47 started keeping any snackbar with an action up until it is tapped, so these stayed on screen for good; the app’s toasts now always time out, and Undo is offered for exactly as long as the toast shows.
Personal agents in the daemon and the contract (#325)
- daemon: a user can create personal agents, each with a name, an avatar and a persona prompt, and chat with them. An agent’s chats speak as the agent, on top of Celindor’s soul and rules, with the same memory, or in its own namespace if it keeps its memory to itself. Plain chats are unchanged. The app side follows in #326.
- contract:
GET/POST /api/v1/user/agentsandPATCH/DELETE /api/v1/user/agents/{id};agentIdonCreateChatRequestandCreatedChat;agentIdandagent(name, avatar) onChatSummary. Renaming and deleting a chat (PATCH/DELETE /api/v1/user/chats/{chatId}) are now on the contract. Additive only. - database: migration 0043 adds the
agentstable andchats.agent_id.
Memory spaces and write scope are on the API contract (#336)
- contract: the memory namespace routes (
GET/POST /api/v1/user/memory/namespaces,GET …/namespaces/stats,PATCH/DELETE …/namespaces/{id},POST …/namespaces/{id}/default), binding a chat to a namespace (PUT /api/v1/user/chats/{chatId}/namespace) and a chat’s memory write scope (GET/PUT /api/v1/user/chats/{chatId}/write-scope) are defined in the contract with contract tests, so the app gets a generated client for them. Payloads are unchanged. Additive only. - daemon: editing, deleting or making default another user’s namespace, which used to fail with 500 or do nothing, is a 404; binding a chat to a namespace that is not the user’s is a 400; an unknown isolation level is a 400.
- flutter: the write-scope calls read the generated contract types.
Dev builds: “What’s new” lists only the changes (#398)
- flutter: the list no longer shows a heading for each pull request merge (the issue’s number, hash and title, written for developers). Each change is its plain-language headline, bold and now at title size and starting with its issue number (“#285 …”), its hash and its summary, and opens its commit. “Built from” stays English in the Swedish app too, like the commits under it.
Security: session state only records your own chats (#410)
- daemon:
PUT /chat-read/{chatId},PUT /session/active-chat/{scope},POST /open-chats,PATCH /open-chats/{chatId}/touchandPUT /chat-scroll/{chatId}now answer 404 for a chat the caller does not own, like every other chat route. Before, they stored any id in the caller’s own state; nothing leaked, but they were the one exception to the tenant-isolation sweep. Clearing an entry stays unchecked, so a deleted chat can still be closed.
Dev builds: “What’s new” grouped by pull request (#398)
- flutter: each pull request is one entry: the issue it was built on in bold (“#398 Version history is still weird”), the pull request (“PR: #434 …”), its commits newest first as “hash subject”, each opening the commit, then the first section of the pull request’s description. A commit made on main directly is only its row. “Built from” stays English in the Swedish app too, like the commits under it.
- flutter: a search field above the list narrows it on every keystroke to the entries whose issue, pull request, commit hashes and subjects or description contain the text, ignoring case. A match shows its whole entry, under its release’s divider.
- scripts:
commit-log.shalso looks up each pull request’s title and description, keeps each commit’s subject as written, and takes a pull request’s issue from its title’s(#N)when its branch has no number.
Memory suggestions: “säkerhet” becomes “tillförlitlighet” (#285)
- flutter: the Swedish labels for how sure Celindor is about a memory
suggestion, on the suggestion row and on its filter chip, now read “Hög /
Medelhög / Låg tillförlitlighet” instead of “Hög / Medelhög / Låg säkerhet”,
which read as security. The English source strings (“High / Medium / Low
confidence”) are unchanged, as are the
high,mediumandlowlevels in the API. - flutter: a section heading’s action (the badge on a suggestion card, “Done” on the date picker, the Select chips) drops under the heading when the two no longer fit on one line, such as at a large text size, instead of overflowing.
Security: the master key can be rotated (#268)
- daemon:
celindor rotate-master-keyre-wraps every user and organization DEK, and the legacy secrets still encrypted with the master key directly, fromsecrets.previous_master_keytosecrets.master_key, in one transaction. A value neither key reads aborts it and changes nothing;--dry-runonly counts. - daemon: with
previous_master_keyset, the daemon reads values under either key and writes with the new one, so it keeps working during a rotation, and warns at start-up until the previous key is removed. - deploy: an optional
PREVIOUS_MASTER_KEYenvironment secret reaches the server. The procedure is indeploy/README.md§ Rotating the master key.
Weaker models stay on track with tools (#104)
- daemon: on the OpenAI-compatible and Gemini paths, a misspelled tool name
(
ScheduleReminder,web_search_tool) runs the tool it meant, and broken argument JSON (a trailing comma, an open bracket, a raw newline) is repaired. A model that loops (calls a missing tool, repeats a failing call, alternates between the same calls) is warned and then stopped, and a turn that runs out of steps ends with a summary of what was found instead of silence. A reply cut off by the output limit is continued, and an empty reply is asked for again.
“Where am I?” is answered from the phone, not from memory (#396)
- daemon: the system prompt now explains the place the phone attaches to a message. With one attached, the agent answers “where am I?” from it, in the user’s own name for the place, without searching memory for that name and reading out an address found there. Without one, it says it cannot tell, instead of inferring the user’s location from documents or earlier chats.
- flutter: a send keeps the saved place the chip shows when the phone cannot get a fresh fix within the send’s 4 s (common indoors), as long as the chip’s position is at most 5 minutes old. A fix the send does get updates the chip, so the two agree.
Hot memories follow the chat’s namespace (#333)
- daemon: the frequently used memories given to the agent on every turn now follow the chat’s memory namespace with the same rules as recall: the chat’s own namespace first, then memory in no namespace, then soft namespaces. A namespaced chat no longer sees the hot memories of other hard-isolated or hypothetical namespaces. Chats without a namespace are unchanged.
Dev builds: every change in “What’s new” has a headline (#398)
- flutter: each commit in “What’s new in this build” starts with a bold plain-language headline, then its hash and the summary, so an entry no longer reads as a bare hash under the previous one. A pull request’s heading is larger than its commits’ headlines and always present, as “Pull request #N” when the issue’s title is unknown.
- scripts:
commit-log.shasks the model for a headline next to the summary (titlebesidetextincommit_log.json); without a rewrite the commit’s subject is the headline and its body the text. The rewrite cache is versioned, so the first dev build after this redoes every entry.
Forgetting a document mid-ingest forgets it for good (#369)
- daemon: a document or recording forgotten while Celindor was still mining it could leave what the extractor found in the review queue, where the dream worker might later promote it. The pending tables now refuse a staged row whose document or recording is gone, and a write that arrives while the purge is running waits for it and is refused as well. Rows left behind before this change are removed by the migration. The ingest and recordings workers log the case as “forgotten while it was being mined” instead of a failure.
Memory suggestions intro no longer says “conversations” (#391)
- flutter: the intro on the memory suggestions page said Celindor noticed them “in your conversations”, though suggestions also come from documents and imports. It now says what Celindor noticed without naming a source.
Toasts stay clear of the composer (#354)
- flutter: the “Deleted · Undo” toast, and every other toast, floats above the chat composer however tall it is (more lines, attachments) instead of a fixed 80 px off the bottom edge, where it covered the field.
Dev builds: see what the agent is told (#393)
- flutter: the dev builds’ sidebar has “What the agent is told”, a page that shows the open chat’s system prompt (both tiers), its hash, the hash the running session was frozen with, and the tools offered to the model, with a filter and copy buttons. On the Claude subscription path the page says that Claude Code’s own preset comes first and is not shown.
- daemon:
GET /api/v1/user/chats/{chatId}/prompt-surfacerenders the prompt and tools for the signed-in user’s own chat, from the same inputs a session freezes its prompt from. Only a dev or local daemon serves it; test and production answer 404.
daemon, flutter: stop or steer a running turn (#105)
- The app has a stop button while a reply is written, and the field stays usable. A message sent meanwhile stops the reply and goes at once, waits until the reply is finished, or is added to the reply Celindor is working on, as chosen under Settings, “Sending while Celindor replies” (stop by default). A stopped reply keeps what it got to and is marked “Stopped”.
POST /api/v1/user/turns/{id}/cancelstops a running turn. The reply so far is kept and markedinterrupted(newChatMessage.interrupted), together with the tools that already ran; calls not yet started are skipped. The turn’sdoneevent carriesinterrupted: trueand the turn is recorded ascancelled(migration0041). On Gemini a stop now also ends the request that is waiting for the model, not only the next round.POST /api/v1/user/turns/{id}/steeradds a message to a running turn. The model reads it after its current tool calls, as its own message marked as added while it was working, and the chat stores it as a user message. A turn on the Agent SDK path cannot take one and answersqueued.POST /api/v1/user/messagesreturns the new turn’sturn_id.GET/PUT /api/v1/user/chat-settingsholdsbusySendMode(interrupt, the default,queueorsteer): what sending does while a reply is still being written.- Replies on the OpenAI-compatible and Gemini paths are now stored with their turn id, as the Claude paths already were.
Memory: your own relations show up (#228)
- daemon: searching the memory browser by a name now matches aliases on relationships and observations, as it already did for entities. The user’s own entity is stored as “me” with their real name as an alias, so searching your own name under Relationer found nothing.
- daemon: an entity’s relationship count leaves out relationships to archived entities, as its list does, so “Relationer (3)” no longer opens a shorter list.
- flutter: your own entity reads “Du” / “You” everywhere in Minnet (lists, relationship rows, its page and the graph) instead of “me”.
flutter: Inter’s own tracking on the web and Android (#368)
- Where Inter is the face (the web and Android), text now uses Inter’s own size-specific tracking in place of Material’s Roboto values, which had spread the letters out on settings, memory, data and recording screens. Body text tightens slightly, headings more so.
daemon: the agent sees the memories relevant to each message (#233)
- Before each chat turn the daemon searches memory for the user’s message (and,
for a follow-up that names nobody, the people and things the conversation
was just about) and attaches the best
matches, up to about 3,000 tokens, to that message. Answers no longer
depend on the model deciding to call recall.
[chat] ambient_memory_tokenssets the size;0turns it off. - The “frequently used” memories a turn carries grew from 8 entities and 10 facts to 25 and 30.
Report an issue from the dev builds (#379)
- The dev builds (TestFlight’s Celindor Dev, the Android dev build and dev.celindor.ai) have “Report an issue” in the left-hand menu: title, what happened, what you expected, steps, bug or suggestion, and an optional screenshot. The version and commit, client, OS, device model (browser on the web), API base and language are filled in and shown before sending. The app then shows “Reported as #123” with a link. The sheet is always in English and asks for the report in English (#387).
- The daemon files the issue in
Gubbdagis/celindorwith the reporter’s display name, labels ittested on device(bugs alsobugandflutter) and puts it in Backlog on the Celindor board with Priority Urgent. Screenshots are committed to theissue-report-attachmentsbranch. Ten reports an hour per user. - Test and production builds never show the entry: it sits in the dev
builds’ sidebar section (
CELINDOR_ENV=dev, see #380). Only a dev (or local) daemon serves the routes, and only withISSUE_REPORTS_GITHUB_TOKENset; the app hides the entry otherwise. - daemon/contract (additive):
GET /api/v1/user/issue-reports/statusandPOST /api/v1/user/issue-reports.
What’s new in this build, in the dev apps (#380)
- The dev builds (TestFlight’s “Celindor Dev”, the Android dev build and dev.celindor.ai) have “What’s new in this build” in the sidebar: the build’s last 200 commits in the order they landed on main (#387), newest first. Each pull request has a header with its issue, starting with the one the build was made from, and its commits under it, each with its short hash and a plain-language line about what changed. Tapping opens the issue, or the commit, on GitHub. A divider marks the release the test app runs. Test and production builds don’t show it.
- Builds now carry
--dart-define=CELINDOR_ENV=dev|test;kDevBuildin the app is true only for dev builds. Report an issue (#379) can use the same sidebar section. - CI: the reusable
commit-log.ymlwrites the list for dev builds withscripts/commit-log.sh, rewriting commits with Claude Haiku on thedevEnvironment’sOPERATOR_ANTHROPIC_API_KEYand caching rewrites between builds. The iOS, Android and web builds download it as an artifact, and pass it thedevEnvironment’s secrets withsecrets: inherit, without which the key was empty and no commit was rewritten (#387).
daemon, flutter: provider errors are retried, repaired or fallen back from (#103)
- A rate-limited, overloaded or failing provider is retried after the wait it
asks for (
Retry-After, or a reset time in its message), else after a backoff of 5, 10, 20… seconds (at most 120) plus jitter. A wait longer than 30 seconds is not sat out. - A request that no longer fits the model’s context window compacts the chat and is sent again; an output limit set too high is lowered to the model’s; an image the provider refuses as too large is left out with a note.
- A chat can fall back to up to three other (provider, model) pairs, tried
in order when its own provider is still rate-limited, overloaded or down.
Set them under Settings → AI provider → Fallback (
chat_fallbacksin tool-config). Operators can set a default for turns on the operator’s key with[chat] fallbacksin config.toml. A fallback needs its own key, and it never runs once the turn has shown text or run a tool. - A rejected key, an account out of credit and a content-policy refusal are never retried or fallen back from: the error says what happened and where to fix it, instead of the provider’s raw response.
- Turn usage records each provider a turn ran on, with
fell_back_fromset on the one that answered after a fallback. - The tool-config routes are now in the API contract, with the same paths
and shapes. Migration
0040allows the newchat_fallbackskey.
daemon: Files keep their names in the library (#355)
- A file saved to the library, from the library itself or from a chat, keeps the name it had (without the extension). Processing used to replace it with an English title made up from the content, such as “MFF Season Ticket 2021 Receipt”. Email, imports and camera photos still get a title from their content.
- Migration
0037puts the filename back on documents that were already renamed this way.
flutter: The Apple system font on iOS and macOS (#367)
- iOS and macOS render the Apple system font (SF Pro) throughout, with Apple’s tracking at each size, in place of Inter. Since the design system (#278) text there carried Material’s Roboto tracking and, in nav bars and buttons, SF’s tracking on Inter, so letters sat visibly too far apart.
- Android and the web are unchanged (Inter).
Security: the docker sandbox, further hardened (#265)
- The daemon no longer mounts the Docker socket. A new
docker-proxyservice holds it and passes on only the sandbox adapter’s calls: it acts only on this environment’s sandbox containers, refuses image pulls, and refuses any create that differs from the adapter’s own (no privileged containers, host mounts, added capabilities or other networks). - Sandbox egress goes through an allowlist. Sandboxes sit on an internal
network whose only way out is a new
egress-proxyservice. It lets HTTPS and HTTP through to PyPI, npm and GitHub only (SANDBOX_EGRESS_ALLOWreplaces the list), and refuses names that resolve to private addresses. - A new
celindor-sandboximage (daemon/sandbox-image) replaces plainubuntu:24.04. It comes with Python, Node, git, curl, ripgrep, jq and a compiler, runs as a non-root user on a read-only root filesystem, and leaves only the workspace, the home directory and/tmpwritable.pip installandnpm installstill work.apt-get installno longer does. - An exec that times out is killed, with what it started, instead of running on in the container. Output is capped at 1 MB per stream.
- Local development builds
celindor-sandbox:localon first start (scripts/daemon-local.sh,scripts/dev-switch.sh);[sandbox] image,networkandegress_proxyinconfig.tomloverride the defaults.
Google Drive: single files, not only whole folders (#356)
- The Drive picker (Library → Google Drive → Choose folders and files) lists the files in each folder that Celindor can read, each with its own tickbox, so one file can be brought in without its whole folder. A file under a ticked folder shows as included already.
- A chosen file is synced like a folder holding only it: edits in Drive are fetched again, and it goes to the Trash when it is deleted or trashed in Drive. Moving it to another folder keeps it.
- Unticking a folder or file always asks whether to keep what Celindor already fetched from it or forget it and what Celindor learned from it. It used to ask only when the library held documents from Drive when the picker opened, so a file ticked by mistake and unticked at once could be kept unasked.
- daemon/contract (additive):
GoogleDriveFile;fileson the connection, on the folder listing, and on the selection request and result. A selection sent withoutfilesleaves the chosen files as they are. Migration 0040 addsgoogle_drive_files.
flutter: The Library in the new look, Google Drive included (#313, epic #256)
- Uploaded files and Google Drive are two matching sections on every platform: an uppercase label with a count, over the same rounded cards. “Add files”, “Choose folders” and “Connect Google Drive” are the first row of their card instead of lone buttons, and the waiting-to-upload note sits under the card.
- Deleting a file is the same red bin button on every platform, with a 44 px target.
- Dragging files over the library highlights it in the accent colour.
- Google Drive settings: Disconnect is a red row of the account card rather than a bare button under it.
- The folder picker’s text uses the app’s type, and each folder row is at least 44 px tall; folder and arrow icons are the same on every platform.
flutter: the Settings screens in the new look (#309, epic #256)
- Your data shows each company that receives your data as a card of its own, and the location badge, “Add a key” and “Read their terms” are chips that are easy to tap.
- AI provider: the model and server fields sit flat inside the provider instead of in a box within a box, model suggestions are chips, and the chosen provider is marked in the same blue on every platform (it was green on iPhone and Mac).
- Instructions: removing one is a quiet bin icon beside it rather than a red word next to a long instruction; the confirmation is still red.
- Calendar: pull to refresh shows the iPhone spinner on iPhone and Mac, and the calendar switches match the rest of the app.
- Quiet hours: the iPhone wheel sits on the app’s card instead of black; elsewhere the hours are a list in the app’s dialog, with the current one ticked.
- Trash, Location, About, Profile, Password, Permissions, Google Drive, Me and Relationships use the app’s type, colours and one set of icons on every platform. About marks matching app and server versions in green.
- Error notices in Settings use the new soft red with readable text.
- The API server override on the sign-in screen is now only in developer builds, as in Settings. A TestFlight or store build ignores an override saved earlier and uses its own server, since it can no longer be changed there.
flutter: To review in the new look (#311, epic #256)
- On a phone, a suggestion’s row gives its name the width: swipe right to save it, left to remove it, or tap ⋯ for Save, Later and Remove. Two labelled buttons used to squeeze the name to a third of the screen, worse in Swedish. On wider screens the row keeps its Remove and Save buttons. A document’s row saves all its suggestions only from ⋯, never on a swipe; a swipe left still removes them all, after asking.
- Save is the app’s blue everywhere in To review, Remove is red.
- To review / Saved for later is one switch; the filters are the app’s chips, and “All dates” clears a date range. A label over the list counts what the filter shows, next to Select.
- On iPhone the dates are picked on iOS’s date wheel in a sheet (From / To), instead of Android’s full-screen calendar.
- The open suggestion is a card in the new look, and its buttons wrap rather than run off the edge. While choosing several, the bar along the bottom fits a phone: the count and Later above Remove and Save.
flutter: the chat’s bar, list and composer in the new look (#314, epic #256)
- The chat looks the same on iPhone, Mac, Android and the web: one bar with the same named buttons (chats, new chat, share, sources) instead of Apple’s navigation bar and its own icons.
- The composer is the new rounded card everywhere, with round buttons for attach, dictation, voice and send. The microphone turns red while it listens. The place chip above it is the new chip too.
- The offline notice and a failed send’s message are calm bands in the app’s colours, the failure in red, instead of Apple’s grey and yellow.
- The phone’s full chat list (☰ → Show all chats) is one rounded list on every platform. Swipe a chat left to delete it on iPhone too, and a ⋯ button or a long press renames or deletes it.
- The Memory target chip and its picker use the new chip and sheet: one card per organization under its name, and a refused change in red.
- A file that failed to upload sits on a soft red with readable text, instead of a solid red chip.
flutter: Memory in the new look, graph included (#310, epic #256)
- Memory’s lists (short-term items, people, places and other entities, relationships, observations, the dream journal and a dream’s decisions) show each item on a rounded card in the new type, with the kind’s coloured disc instead of a grey circle.
- Names that open an entity inside a relationship or an observation are now a full-size tap target, not just the height of the text.
- The kind filters, the trail above the graph and the status and “Frequently used” labels are the app’s round chips. The graph/list switch and the sort switch are the same pill toggle as Short-term/Long-term; the graph/list switch stays compact, so the tabs beside it keep their width.
- Each entity kind has its own colour in light and in dark, picked to be told apart and to read on either background. The graph’s card, folded bar, corner buttons, group labels and folded groups follow the theme instead of white panels and mixed violet and blue.
- An entity’s page groups its aliases and details on one card under a section label; a dream’s page puts the night’s story and counts on a memory card, with its decisions under their own label.
- The failed-dream notice uses the app’s red error colours for its text and buttons.
- The About you entries have icons, as in Settings.
flutter: Recordings in the new look (#312, epic #256)
- The record button is the Recordings teal, and turns red while it records. Upload and History are the app’s pill buttons on every platform.
- The last recording is a card with a status chip (Ready, Processing, Failed) and one row of actions, instead of up to three stacked buttons. History is a list of cards with the same chips.
- The review screen no longer uses Android-style controls on iPhone and Mac: the player has a round play button and a thin teal timeline, and who said a sentence is picked from speaker chips instead of a drop-down menu.
- Each speaker has their own colour, readable in light and dark, and no longer red like an error. Speaker chips and transcript sentences are at least 44 px tall, so they are easier to tap.
- The summary, key points, quotes, decisions and to-dos sit on cards under section labels.
- Fixed: closing the sentence or speaker-name dialog could raise an error while it faded out.
flutter: sign-in, onboarding and invite screens in the new look (#315, epic #256)
- Opening the app in dark mode no longer flashes white on iPhone before the first screen: the launch screen is the app’s own dark navy in dark mode and its light page otherwise (the Mac window opens the same way), and a saved Light or Dark choice applies from the very first frame.
- The sign-in page keeps its branded navy and glow in both light and dark, with its headings on the app’s type scale.
- The welcome (onboarding) and invite-code screens use the same title, text and colours on iPhone, Mac, Android and the web.
daemon, admin: operators see memory as metadata, and every admin change is audited (#267)
- The admin memory routes no longer return an entity’s name, slug or
attributes, or an observation’s text:
GET /api/v1/admin/users/{id}/entitiesand…/entities/{entityId}return ids, kinds, status, dates and text lengths. Entities are addressed by id instead of kind and slug. - Operators can no longer create, edit, merge or reflect on a user’s entities, add observations, or set or remove a user’s keys. Deleting an entity or one of its observations remains, for support cleanup.
- Every admin write is recorded in a new
admin_audittable (migration0039): operator, user, method, route pattern and status, never the request body.GET /api/v1/admin/auditlists it, and the admin console has an Audit page and an Audit tab per user.
daemon, admin, flutter: Token usage and cost per chat turn (#106)
- Every chat turn that reaches a model records which provider and model
answered, its input, output, cache-read and cache-write tokens, and what it
cost, in a new
turn_usagetable (migration0038). A refusal fallback is recorded at the model that answered, with the one asked for beside it. - Costs come from rate cards for Anthropic, OpenAI (
api.openai.com) and the Gemini API, in whole millionths of a dollar. Turns on other OpenAI-compatible hosts are counted in tokens only. A turn on the user’s own key is marked as an estimate. - Chat turns now count toward the user’s monthly tokens in
user_quotas(cache reads excluded). A turn is refused when the month is used up only for users whose quota row has the newenforce_chat_quotaset. GET /api/v1/user/usage?days=returns the user’s totals and per-model usage;GET /api/v1/admin/usage?days=adds a per-user breakdown.- Admin has a Usage page: tokens, cache hit rate and cost per user and per model over 7, 30 or 90 days.
- In the app, Memory → About you → Your data has an “AI usage” section: what the last 30 days of chat cost, how many replies, tokens read and written, the share read from cache, and each model’s replies and cost.
API changes
GET /api/v1/admin/audit
- endpoint added
GET /api/v1/admin/chats/{chatId}/messages
- added the non-success response with the status
404
GET /api/v1/admin/usage
- endpoint added
GET /api/v1/admin/usage/performance
- endpoint added
GET /api/v1/admin/users/{id}/entities
- endpoint added
GET /api/v1/admin/users/{id}/entities/{entityId}
- endpoint added
POST /api/v1/auth/claim
- added the non-success response with the status
429
POST /api/v1/auth/login
- added the non-success response with the status
413
POST /api/v1/auth/password
- the request property
current_passwordbecame optional
GET /api/v1/auth/session
- added the optional property
anyOf[#/components/schemas/AuthSessionBound]/email_verifiedto the response with the200status - added the required property
anyOf[#/components/schemas/AuthSessionBound]/has_passwordto the response with the200status - added the required property
anyOf[#/components/schemas/AuthSessionUnbound]/has_passwordto the response with the200status
POST /api/v1/auth/setup
- endpoint added
POST /api/v1/auth/setup/preview
- endpoint added
GET /api/v1/auth/verify-email
- endpoint added
POST /api/v1/auth/verify-email
- endpoint added
POST /api/v1/auth/verify-email/resend
- endpoint added
POST /api/v1/auth/watch/code
- endpoint added
POST /api/v1/auth/watch/exchange
- endpoint added
GET /api/v1/auth/watch/sessions
- endpoint added
DELETE /api/v1/auth/watch/sessions/{id}
- endpoint added
GET /api/v1/oauth/email/{provider}/callback
- endpoint added
GET /api/v1/oauth/google-drive/callback
- :warning: removed the success response with the status
200 - added the non-success response with the status
302 - removed the non-success response with the status
502
POST /api/v1/tool-config
- endpoint added
DELETE /api/v1/tool-config/{key}
- endpoint added
GET /api/v1/user/agents
- endpoint added
POST /api/v1/user/agents
- endpoint added
DELETE /api/v1/user/agents/{id}
- endpoint added
PATCH /api/v1/user/agents/{id}
- endpoint added
POST /api/v1/user/agents/{id}/retire
- endpoint added
POST /api/v1/user/agents/{id}/unretire
- endpoint added
GET /api/v1/user/attachments/{id}/download
- added the non-success response with the status
501
POST /api/v1/user/calendar/accounts/icloud-from-mail
- endpoint added
GET /api/v1/user/calendar/oauth/{provider}/callback
- :warning: removed the success response with the status
200 - added the non-success response with the status
400
GET /api/v1/user/channels
- endpoint added
POST /api/v1/user/channels/link
- endpoint added
DELETE /api/v1/user/channels/links/{linkId}
- endpoint added
POST /api/v1/user/channels/pair
- endpoint added
GET /api/v1/user/chat-settings
- endpoint added
PUT /api/v1/user/chat-settings
- endpoint added
GET /api/v1/user/chats
- added the new optional
queryrequest parameterarchived - added the optional property
chats/items/agentto the response with the200status - added the optional property
chats/items/agentIdto the response with the200status - added the optional property
chats/items/archivedAtto the response with the200status
POST /api/v1/user/chats
- added the new optional request property
agentId - added the non-success response with the status
400 - added the optional property
chat/agentto the response with the201status - added the optional property
chat/agentIdto the response with the201status
DELETE /api/v1/user/chats/{chatId}
- endpoint added
PATCH /api/v1/user/chats/{chatId}
- endpoint added
POST /api/v1/user/chats/{chatId}/archive
- endpoint added
POST /api/v1/user/chats/{chatId}/attachments
- added the non-success response with the status
501
GET /api/v1/user/chats/{chatId}/messages
- :warning: added the new
settingsenum value to themessages/items/cards/items/kindresponse property for the response status200 - added the new optional
queryrequest parameterbefore - added the new optional
queryrequest parameterbeforeCreatedAt - added the non-success response with the status
400 - added the optional property
messages/items/cards/items/prefillto the response with the200status - added the optional property
messages/items/cards/items/screento the response with the200status - added the optional property
messages/items/cards/items/workItemIdto the response with the200status - added the optional property
messages/items/interruptedto the response with the200status - added the optional property
messages/items/memorySourcesto the response with the200status - added the optional property
messages/items/toolCalls/items/askIdto the response with the200status - added the optional property
messages/items/toolCalls/items/chatIdto the response with the200status - added the optional property
messages/items/toolCalls/items/outcometo the response with the200status
PUT /api/v1/user/chats/{chatId}/namespace
- endpoint added
GET /api/v1/user/chats/{chatId}/plan
- endpoint added
GET /api/v1/user/chats/{chatId}/prompt-surface
- endpoint added
POST /api/v1/user/chats/{chatId}/unarchive
- endpoint added
GET /api/v1/user/chats/{chatId}/write-scope
- endpoint added
PUT /api/v1/user/chats/{chatId}/write-scope
- endpoint added
POST /api/v1/user/clarify/{requestId}
- endpoint added
GET /api/v1/user/contacts
- endpoint added
GET /api/v1/user/contacts/accounts
- endpoint added
POST /api/v1/user/contacts/accounts/icloud
- endpoint added
DELETE /api/v1/user/contacts/accounts/{id}
- endpoint added
POST /api/v1/user/data/imports
- added the non-success response with the status
400
POST /api/v1/user/data/imports/{id}/commit
- added the non-success response with the status
400
DELETE /api/v1/user/detach
- endpoint added
GET /api/v1/user/detach
- endpoint added
POST /api/v1/user/detach
- endpoint added
GET /api/v1/user/devices
- :warning: added the new
watchosenum value to thedevices/items/platformresponse property for the response status200
POST /api/v1/user/devices
- :warning: added the new
watchosenum value to theplatformresponse property for the response status201 - added the new optional request property
language - added the new
watchosenum value to the request propertyplatform
DELETE /api/v1/user/devices/{token}
- added the non-success response with the status
400
POST /api/v1/user/documents/{id}/upload
- endpoint added
GET /api/v1/user/dreams/{id}
- added the required property
decisions/items/statusto the response with the200status - added the required property
decisions/items/titleto the response with the200status - added the required property
decisions/items/undoableto the response with the200status
POST /api/v1/user/dreams/{id}/decisions/{extraction_id}/undo
- endpoint added
GET /api/v1/user/email-accounts
- endpoint added
POST /api/v1/user/email-accounts/icloud-from-calendar
- endpoint added
POST /api/v1/user/email-accounts/imap
- endpoint added
POST /api/v1/user/email-accounts/oauth/{provider}/connect
- endpoint added
DELETE /api/v1/user/email-accounts/{id}
- endpoint added
GET /api/v1/user/email-accounts/{id}
- endpoint added
PUT /api/v1/user/email-accounts/{id}
- endpoint added
POST /api/v1/user/email-accounts/{id}/sync
- endpoint added
DELETE /api/v1/user/google-drive
- added the non-success response with the status
400
GET /api/v1/user/google-drive
- added the optional property
filesto the response with the200status
POST /api/v1/user/google-drive/connect
- added optional request body
- added the non-success response with the status
400
GET /api/v1/user/google-drive/folders
- added the non-success response with the status
400 - added the non-success response with the status
501 - added the optional property
filesto the response with the200status
PUT /api/v1/user/google-drive/folders
- added the new optional request property
files - added the optional property
filesto the response with the200status
GET /api/v1/user/household-agents
- endpoint added
DELETE /api/v1/user/instructions/{id}
- added the non-success response with the status
400
GET /api/v1/user/invites
- endpoint added
POST /api/v1/user/invites/consume
- endpoint added
POST /api/v1/user/invites/{inviteId}/accept
- endpoint added
POST /api/v1/user/invites/{inviteId}/decline
- endpoint added
POST /api/v1/user/issue-reports
- endpoint added
GET /api/v1/user/issue-reports/status
- endpoint added
GET /api/v1/user/manuals
- endpoint added
GET /api/v1/user/manuals/{version}/{kind}/{language}
- endpoint added
GET /api/v1/user/manuals/{version}/{kind}/{language}/download
- endpoint added
POST /api/v1/user/memory/anomalies/{id}/resolve
- endpoint added
PATCH /api/v1/user/memory/entities/{id}
- endpoint added
GET /api/v1/user/memory/namespaces
- endpoint added
POST /api/v1/user/memory/namespaces
- endpoint added
GET /api/v1/user/memory/namespaces/stats
- endpoint added
DELETE /api/v1/user/memory/namespaces/{id}
- endpoint added
PATCH /api/v1/user/memory/namespaces/{id}
- endpoint added
POST /api/v1/user/memory/namespaces/{id}/default
- endpoint added
GET /api/v1/user/memory/observations
- added the new optional
queryrequest parameterstatus
POST /api/v1/user/memory/observations/{id}/correct
- endpoint added
POST /api/v1/user/memory/observations/{id}/forget
- endpoint added
POST /api/v1/user/memory/observations/{id}/not-true
- endpoint added
POST /api/v1/user/memory/observations/{id}/restore
- endpoint added
GET /api/v1/user/memory/relationships
- added the new optional
queryrequest parameterstatus
POST /api/v1/user/memory/relationships/{id}/forget
- endpoint added
POST /api/v1/user/memory/relationships/{id}/not-true
- endpoint added
POST /api/v1/user/memory/relationships/{id}/restore
- endpoint added
GET /api/v1/user/memory/search
- endpoint added
POST /api/v1/user/messages
- added the non-success response with the status
501 - added the optional property
duplicateto the response with the200status - added the optional property
turn_idto the response with the200status
GET /api/v1/user/mission-settings
- endpoint added
PUT /api/v1/user/mission-settings
- endpoint added
GET /api/v1/user/missions
- endpoint added
POST /api/v1/user/missions
- endpoint added
GET /api/v1/user/missions/{missionId}
- endpoint added
PATCH /api/v1/user/missions/{missionId}
- endpoint added
POST /api/v1/user/missions/{missionId}/answer
- endpoint added
GET /api/v1/user/notification-preferences
- endpoint added
PUT /api/v1/user/notification-preferences
- endpoint added
POST /api/v1/user/oauth/complete
- endpoint added
GET /api/v1/user/operator-access
- endpoint added
GET /api/v1/user/organization-notices
- endpoint added
DELETE /api/v1/user/organization-notices/{noticeId}
- endpoint added
GET /api/v1/user/organizations
- endpoint added
POST /api/v1/user/organizations
- endpoint added
GET /api/v1/user/organizations/deleted
- endpoint added
DELETE /api/v1/user/organizations/{orgId}
- endpoint added
PATCH /api/v1/user/organizations/{orgId}
- endpoint added
GET /api/v1/user/organizations/{orgId}/agents
- endpoint added
POST /api/v1/user/organizations/{orgId}/agents
- endpoint added
DELETE /api/v1/user/organizations/{orgId}/agents/{id}
- endpoint added
PATCH /api/v1/user/organizations/{orgId}/agents/{id}
- endpoint added
POST /api/v1/user/organizations/{orgId}/agents/{id}/retire
- endpoint added
POST /api/v1/user/organizations/{orgId}/agents/{id}/unretire
- endpoint added
GET /api/v1/user/organizations/{orgId}/available-members
- endpoint added
GET /api/v1/user/organizations/{orgId}/budget
- endpoint added
PUT /api/v1/user/organizations/{orgId}/budget
- endpoint added
PUT /api/v1/user/organizations/{orgId}/entities/{entityId}/shared-link
- endpoint added
GET /api/v1/user/organizations/{orgId}/invites
- endpoint added
POST /api/v1/user/organizations/{orgId}/invites
- endpoint added
DELETE /api/v1/user/organizations/{orgId}/invites/{inviteId}
- endpoint added
POST /api/v1/user/organizations/{orgId}/leave
- endpoint added
GET /api/v1/user/organizations/{orgId}/members
- endpoint added
POST /api/v1/user/organizations/{orgId}/members
- endpoint added
DELETE /api/v1/user/organizations/{orgId}/members/{userId}
- endpoint added
PATCH /api/v1/user/organizations/{orgId}/members/{userId}
- endpoint added
GET /api/v1/user/organizations/{orgId}/memory/contributions
- endpoint added
POST /api/v1/user/organizations/{orgId}/memory/entities/promote
- endpoint added
DELETE /api/v1/user/organizations/{orgId}/memory/entities/{entityId}
- endpoint added
POST /api/v1/user/organizations/{orgId}/memory/entities/{entityId}/restore
- endpoint added
POST /api/v1/user/organizations/{orgId}/memory/observations/promote
- endpoint added
GET /api/v1/user/organizations/{orgId}/missions
- endpoint added
POST /api/v1/user/organizations/{orgId}/missions
- endpoint added
GET /api/v1/user/organizations/{orgId}/missions/{missionId}
- endpoint added
PATCH /api/v1/user/organizations/{orgId}/missions/{missionId}
- endpoint added
POST /api/v1/user/organizations/{orgId}/missions/{missionId}/answer
- endpoint added
GET /api/v1/user/organizations/{orgId}/missions/{missionId}/messages
- endpoint added
POST /api/v1/user/organizations/{orgId}/restore
- endpoint added
GET /api/v1/user/organizations/{orgId}/scopes
- endpoint added
POST /api/v1/user/organizations/{orgId}/scopes
- endpoint added
DELETE /api/v1/user/organizations/{orgId}/scopes/{scopeId}
- endpoint added
PATCH /api/v1/user/organizations/{orgId}/scopes/{scopeId}
- endpoint added
GET /api/v1/user/organizations/{orgId}/scopes/{scopeId}/answer-policy
- endpoint added
PUT /api/v1/user/organizations/{orgId}/scopes/{scopeId}/answer-policy
- endpoint added
GET /api/v1/user/organizations/{orgId}/scopes/{scopeId}/ask-allowlist
- endpoint added
PUT /api/v1/user/organizations/{orgId}/scopes/{scopeId}/ask-allowlist
- endpoint added
GET /api/v1/user/organizations/{orgId}/scopes/{scopeId}/ask-requests
- endpoint added
POST /api/v1/user/organizations/{orgId}/scopes/{scopeId}/ask-requests/{requestId}/decision
- endpoint added
GET /api/v1/user/organizations/{orgId}/scopes/{scopeId}/audit-export
- endpoint added
GET /api/v1/user/organizations/{orgId}/scopes/{scopeId}/break-glass
- endpoint added
POST /api/v1/user/organizations/{orgId}/scopes/{scopeId}/break-glass
- endpoint added
GET /api/v1/user/organizations/{orgId}/scopes/{scopeId}/budget
- endpoint added
PUT /api/v1/user/organizations/{orgId}/scopes/{scopeId}/budget
- endpoint added
GET /api/v1/user/organizations/{orgId}/scopes/{scopeId}/disclosures
- endpoint added
GET /api/v1/user/organizations/{orgId}/scopes/{scopeId}/entity-links
- endpoint added
POST /api/v1/user/organizations/{orgId}/scopes/{scopeId}/entity-links/dismissals
- endpoint added
GET /api/v1/user/organizations/{orgId}/scopes/{scopeId}/entity-links/search
- endpoint added
GET /api/v1/user/organizations/{orgId}/scopes/{scopeId}/members
- endpoint added
POST /api/v1/user/organizations/{orgId}/scopes/{scopeId}/members
- endpoint added
DELETE /api/v1/user/organizations/{orgId}/scopes/{scopeId}/members/{userId}
- endpoint added
GET /api/v1/user/organizations/{orgId}/scopes/{scopeId}/publish-policy
- endpoint added
PUT /api/v1/user/organizations/{orgId}/scopes/{scopeId}/publish-policy
- endpoint added
GET /api/v1/user/organizations/{orgId}/scopes/{scopeId}/publish-reviews
- endpoint added
POST /api/v1/user/organizations/{orgId}/scopes/{scopeId}/publish-reviews/{reviewId}/approve
- endpoint added
POST /api/v1/user/organizations/{orgId}/scopes/{scopeId}/publish-reviews/{reviewId}/decline
- endpoint added
POST /api/v1/user/organizations/{orgId}/scopes/{scopeId}/publish-reviews/{reviewId}/unpublish
- endpoint added
POST /api/v1/user/organizations/{orgId}/transfer
- endpoint added
GET /api/v1/user/organizations/{orgId}/walls
- endpoint added
POST /api/v1/user/organizations/{orgId}/walls
- endpoint added
PUT /api/v1/user/organizations/{orgId}/walls/membership
- endpoint added
DELETE /api/v1/user/organizations/{orgId}/walls/{wallId}
- endpoint added
POST /api/v1/user/pending-entities/{id}/approve
- endpoint added
POST /api/v1/user/pending-entities/{id}/reject
- endpoint added
POST /api/v1/user/pending-observations/{id}/approve
- endpoint added
POST /api/v1/user/pending-observations/{id}/reject
- endpoint added
POST /api/v1/user/person-links
- endpoint added
POST /api/v1/user/person-links/dismiss
- endpoint added
GET /api/v1/user/person-links/suggestions
- endpoint added
POST /api/v1/user/presence
- added the non-success response with the status
501
POST /api/v1/user/recordings/upload
- added optional request body
- added the non-success response with the status
400 - added the non-success response with the status
413
GET /api/v1/user/recordings/{id}/people
- endpoint added
POST /api/v1/user/recordings/{id}/summary
- added the non-success response with the status
429
POST /api/v1/user/recordings/{id}/transcribe
- added the non-success response with the status
429
PUT /api/v1/user/recordings/{id}/transcript
- added the new optional request property
speakers/items/entity_id - added the optional property
speakers/items/entity_idto the response with the200status
GET /api/v1/user/recordings/{id}/transcripts
- added the optional property
transcripts/items/speakers/items/entity_idto the response with the200status
GET /api/v1/user/reminders
- endpoint added
DELETE /api/v1/user/reminders/{id}
- endpoint added
GET /api/v1/user/skill-proposals
- endpoint added
POST /api/v1/user/skill-proposals/{proposalId}/approve
- endpoint added
POST /api/v1/user/skill-proposals/{proposalId}/reject
- endpoint added
GET /api/v1/user/skills
- endpoint added
GET /api/v1/user/staging
- endpoint added
GET /api/v1/user/staging/auto-merges
- endpoint added
POST /api/v1/user/staging/auto-merges/acknowledge
- endpoint added
POST /api/v1/user/staging/auto-merges/{id}/undo
- endpoint added
POST /api/v1/user/staging/entities/{id}/dismiss
- endpoint added
POST /api/v1/user/staging/entities/{id}/promote
- endpoint added
POST /api/v1/user/staging/entities/{id}/relink
- endpoint added
POST /api/v1/user/staging/observations/{id}/dismiss
- endpoint added
POST /api/v1/user/staging/observations/{id}/promote
- endpoint added
POST /api/v1/user/staging/relationships/{id}/dismiss
- endpoint added
POST /api/v1/user/staging/relationships/{id}/promote
- endpoint added
POST /api/v1/user/sub-accounts
- endpoint added
DELETE /api/v1/user/sub-accounts/{id}/detach
- endpoint added
POST /api/v1/user/sub-accounts/{id}/detach
- endpoint added
POST /api/v1/user/sub-accounts/{id}/setup-link
- endpoint added
GET /api/v1/user/surface/today
- added the new optional
queryrequest parametertimezone
GET /api/v1/user/threads
- endpoint added
PUT /api/v1/user/threads/{threadId}/activity
- endpoint added
POST /api/v1/user/threads/{threadId}/attachments
- endpoint added
GET /api/v1/user/threads/{threadId}/entities/{entityId}
- endpoint added
GET /api/v1/user/threads/{threadId}/files
- endpoint added
POST /api/v1/user/threads/{threadId}/files/{fileId}/transcribe
- endpoint added
GET /api/v1/user/threads/{threadId}/members
- endpoint added
GET /api/v1/user/threads/{threadId}/messages
- endpoint added
POST /api/v1/user/threads/{threadId}/messages
- endpoint added
DELETE /api/v1/user/threads/{threadId}/messages/{messageId}
- endpoint added
PATCH /api/v1/user/threads/{threadId}/messages/{messageId}
- endpoint added
GET /api/v1/user/threads/{threadId}/messages/{messageId}/forward
- endpoint added
POST /api/v1/user/threads/{threadId}/messages/{messageId}/forward
- endpoint added
PUT /api/v1/user/threads/{threadId}/messages/{messageId}/pin
- endpoint added
PUT /api/v1/user/threads/{threadId}/messages/{messageId}/reactions
- endpoint added
PUT /api/v1/user/threads/{threadId}/mute
- endpoint added
GET /api/v1/user/threads/{threadId}/pins
- endpoint added
PUT /api/v1/user/threads/{threadId}/read
- endpoint added
GET /api/v1/user/threads/{threadId}/reads
- endpoint added
GET /api/v1/user/threads/{threadId}/search
- endpoint added
PUT /api/v1/user/threads/{threadId}/settings
- endpoint added
POST /api/v1/user/threads/{threadId}/typing
- endpoint added
GET /api/v1/user/tool-config
- endpoint added
POST /api/v1/user/turns/{id}/cancel
- endpoint added
POST /api/v1/user/turns/{id}/steer
- endpoint added
GET /api/v1/user/unread
- endpoint added
POST /api/v1/user/unread/mark-read
- endpoint added
GET /api/v1/user/usage
- endpoint added
POST /api/v1/user/voice/transcribe
- endpoint added
POST /api/v1/user/watch/chat
- endpoint added
GET /api/v1/user/web/search
- endpoint added
GET /api/v1/user/work-items
- endpoint added
POST /api/v1/user/work-items
- endpoint added
GET /api/v1/user/work-items/due
- endpoint added
DELETE /api/v1/user/work-items/{id}
- endpoint added
GET /api/v1/user/work-items/{id}
- endpoint added
PATCH /api/v1/user/work-items/{id}
- endpoint added
POST /api/v1/user/work-items/{id}/reminder
- endpoint added
POST /api/v1/user/work-items/{id}/snooze
- endpoint added
[0.6.2] - 2026-10-26
Fixed: uploads that never arrived waited forever (#294)
- A document whose file never reached the daemon (the app was closed mid-upload,
say) stayed Waiting for upload for good, and opening it showed a raw
storage error page. The daemon now fails such uploads 24 hours after they
were started, and Library says the file never arrived and to delete it and
add it again. A document without its file can no longer be opened, and the
download route answers
409for one instead of a link to nothing.
daemon: an operator AI key for dev, test and local instances
- A new instance (test, a fresh local database, another
CELINDOR_HOME) starts with no AI keys: users’ keys live in each instance’s own database, encrypted under its own master key. Now an instance can carry an operator fallback,[secrets] anthropic_api_key. Users without a Claude key of their own (nor an org’s or an inherited one) run on it, for chat and for dreams, extraction and summaries: every user on dev and local, only@celindor.aiaccounts on test. A user’s own key always wins. It is ignored in prod. - deploy: optional
OPERATOR_ANTHROPIC_API_KEY,OPERATOR_OPENAI_API_KEYandOPERATOR_GEMINI_API_KEYGitHub Environment secrets are rendered into the container’s config (never for prod) and kept out of the daemon’s own environment. celindor inittakes the key from$ANTHROPIC_API_KEYwhen set.
daemon: Boundary tests for SECURITY.md (#263)
- Fixed: the review view for a document or recording’s pending memories
(
/api/v1/user/pending-extractions/:type/:id) showed the title of another user’s document, or the start of their recording’s transcript, to anyone who had its id. It now looks up only the caller’s own. - New tests hold the tenant, operator and sandbox boundaries in
daemon/SECURITY.mdagainst every route the daemon mounts. The tenant tests need a real Postgres and now run in CI.
A dev app on TestFlight, beside the test app (#347)
- flutter (iOS): every
mainpush that touches the app is uploaded to TestFlight as Celindor Dev (com.celindor.app.dev), talking to dev.celindor.ai, with a red banner on the icon showing the commit. It installs next to the test app, Celindor, whose icon now has a green banner with the release tag. Each app opens only its own server’s sign-in links. Setup and push notes:RELEASING.md→ App Store. - flutter (iOS): App Store Connect no longer warns about a missing “Always” location purpose string (ITMS-90683). The app only asks for location while in use; the unused “Always” request in geolocator is now compiled out of TestFlight builds.
- daemon: push reaches every Apple app on a daemon — the TestFlight dev app, a local Xcode build and the test app — each with its own bundle ID and APNs environment. Before, a daemon pushed to one app on one environment, and a device on the other environment lost its registration on the first push. Registering the same token again now updates it; it failed before.
- contract:
POST /api/v1/user/devicestakes optionalapp_idandapns_environment(sandbox/production) for iOS and macOS devices. - flutter (iOS, macOS): the app sends its bundle ID and APNs environment when it registers for push.
- migration
0037_device_apns_app.sqladds both touser_devices. Until it runs, only apps from before this change can register.
flutter: Settings and lists in the new look (#308, epic #256)
- Settings and every grouped list in the app (Library, Calendar, Trash, Your data and the other settings screens) use the new look: an uppercase section label over one rounded card, with hairlines between rows. On iPhone and Mac they were Apple’s grey grouped panels on the navy background, and raised cards elsewhere. Rows are at least 44 px tall.
- Each Settings entry has an icon, and Sign out is a red row of its own.
- Buttons, text fields, switches and view toggles look the same on every platform: pill buttons, filled fields with a placeholder, the accent on switches, one pill toggle (Memory’s Short-term/Long-term no longer sits a Cupertino control next to Material ones). Approve buttons are no longer violet, and error and empty states use the new type.
- Dialogs look the same everywhere: a rounded card in the app’s colours, with the destructive choice (Delete, Deny, Stop sharing) in red. On iPhone and Mac they were Apple’s grey alerts; they still fade in the iPhone way.
- Sheets (the chat list, Memory target, filing a relationship, the place menu, the quiet-hours wheel) and their menus of choices draw on the same card, instead of system black or Apple’s grey action sheets.
- Short messages and Undo after an action all appear the same way.
- Text on iPhone and Mac is the same size as on Android and the web.
- Confidence in To review and the location badge in Your data have their own green and amber, readable in light and dark.
- Spinners, the loading and offline screens and the offline banner use the app’s colours on every platform.
Fixed: a bare year as a date lost a whole document’s memories (#350)
- daemon: when the extractor wrote a date as just a year (“2007”) or a year and month (“2007-05”), Postgres rejected it and every entity, observation and relationship from that document or message was lost. A year now reads as 1 January to 31 December and a month as its first to last day; a date that can’t be read is left off that one observation, and the rest is kept.
Fixed: the failed-dream notice was a solid red block
- The notice at the top of Memory when a dream failed sat on solid red, with its text and buttons hard to read. It is now a soft red wash with a thin red edge, and the text and buttons are readable in light and dark. The theme never set Material’s error-container colours, so anything using them fell back to plain red; they now come from the design system.
A failed dream no longer lingers after the key is fixed (#208)
- daemon: saving a new AI key dreams again right away when the latest dream failed, for the account and everyone inheriting its key. Changing an organization’s or a sub-account’s key now takes effect without a restart.
- daemon: dreams on a Claude subscription token send the OAuth header the API requires; they failed with “Invalid bearer token” before.
- daemon: the nightly hour in dream settings is honoured; it was ignored and every dream ran at 04:00 UTC.
- contract:
POST /api/v1/user/dreams/retrystarts a dream when the latest one failed. - flutter: the Memory tab’s failure notice has Try again, its buttons are readable on the red, and a rate-limited dream no longer tells you to get a bigger key when the provider was only busy.
- flutter: snackbars float above the chat composer instead of covering it.
daemon (history replay, #262)
- Replayed tool calls no longer break strict OpenAI-compatible hosts or
Gemini. Their ids are now nine alphanumeric characters (Mistral accepts
nothing else) and the call message carries an empty string rather than
null. A reply with tool calls that follows another reply, rather than the
user’s turn, keeps its
[tools used: …]line instead of structured calls, so model turns never sit back to back, which Gemini refuses. The tool-name lookup is built once per request instead of scanning per call.
Anonymous usage statistics with TelemetryDeck (#247)
- flutter: the app counts which screens and features are used (messages sent, voice, recordings, documents added, review decisions, exports), with the app version, platform and language, and sends the counts to TelemetryDeck (EU). Never content, never the account, nothing from an incognito chat; the counts sit under a random, hashed id for the device. On by default; Your Data → Usage statistics turns it off and forgets that id. Debug builds report as test data.
- Builds report only when
TELEMETRYDECK_APP_IDis set (repository variablesTELEMETRYDECK_APP_IDandTELEMETRYDECK_NAMESPACEfor the iOS, Android and web builds);flutter runand self-hosted builds send nothing. - site: the privacy policy describes the usage statistics and names TelemetryDeck among the services we share data with.
Export everything stored for you (#140)
- flutter: Your Data has an Export section. “Export my data” builds a zip of everything stored for you: memories, chats, documents, recordings and the skills you use. The app follows the export while it is built, and the download is kept for 7 days. The “no way to export” gap is gone; erasing the whole account in one step is still listed.
- daemon:
POST /api/v1/user/data/exportstarts an export in the background,GET /api/v1/user/data/exportreturns the latest one andGET /api/v1/user/data/export/{id}/downloadgives an hour-long link. The zip holds one JSONL file per table from one consistent snapshot, the stored files, enabled skills asSKILL.mdand amanifest.jsonnaming the schema. Stored keys, tokens, key material and embeddings are left out. A newer export replaces the older one. Migration0035_data_exports. - cli:
celindor export --user-id <uuid> --out <file>writes the same zip locally.
Import from Hermes, OpenClaw and ChatGPT (#140)
- flutter: Your Data → Import from another assistant. Choose a zip (or
an OpenClaw
.tar.gzbackup) of Hermes’s home folder, OpenClaw’s workspace, or a ChatGPT data export. The app shows what is in it: memory notes, what the assistant knew about you, daily notes, persona and skills always come in; conversations are listed unticked, and only the ones you choose are imported. A second import of the same assistant updates what changed and leaves out anything you deleted since. - daemon: each imported file becomes a document under
Imported/
(conversations and skills in their own folders). Ingest mines it as usual but stages what it finds as source_typeimport, shown as “Imported” in review, saved as third-party, and weighed more sceptically by the dream. Personas and skills are kept and searchable but never mined into memory. Files that may hold keys or tokens (.env,auth.json,credentials/, provider config, OpenClaw’s auth database) are never read, and the upload is deleted when the import ends. Hermes conversations come fromstate.dborhermes sessions export; OpenClaw transcripts are not imported. Routes under/api/v1/user/data/imports; migration0036_data_imports.
flutter: new look, first step (#278, epic #256)
- The app uses the Celindor brand palette and the Inter typeface on every platform, in dark and light. It replaces the default blue Material and Cupertino themes. Material widgets on iOS and macOS now use the same colours as the rest of the app. Inter is bundled, so it works offline.
- A design system in
lib/shared/design/(colour tokens, spacing, type scale) and the first shared components for the chat-first redesign: card, chip, composer frame, round button, activity line, section label and nav item. Screens adopt them in the next phases.
flutter: a home screen for new chats (#280, epic #256)
- An empty chat opens on a home screen instead of a blank list. It has a greeting by first name and time of day, starter prompts, and Today: up to three memories worth bringing back, from the resurfacing API that already existed (#44). “Tell me more” asks about one, and “Not now” cools it down. Incognito chats show no Today cards.
- On wide windows the message field sits under the greeting, as in the chat apps people know. On phones it stays at the bottom. The message field has the new look everywhere: rounded, bordered, with an arrow send button.
flutter: a sidebar on desktop and the web (#279, epic #256)
- Windows at least 1024 px wide get a sidebar instead of the tab bar or the rail. It holds New chat, the Spaces (Memory, Library, Recordings, and To review with a badge for proposals waiting), your recent chats with unread counts, and your name with the way into Settings. It collapses to icons, and the app remembers that.
- Phones and narrow windows open the same items from a drawer (☰ in each main page’s bar) instead of the bottom tab bar. Next to the sidebar, the chat’s bar drops its chat-list and new-chat buttons, which the sidebar has.
- Each page has its own address in the web app (
#/memory,#/library,#/recordings,#/review,#/settings,#/chat/<id>). A link or a reload opens that page or chat, and the browser’s back and forward move between them.
flutter: the chat reads like a chat app (#281, epic #256)
- Replies are plain text on the page instead of grey bubbles, in a centred column at a comfortable reading width. Your own messages sit in a navy bubble on the right.
- What the agent did with its tools is one line per step of the turn (“Used 3 tools”), which expands to the steps. While it works the line says “Using 2 tools…” with a spinner. This replaces one grey row per tool call and result. Steps read as what they did (“Searched memory”, “Read a document”, “Checked the calendar”), and a run of one kind is named by it.
- What Celindor picked up from the chat waits at the end of it: “I’d like to remember 2 things”, with Save all, Review (opens To review) and Not now. No more trips to the review list for the common case.
Cards under a reply (#282, epic #256)
- When an answer is about a specific person, document, recording or date, Celindor shows it as a card under the reply: the person (opens them in Memory), the document (opens Library), the recording at the right moment (opens it), or the date with Remind me, which asks Celindor in the chat to set the reminder. At most three a reply. Cards stay when the chat is reopened, and show in incognito chats too (they only point at memory).
- daemon: a
show_cardtool for the agent, which checks every id against your own memory. Stored on the reply in a newmessages.cardscolumn (migration 0035, which must run before this daemon starts). The contract adds an optionalcardsfield to chat messages (additive).
flutter: closer to the redesign mockups (epic #256)
- The message field is a two-row card under the greeting on wide screens and a slim pill in a chat. It has round buttons and a Memory on / Private chip that switches to the other kind of chat.
- A chat’s bar shows its name and “Memory on” or “Private” (tap to switch). Home has no bar beside the sidebar, and on a phone it shows only the memory chip.
- Search everything (⌘K, or Ctrl+K) finds chats and people or things in memory, or asks Celindor straight away. ⌘N (Ctrl+N) starts a new chat. Both shortcuts are shown in the sidebar, and Recent lists chats by name.
- Today has a To review card on wide screens. On a phone, Today shows compact rows (swipe one away for “Not now”) and a “new facts to review” link.
- The activity line names what happened (“Searched memory, read a document”). A person’s card shows who they are (“Contractor at Lund Bygg”).
- The memory card under a chat says Save both, Edit (change the wording before saving) and Not now.
- Follow-up chips: under the latest reply, Celindor can offer up to three
next messages, sent with one tap. daemon: these are
show_cardkind=suggestion, counted apart from the cards. The contract’sChatCard.kindgainssuggestion(additive: older apps skip kinds they do not know). - Numbered sources: a reply can mark a sentence [1], [2]… after the person, document or recording card it rests on. The number is a small badge that opens that source, and the card carries the same number.
- Closer still to the mockups:
- The message field keeps a send button beside the voice button on wide screens. On a phone, attach sits before the field and the voice button is white.
- “Memory on” and “Private” are two chips side by side.
- Deadline cards read “Sat 31 Oct” with “Remind me Friday” (the day before).
- Today’s buttons fit the card: “Help me plan” or “Help me decide”.
- A share button in the chat bar copies the conversation as text.
- A person’s card shows the latest thing remembered about them.
- A phone shows “Two things for today”, dot-only Today rows, the chat title alone, two wide buttons on the remember card (tap a line to edit it), and “Checked 3 sources”.
- Fixed: on iPhone and Mac, tapping an activity line or the collapsed sidebar crashed.
flutter: a light look in Celindor’s colours, and a light/dark switch (epic #256)
- The app now opens in a light theme built from the logo’s colours: deep navy text, white cards on a faint lilac page, the logo’s blue for actions and its violet for memory. The greeting, the send button and the voice orb carry the logo’s blue-to-violet sweep, in both themes.
- Settings → Appearance switches between Light, Dark and Same as device, at once, and the choice is kept on the device. Before, the app always followed the device and had no switch.
flutter: the redesign’s last pieces (epic #256)
- Memory → About you holds Me, Relationships, Instructions and Your data. They have moved out of Settings, which now covers the account and the assistant.
- The message field’s + offers Attach files and Record (it opens Recordings).
- Search everything also finds documents in the Library and opens them.
- Settings no longer shows the developer tools (migration plan, server address) in the apps people install. They appear only in developer builds.
- Every screen now takes its colours from the design system, so older screens (the chat list, the Library, Recordings, To review, Settings) follow light and dark like the rest. A test keeps hardcoded colours out.
flutter: a side panel for sources (#283, epic #256)
- On windows 1280 px or wider, opening a person, document or recording from a reply (its card, or a [1] citation) opens it in a panel beside the chat instead of leaving the conversation. Each opens in its own tab next to Sources, which lists everything the chat has pointed at.
- A recording shows when it was made and how long it is, a player, and the transcript around the cited passage, with that passage highlighted. Tapping a line plays from there. The audio loads only when you press play.
- A document shows its summary and the quoted passage, with Open and Show in Library. A person shows the same details as in Memory.
- The tabs stay while you switch chats. On narrower windows and phones, sources open full screen as before.
- Follow-up chips now sit under the “I’d like to remember” card, as in the mockup.
flutter: full-screen voice (#284, epic #256)
- Talking with Celindor now opens full screen: a blue orb that breathes while listening and pulses while it speaks, what it heard (or the reply as it is read aloud), and three buttons: the microphone, back to the chat, and end. Back to the chat keeps the conversation going in the voice bar, which has a button to return to full screen.
- This is the interface on the existing on-device voice conversation (#271). Speech still stays on the device; the realtime speech providers in #284 are a separate design.
Fixed: the place chip is back above the chat field (#306)
- The pin above the message field from Saved places (#255) was lost in a merge before it reached a release, so the only way in was Settings. It is back on phones.
Fixed: messages and Undo on iPhone, iPad and Mac (#305)
- Short notices at the bottom of the screen never appeared on Apple devices, and with them went Undo: after approving, rejecting or removing in To review, deleting a document, or emptying and restoring the Trash. Save confirmations in Profile, Password, Calendar and AI settings, and errors opening an attachment, were silent too. They now show on every platform.
Fixed: editing a relationship on iPhone, iPad and Mac (#307)
- The sheet for filing a relationship had no background on Apple devices and drew straight over the dimmed screen; it now sits on a card. The edit button next to each relationship is a full-size tap target.
Fixed: adding files in the Mac app
- Add files in Library did nothing on macOS. The sandboxed app lacked the entitlement to open files the user picks, so the file dialog never showed. Drag and drop was unaffected.
Saved places on the phone (#255)
- Celindor can know you’re at home or at the office. On iPhone and Android, a pin above the chat composer turns it on in one tap and saves where you stand under a name (“Home”, “Office”). Once on, it shows the place you’re at, or offers to save a new one; hold it to stop sharing or manage places, also found under Settings → Location. Each message you send then says which saved place you’re at, so you can ask “where am I?” or get answers that fit where you are. Off by default.
- Only the place’s name leaves the phone. The phone matches its position against your places itself; coordinates and the places stay on the device (in its keychain/keystore). The daemon shows the name to the agent for that one message, in its own block next to the recalled memory, and never stores it: not on the message, not in memory, not in logs. A message that waited offline for more than five minutes is sent without it.
- API:
POST /api/v1/user/messagestakes an optionalplace(at most 64 characters). Additive; older clients are unaffected.
App: talk with Celindor (#271)
- Voice conversation in the chat. The waveform button next to send opens voice mode: speak, and what you say is sent as an ordinary message in the open chat (so memory and tools work as usual); the reply is read aloud sentence by sentence as it streams in, and then Celindor listens again. Tap the microphone to end your turn early or to cut a reply off; End closes voice mode. Listening and speaking follow the app’s language (English or Swedish).
- Dictation. The microphone inside the message field types what you say into it, after anything already there; edit it and send as usual. Typing or sending stops it.
- Speech uses the device’s own recognizer and voice, so nothing new is sent to the daemon: iPhone, Android and Mac, and on the web in browsers with speech recognition (Chrome, Edge, Safari; not Firefox). The OS recognizer may itself use Apple’s or Google’s servers. The app asks for speech-recognition access the first time.
Fixed: conversation compaction follow-ups (#261)
- The newest message of a summarised stretch is archived too. It used to stay in the history as well as in the summary, and was summarised again the next time. Migration 0033 archives up to a message id instead of a timestamp.
- A failed read of a chat’s summary no longer replaces it. Compaction now stops for that turn instead of writing a summary of the newest part only.
- Exact references are the user’s own: the clock line in front of each message no longer fills the list with timestamps.
- Very long chats of short messages are summarised once they pass the 600 rows a turn loads, instead of the oldest being cut off silently.
- Without a summarising model the summary no longer grows with every compaction; it carries the last model summary once, capped.
- Mistral and other strict OpenAI-compatible hosts can summarise: the summariser no longer sends a token cap they reject.
- The app shows “Earlier messages were summarised” in the chat, which it never did because the marker arrived after the reply had finished.
daemon (prompt cache, #260)
- The conversation is a cache hit again from turn 2 on. Since #102 the
per-turn
<memory-context>block was removed from a message once it was no longer the latest, which changed that message’s bytes and made Claude re-write the whole conversation into the cache on every turn, at the 1-hour rate. The Claude path now puts a cache breakpoint on the message that ends the previous turn (and drops the one after the tool list, which the system prompt’s own breakpoint already covers), so each turn re-writes only the turn before it. A standing instruction that changes mid-chat is now told once, as a line on that turn’s message, instead of the whole list riding on every later turn. Also: an empty message keeps its placeholder next to the context block, and the frozen prompt’s logged hash is the hash of the text a one-block provider gets.
Fixed: connecting Google Drive on dev and test (#28)
- Connecting Google Drive failed on dev and test (“column provider does not
exist”). Calendar (0023) and Drive (0030) had both created an
oauth_statestable with different columns, and whichever migration ran first won. Drive now has its owngoogle_drive_oauth_states(migration 0033), which also repairs databases where Drive’s table had taken the name, there breaking Calendar’s connect instead.
Security: HTTP hardening (#266)
- Connecting Gmail or Microsoft mail now carries a single-use OAuth
statebound to the signed-in user, so a consent link started by someone else cannot be completed in your session. Admin’s callback pages pass it on. - CORS answers only the configured web origin (and, without credentials, localhost). Other sites’ pages can no longer read API responses.
- Every response carries
nosniff,no-referrerandX-Frame-Options: DENY, plus HSTS behind HTTPS. - Chat turns, uploads, transcription and re-ingest, and sign-in links are
rate-limited per session (or per address), answering
429withRetry-After. The limits are far above use by hand. - The login throttle and sign-in-link log believe
X-Forwarded-Foronly from the proxy, so a client reaching the daemon directly cannot pick a fresh address per attempt.
Security: threat model and a corrected privacy policy (#114)
- New
daemon/SECURITY.md: the three security boundaries (tenant isolation, encryption of stored secrets, the sandbox), the heuristics that are not boundaries, known gaps, and how to report a vulnerability. A test fails when a file or symbol it names disappears. - site: the privacy policy said memory is encrypted at rest. Only model-provider keys and connected-account tokens are. It now says so, in English and Swedish. ARCHITECTURE.md and CLAUDE.md are corrected the same way.
flutter (App Store)
- The iOS and macOS apps declare
ITSAppUsesNonExemptEncryption = false: they only use HTTPS and the system Keychain, which are exempt. TestFlight builds no longer stop at Missing Compliance waiting for the question in App Store Connect.
flutter (Google Play)
- Sign-in links open the Android app installed from Google Play instead of
the web app.
assetlinks.jsonwas missing Play’s app signing key, so Android never verified the app for dev.celindor.ai or test.celindor.ai.
Security: sandbox hardening (#113)
- A floor of commands the admin sandbox never runs, even when
exec_bashis always-allowed or the call was approved: recursivermof the root or a system directory (and--no-preserve-root),mkfsand its relatives, fork bombs, raw writes to block devices (dd of=,>,tee,shred), andshutdown/reboot/halt/poweroff. Commands are matched the way a shell would run them: quoted text is data (echo "rm -rf /"runs), whilesh -c,bash -c,eval,$(…)and backticks are unwrapped and checked, as is every segment after;,&&,||and|. A refusal tells the agent why and runs nothing. - Sandbox containers are capped at 256 processes, so a fork bomb cannot exhaust the host. They already dropped every capability and blocked privilege escalation.
- The dev-only local sandbox no longer hands the daemon’s environment to
commands: only
PATH,HOME, locale and similar variables pass, never the master key, database URL or provider keys. ItsHOMEis the sandbox’s own directory, so the login shell no longer reads the host user’s~/.profile(which could export those keys again) and commands no longer write into their home folder. Docker sandboxes never got the daemon’s environment. write_filein a Docker sandbox passes the path as an argument: a path containing a quote used to run as shell, past the floor.- Outbound network stays open for the admin sandbox, which installs packages; an egress allowlist needs a proxy and is left for a later card.
API changes
GET /api/v1/user/chats/{chatId}/messages
- added the optional property
messages/items/cardsto the response with the200status
GET /api/v1/user/data/export
- endpoint added
POST /api/v1/user/data/export
- endpoint added
GET /api/v1/user/data/export/{id}/download
- endpoint added
GET /api/v1/user/data/imports
- endpoint added
POST /api/v1/user/data/imports
- endpoint added
DELETE /api/v1/user/data/imports/{id}
- endpoint added
GET /api/v1/user/data/imports/{id}
- endpoint added
POST /api/v1/user/data/imports/{id}/commit
- endpoint added
POST /api/v1/user/data/imports/{id}/uploaded
- endpoint added
POST /api/v1/user/devices
- added the new optional request property
apns_environment - added the new optional request property
app_id
POST /api/v1/user/dreams/retry
- endpoint added
POST /api/v1/user/messages
- added the new optional request property
place
[0.6.1] - 2026-10-23
Security: safer fetching of outside URLs (#112)
web_fetch, ICS links and CalDAV servers share onesafeFetch(@celindor/security). The address that passed the check is the one dialed, so DNS rebinding cannot reach the daemon’s own network, and every redirect hop is checked again (web_fetchfollowed redirects unchecked).- More ranges refused: CGNAT, multicast, reserved, NAT64 and IPv4-mapped IPv6 in hex form. Cloud metadata endpoints are refused even where private addresses are allowed. A redirect to another host drops credentials.
web_fetchtakes an optional domain blocklist (blockedDomains); storing one per user comes later.
daemon (skills reach the agent, #97)
- Enabled skills now work. The system prompt only ever listed each skill’s
name and one-line description, and told the agent to “read SKILL.md”, but
skills live in the database and no tool returned their instructions, so a
skill never did more than its summary. A new
skill_viewtool (thecelindor-skillsserver, low risk, also in incognito chats) loads a skill’s full instructions on demand, and the prompt now tells the agent to load any relevant skill before acting. It only returns published skills the user has enabled, on every chat provider. New accounts’ default tools guidance points atskill_viewinstead of SKILL.md files.
daemon (context budget and compaction, #101)
- A long chat no longer forgets its own beginning. Each turn carried the
last ten messages; it now carries the newest messages that fit a token
budget (60k tokens by default, never more than half the model’s context
window:
[chat] history_budget_tokensandcontext_window_tokensin config.toml), which is a hundred or more messages in an ordinary chat. When the live history outgrows the budget, the oldest part is folded, in the background after the reply, into a running summary written by a small model on the user’s own provider (Haiku on Anthropic accounts, the chat’s model elsewhere): goal, constraints, progress, decisions, entities, next steps and critical context, updated rather than rewritten on later compactions. Exact strings from the removed messages (names, dates, URLs, amounts) and the user’s own words are kept verbatim next to it. The removed messages stay in the chat, in search and in memory extraction; only what the model is shown changes. A failed summariser call backs off (1, 5, then 15 minutes) and after three failures a model-free summary takes over. Migration0032_chat_compaction.sql.
API contract (#101)
chat-compactedon/api/v1/user/stream: the chat’s oldest messages were summarised (chatId,messagesCompacted,at). Additive.
flutter (#101)
- A subtle line in the chat when earlier messages were summarised. EN + SV.
daemon (conversation history, #100)
- OpenAI and Gemini chats see earlier tool calls as real turns. On those
paths an earlier reply’s tool calls reached the model as a
[tools used: …]line of text; they are now replayed the way the model emitted them: the call, a short result stub, then the reply’s text, exactly as on the Claude path. All three API paths build the history from one shared builder, so roles alternate strictly (same-role neighbours are merged, a chat the agent opened with a reminder gets a placeholder user turn in front) and no tool call is left without a result. The development-only Agent SDK path keeps its flattened transcript: the SDK takes user input only. - The agent no longer reads a replayed action as one that never happened.
Its instructions said a reply without a
[tools used: …]line did nothing and should be redone, which would have made it repeat real earlier actions (a reminder set twice) now that those replies carry real tool calls instead. The rule now names both forms and says an action whose call showsokhappened and is not repeated.
daemon (prompt caching, #102)
- Every turn after the first reuses the cached prompt. The system prompt
is now frozen for the life of a chat session and sent in two tiers: the
agent as authored (shared by all of a user’s chats) and the user’s data
plus this chat, each ending in a cache breakpoint on the Claude path,
with one more after the tool list and one on the latest message (1-hour
TTL for chats). What changes per turn no longer sits in the prompt: the
frequently used memories, and a standing instruction that changed
mid-chat, are attached to the user message as a
<memory-context>block that only the latest message carries, so earlier turns stay byte-identical. The hot memories are read fresh every turn instead of once per session. OpenAI getsprompt_cache_key= the chat id. The daemon logs each request’scache_read/cache_writetokens and the frozen prompt’s hash per chat, until the usage card (#106) shows them.
API changes
No changes to report, but the specs are different
[0.6.0] - 2026-10-22
site (privacy policy and terms, #178)
- celindor.ai/privacy and celindor.ai/terms are real pages now, in English and Swedish, without the placeholder banner. The privacy policy states the operator-access model decided in #218: operators see metadata only, conversation content is readable by support only while the user has granted time-limited access from the app, every such read is logged where the user can see it, and incognito chats are never readable. It also covers connected Google accounts (Gmail, Calendar) with the Limited Use disclosure Google’s OAuth verification asks for, where data is stored, retention via the Trash, and GDPR rights. The terms gain sections on the user’s content, support access and termination. These are the pages the Google Cloud OAuth consent screen points at.
daemon (memory recall, #232)
- The agent finds names and Swedish words in memory. Recall ranked by
embedding similarity alone, so an exact name or a Swedish word
(“Gästgivaregården”, “Anders”) was missed whenever the embedding was off.
Every recall now ranks by meaning and by keyword, fused: the keyword index
holds exact words plus English and Swedish word forms, and entities also
match on a partial name or a nickname. A memory found by only one of the
two is still returned, and the agent is told which found it (“keyword
match”). Without an embedding provider recall runs on keywords alone
instead of a substring match. Chat messages and recording segments carry
the same keyword index. Migration
0029_hybrid_recall_swedish_keywords.sql(restart the daemon after applying it: it caches function signatures).
Privacy: admins no longer see message text (#229)
GET /api/v1/admin/chats/{chatId}/messagesandGET /api/v1/admin/users/{id}/activityare now contract routes and return metadata only (id, chat, role,content_chars, time), never the text.GET /api/v1/admin/users/{id}/messages-searchis removed.GET /api/v1/admin/users/{id}/chatsno longer returnstitle: it is cut from the user’s first message, so it quotes them.message.createdevents on/api/v1/admin/streamcarrycontentCharsinstead ofcontent. The user’s own realtime stream is unchanged.- Admin console: the chat view shows role, time and length; the message search form is gone. Admin-only routes, shipped together with the console, so no client floor change.
API contract (calendar, #66)
- Calendar (#66):
GET /api/v1/user/calendar/accounts(connected calendars and which providers this server can connect),DELETE .../accounts/{id},GET/PUT .../accounts/{id}/calendars(which calendars in an account are read), andPOST .../oauth/{provider}/start+GET .../oauth/{provider}/callbackfor connecting Outlook or Google;POST .../accounts/link(a shared .ics link, read-only) andPOST .../accounts/caldav(iCloud or another CalDAV server).CalendarProvidergainsicloud,caldavandics; the OAuth routes take the newCalendarOAuthProvider. Additive. permission-requestedevents may carryaction(e.g.calendar.create, for the client’s own heading) andcanRemember: false(asked every time; offer no “always allow”). Additive.
daemon (calendar, #66)
- Calendar integration, part 1 (#66): connect Outlook (Microsoft Graph) or
Google from the app and let Celindor read it. The provider redirects to the
daemon, a one-time
statecarries the user; tokens are stored encrypted under the user’s DEK (calendar_accounts, migrations0023/0024, rotated with it). New@celindor/calendarpackage. The agent getslist_calendar_eventsandfind_free_time(read-only, in the user’s profile time zone, default Europe/Stockholm), over the calendars the user picked per account (the main one by default). Configure with[oauth.microsoft]/[oauth.google]and an optionalcalendar_redirect_uri. - Calendar (#66): iCloud and other CalDAV servers connect with an app-specific
password, and any other calendar (Proton, …) with its shared link (.ics,
recurrences and time zones expanded with
ical.js). Addresses the user gives are fetched only if they resolve to a public IP, redirects re-checked (safeFetch). Events now carry attendees and their answers (accepted, declined, tentative, no answer). Migration0025. - Calendar (#66): the agent books, moves and cancels meetings and answers
invitations (
create_/update_/cancel_/respond_to_calendar_event) in Outlook, Google and iCloud/CalDAV, in the user’s time zone, with invitations sent by the calendar itself (Teams or Meet links on request). Each one asks the user every time, with a readable summary; no stored grant oralways_allowskips it. Events listed to the agent carry a short ref to act on. Repeating CalDAV events are not changed from Celindor. With several writable calendars and none named, the agent is told to ask which one instead of booking. On the Claude path these tools are kept off the SDK’s auto-approve list (alwaysAskTools), which would otherwise skip the prompt. The system prompt forbids claiming an action no tool performed, and the daemon logs which tools ran (names only). - Calendar listings say which calendar each event is in, and the time in a booking confirmation is read in the event’s own zone (it was off by the UTC offset).
- iCloud/CalDAV events are written in the user’s time zone with a VTIMEZONE (built from Intl), not in UTC, so Apple Calendar and the invitation show local time.
- The #190 tool-call record now covers the OpenAI-compatible and Gemini
paths too: their turns store what they called in
messages.tool_calls, and their history shows[tools used: …]before each reply, as on the Claude path.
flutter (calendar, #66)
- Settings → Calendar (#66): connect Outlook, Google, iCloud, another CalDAV calendar or a calendar link (Proton and others), pick which calendars in each account Celindor reads, and disconnect.
- Booking, moving or cancelling a meeting, or answering an invitation, asks first with its own heading and the event’s details, and never offers “Allow & Remember” (#66).
- Ctrl+C / Cmd+C copies text selected in the chat on the web; before, the selection showed but never reached the clipboard.
Google Drive (#28)
- flutter: Settings → Google Drive connects a Google account read-only and then opens a folder tree from that account: tick the folders Celindor may use (a ticked folder includes everything inside it; folders above a ticked one are half-ticked). The library has two collapsible sections: Uploaded files, and Google Drive with the chosen folders, “Choose folders”, how sync is going and the documents it brought in. Removing folders or disconnecting asks whether to keep those documents (they stop syncing) or forget them and what Celindor learned from them.
- daemon: files in the chosen folders are ingested like files added by
hand (source
google_drive): Docs and Slides as text, Sheets as xlsx, and whatever else an extractor reads, images through OCR, up to 25 MB. A sync worker walks the folders first and then follows Google’s change feed every 5 minutes, at most 25 downloads per run. An edited file replaces what its old version taught; a file deleted or moved out in Drive puts its document in the Trash; a file the user deleted in Celindor stays out. Migrations0030_google_drive.sql(connection, folders, OAuth state) and0031_google_drive_sync.sql(sync state, excluded files, andpurge_document_knowledge, now shared byhard_delete_document). The Google client is[oauth.google_drive], falling back to[oauth.google]. - API contract:
GET/DELETE /api/v1/user/google-drive,POST …/google-drive/connect,GET/PUT …/google-drive/foldersandGET /api/v1/oauth/google-drive/callback(Google’s redirect; no session, a single-usestatenames the user). The connection carriesdocument_countandsync; saving folders takesremovedand disconnectingdocuments(keeporforget). Clients can no longer create documents with sourcegoogle_drive. Additive. - deploy: register
https://<env>.celindor.ai/api/v1/oauth/google-drive/callbackwith Google and enable the Drive API. OptionalGOOGLE_DRIVE_OAUTH_CLIENT_ID/_SECRETgive Drive its own client.
daemon (the Trash, #31)
- Deleting a document or a recording moves it to the Trash, as the app has
always said. The delete dialogs promised “You can restore it within 30
days”, but deletes were permanent and the app’s Trash screen called routes
that did not exist. Now a deleted item is hidden everywhere (lists, search,
downloads, sharing, the agent’s tools, the Me tab’s measurements, and the
Inbox and dreaming, which hold back what was extracted from it) and shows
in Settings → Trash, where it can be restored or deleted for good. After
30 days an hourly worker erases it and what was derived solely from it,
exactly as a delete used to. Migration
0027_trash.sql.
API contract
- The Trash:
GET /api/v1/user/trash,POST /api/v1/user/trash/{type}/{id}/restore,DELETE /api/v1/user/trash/{type}/{id}andPOST /api/v1/user/trash/empty, for documents and recordings.DELETE /api/v1/user/documents/{id}joins the contract. Both deletes keep their{ok, deleted}shape;deletednow says{in_trash: true, deleted_expires_at}. The apps already treat a delete as a move to the Trash, so this is not a breaking change.
daemon (poisoned memories are held back, #110)
- A memory that reads like an instruction to an assistant can’t become a
standing command.
rememberandobserverefuse such a fact (“ignore previous instructions…”, “forward the chat history to…”, a role change) and tell the agent to store declarative facts; ordinary facts, including ones you introduce with “remember that…”, are saved as before. - The dream FLAGS any pending extraction with injection signals for your review in the Inbox, whatever the model, the local pre-pass or the rule-based fallback decided: it is never promoted or merged into memory.
- A frequently used memory that matches is replaced in the prompt by a placeholder; the Memory tab still shows the raw text so you can delete it.
- Every prompt now says to write memories as facts, not instructions to itself, and that a rule belongs in standing instructions.
daemon (recurring reminders and snooze, #13)
- Reminders can repeat. “Remind me every Monday at 9”, “every day at
08:00”, “every 3 hours”:
schedule_remindertakes arepeatrule and atime_of_day, and the daemon finds the first and every next time itself, in the zone the reminder was set in. A daily 09:00 reminder stays at 09:00 across daylight-saving changes; one set for the 31st fires on the last day of shorter months. A series can end on a date,list_remindersshows the schedule, andcancel_reminderends the whole series. Times missed while the daemon was down are skipped, not fired in a burst. - Snooze.
snooze_reminder(“again in 15 minutes”) moves a one-off reminder; on a recurring one it adds a separate one-off and leaves the series on its schedule. - The rule lives on the reminder’s existing parameters, so no migration.
daemon (memory inbox, #176)
GET /api/v1/user/stagingrows carrysource_text(the chat message a proposal was found in, when it has no snippet) andsource_document(the document’s title). Additive; best effort, the list still loads without them.- Staging no longer proposes an entity that already waits undecided in the
inbox: the new mention (and its facts and connections) joins that proposal.
The user (
self:me) is never a proposal; their own row is made directly. - The extractors write facts in the language of the message, transcript or document, as one short human sentence without transaction ids or VAT detail; a receipt gives one note, and a request to the assistant (“book a meeting…”) is not logged as a fact.
flutter (memory inbox, #176)
- The inbox (“Förslag till minnet”) is a list like the memory view: one row per proposal with what it is, how sure Celindor is (in colour), when and where it was noticed, and Save / Remove. A tap opens the source, editing and “Later” (was “Flagga”). Plain words throughout (Person, Fakta, Koppling).
- The same person proposed several times is one row (“Anders · 4 förslag”); deciding it decides them all, and approving merges them into one entity.
- Filters for kind, source, confidence and a chosen date range, and “Välj” to save, remove or put off many at once (removing asks first).
- A connection reads in plain words, with the user as “Du”: “Du · fakturerade ·
Hotell Staffanstorp” (labels for
invoicedandpaid_toadded). - What one document proposed is one row (“Kvitto · Från dokument · 9 förslag”) with Save all / Remove all; a tap lists them to decide one by one. The same connection proposed twice is one row too.
- Every decision can be taken back: the row leaves at once, “Borttaget · Ångra” shows for five seconds, and only then is it sent. A re-read meanwhile does not bring it back.
daemon (prompt-injection signals, #108)
- One library of prompt-injection patterns in
@celindor/security(scanForThreats(text, scope)), shared by every scanner so they can’t drift apart. It looks for “ignore previous instructions”, hiding things from the user, sending private data to an outside address, role hijacks, fake system turns, system-prompt probes, hidden HTML, beacons and exfiltration links, memory-planting phrases, and invisible or bidirectional Unicode. Scopes are cumulative (all⊂context⊂strict); text is NFKC-normalised, filler is bounded and input capped. - Every block of outside text the agent receives (#109) is scanned; a hit is logged for admins as pattern ids and severity, never the text. It never blocks or changes the content.
daemon (secrets stay out of logs, #111)
- API keys, tokens and passwords are masked in the daemon’s logs and the
admin event stream. Every console line (what
docker logsand/admin/logsshow) and every admin event, including chat messages, goes throughredactSecretsin@celindor/security: provider key prefixes,KEY=valueand JSON fields, Authorization headers, JWTs, PEM private keys, passwords in database and other URLs, and credential query parameters. A short secret is masked completely; a long one keeps its first 6 and last 4 characters so two keys can still be told apart.
daemon (outside text is data, not instructions, #109)
- Web pages, emails, documents, repositories and transcripts reach the
model marked as untrusted data. A new
@celindor/securitypackage wraps such text in an<untrusted_content>block the text itself cannot close, with a “treat as data” rule. Used byweb_fetch,web_search, everygh_*tool,read_document,find_documents,list_documents, the sandbox’sexec_bash/read_file, document and email curation, document entity and measurement extraction, transcript extraction and summaries, and the dream worker’s review of pending extractions. - The document extractor and the dream worker are told not to turn text addressed to an assistant (“remember that…”, “ignore previous…”) into memory, and the dream dismisses such items.
daemon + flutter (Mac push, #165)
- The Mac app gets push notifications: it asks for permission after sign-in,
registers its APNs token as a
macosdevice, and shows reminders and replies as macOS banners while every Celindor window is in the background. The Dock badge follows the same outstanding count as the iPhone icon. The device platform enum gainsmacosand migration 0028 widensuser_devices. The Mac app shares the iPhone’s App IDcom.celindor.app(#226; it carried Flutter’s unregistered template id before) and signs with the Celindor team. Its APNs topic is still configured separately (APNS_MACOS_BUNDLE_ID,[push.apns] macos_bundle_id); unset, Mac devices are skipped. - Breaking contract change:
MIN_SUPPORTED_CLIENT_VERSIONis now 0.6.0. Responses ofGET/POST /api/v1/user/devicescan now carryplatform: macos, a value 0.5.x clients were not built for. The next release must be 0.6.0. Clients below it are logged asclient_too_old, not refused, whileCELINDOR_COMPAT_ENFORCEstays off.DevicePlatformis now declaredx-extensible-enum, so later platforms are additive.
daemon + flutter (tool calls in chat history, #213)
- A chat shows which tools the agent used, also after it reloads. The
“Using tool …” lines used to exist only on the device that watched the
reply stream; any later load showed the question and the answer with
nothing in between. They now come back from history, before the reply they
belong to (name, input, and whether it returned; results are never
stored).
ChatMessagedocumentstoolCalls, and the messages route sends only well-formed entries. Replies from before #192, and from the OpenAI and Gemini paths, have none.
flutter (each chat keeps its own draft, #36)
- Drafts belong to a chat. Unsent text was one draft for the whole account, so typing in one chat on the phone filled the composer of a different chat on the Mac. Now each chat has its own draft: it follows you into the same chat on any device, switching chats swaps in that chat’s draft, and sending clears only that chat’s. Text left unsent in an older build stays in the old shared draft and is not shown.
flutter (a stale “no API key” error no longer sticks, #236)
- A failed turn’s error goes away once the chat moves on. A chat started before an LLM key was set showed “credentials not configured” as a red bubble, and that bubble stayed, on screen and in the device cache, after the key had been added and later messages were answered. Now a failure notice is taken down when a key is saved or the provider is switched, and when the next message is sent in that chat.
flutter (chat no longer jumps when a reply finishes, #185)
- The chat stays put when a reply finishes. At the end of every reply the app reloaded the chat from the server and replaced all rows; the server keeps no tool rows and stores a reply as one message, so the “using tool” lines vanished and a reply written around a tool call merged into one bubble, and the view jumped. Now the rows on screen stay and only take the server’s ids. A message that arrived from another device in the meantime still triggers the full reload.
daemon + flutter (a failing dream is no longer silent, #208)
- Memories waiting for the dream no longer expire while it can’t run. A
pending extraction used to expire 30 days after it was written even if
every dream in that time had failed (a refused, rate-limited or empty AI
key), so everything said in that window was lost. Now it only expires once
a dream has completed after it. Needs migration
0026_expire_only_after_a_dream_ran.sql. - The Memory tab says when the last dream failed, and why: the AI key was
refused, rate-limited or out of credit (with a button to AI settings), or
something else (it will retry tonight). The note goes away after the next
dream that completes, and a failed dream in the journal gives the same
reason. The dream API adds
failure_kind.
daemon (episodes the agent can recall, #206)
- The agent can look up episodes, the trips, projects and periods the
nightly dream groups your memories into (“what happened on the Tokyo
trip?”).
recall_episodesexisted but was never offered. - Episodes made before the embedder was set up had no embedding and could never be found; the embedding backfill now heals them along with entities and observations.
- Editing only an episode’s title no longer re-embeds it without its type and description.
daemon + flutter (unread replies per chat, #203)
- The chat list shows how many replies you haven’t seen in each chat, as
a red count on the chat’s row. Opening the chat clears it. The daemon counts
them by the same rule as the icon badge (the badge is now their sum), and
GET /api/v1/user/chatsreturnsunreadCounton each chat. Needs migration0025_unread_counts_by_chat.sql.
daemon (badge for unseen replies, #189)
- The iPhone badge rises when a reply lands in a chat you are not viewing. Before, it only moved the next time some device read something. A reply in the chat on screen still leaves the badge alone.
flutter (a sign-in page that looks like Celindor, #211)
- The sign-in page is branded. It used to be a settings-style list with a generic chat icon, an email field stretched across the whole window and a near-invisible grey button. It is now a dark page in the logo’s colours, like celindor.ai: the logo, “Your private AI agent.”, one line on what Celindor is, and the sign-in form in a card that is centred and never wider than a phone. It stays dark in light mode too. The page that opens from an emailed sign-in link matches it. The copy no longer tells visitors their account “must be enrolled with the operator”.
flutter (Memory tab on macOS, #198)
- The Memory tab’s kind filter chips and sort toggle work on macOS (and the web). They are Material controls on a Cupertino page with nothing Material above them, so they failed with “No Material widget found” from the day the tab was added; they now sit on a see-through Material.
daemon (shared documents and listing documents, #199)
- “Documents shared with me” works.
GET /api/v1/user/documents/sharedalways returned 500: the single-document route was registered first and took “shared” for a document id. A malformed document id now gets a 404 instead of a 500. - The agent can list your documents.
list_documents— the exact listing thatfind_documentstells the model to use — was defined but never offered to it, so asked for “all my documents” it said it had no way to. A test now fails if a memory tool is registered but not offered (recall_episodesis the one deliberate exception for now).
daemon (dead push and Telegram leftovers, #99)
- A dream cycle that flags extractions or finds a serious timeline conflict
now sends one “needs your review” push, through the same push service and
devices as agent replies, honouring the flag toggle and quiet hours. The
code meant to do this per flagged row (
apns-service.ts) was never wired up, so no such push ever went out; it is deleted with its device table (user_apns_devices, migration 0024) and store methods. - The admin console’s “drop session” button ends the session it points at. It had been a no-op since the Telegram gateway was removed.
- Comments, a chat lookup and a tool description that still described the removed Telegram gateway are gone.
Repository (deploys reach the VPS again, #196)
- Dev deploys had failed since 2026-10-15 while every build passed: the VPS
was refused (
error from registry: denied) when pulling images, because it pulled with a GHCR token logged in once by hand. Each Deploy run (and the celindor.ai publish) now logs the VPS in with the run’s own token right before the pull, so there is no long-lived token left to expire.
daemon (Claude sees the chat as a conversation, #191)
- Claude accounts with an API key now talk to Anthropic’s Messages API
directly, and the model gets the chat as real turns instead of one pasted
transcript. A reply that used tools carries them as tool calls with a short
result (ok or failed — the result itself is never stored), so a reply that
claimed an action it never took no longer reads like one that did: a chat
that had kept “setting” reminders without calling the tool now sets them.
Default model
claude-opus-5-5(thechat_modelsetting still wins), with Anthropic’s server-side refusal fallback where the model supports it. Accounts with only a subscription token stay on the Agent SDK path, which is for development.[chat] claude_api = falseinconfig.tomlputs API-key accounts back on it too. - Reloaded history says when each user message was sent and which messages a reminder sent, for every provider — an earlier “it’s 08:23” no longer reads as made up, nor a fired reminder as one blurted out early.
- Tools with no arguments (
list_reminders,list_instructions) had a broken parameter schema on the OpenAI and Gemini paths; fixed.
daemon (no chat transcripts on disk, #193)
- The daemon no longer writes chats to disk in clear. The Claude Agent SDK
saves every query as a session file under
~/.claude/projects/unless told not to, and the daemon never told it: each turn left a file with the whole chat, every tool call with its full result (decrypted memory included) and the reply — outside Postgres, kept after the chat was deleted, and written for incognito chats too. Sessions are now never persisted (nothing resumed them; history comes from Postgres). Operators: delete the files already written —~/.claude/projects/of the user the daemon runs as, inside the container on each environment (or recreate the container).
daemon (actions the agent never took, #190)
- The agent no longer claims to have done things it did not do. A reply’s
tool calls were never kept: the next turn saw only the text, so “I’ve set a
reminder” without a
schedule_remindercall looked like any other reply, and once one slipped through the chat kept doing it — reminders were “set”, “listed” and “fired” with no row behind them. Each reply now stores the tools it called (name, capped input, ok or failed — never the results, which can hold recalled memory) inmessages.tool_calls, the transcript shows them as[tools used: …]before the reply —[tools used: none]for a reply that called nothing, so a claim without a call is visible next to the claim — and both the system prompt and a note after the user’s message say an action is only claimed after its tool ran. Replies stored before this carry no marker. A chat whose recent history is mostly such claims can keep repeating them; start a new chat. The flattened transcript itself is the deeper cause (#191).
daemon + flutter (the agent’s clock, #179)
- The agent now knows what time it is. Every message carries the moment it was written and the user’s timezone into the turn (the app sends the device’s IANA zone with each send; the profile’s timezone, then UTC, are the fallbacks), so “remind me in two minutes” no longer begins with the model working out the time itself, or guessing when it has no shell.
schedule_remindertakes a delay (in_minutes), computed on the daemon’s clock, besides an absoluteat; its reply andlist_remindersstate times in the user’s zone, so a wrong time is visible in the chat at once. A second identical call in the same minute returns the existing reminder instead of a second timer.- A timer the user set always speaks when it fires: the router picks the chat (the one the reminder was set in, when it still exists) and the wording, and a decline, error or missing credential falls back to a plain reminder. Every wake of the proactive agent now logs one line with its outcome and the router’s reason.
flutter (restore open chats, #64)
- The app opens on the chat you had in front last time on that kind of device. The daemon already kept the set of open chats per device class, and the app recorded opens and closes, but never read the set back: on launch it jumped to whichever chat the account was focused on anywhere. It now waits for the session snapshot and picks the most recently focused open chat of its own class (phones, including Android, share one; Macs another; web a third), skipping chats deleted since. Without one, it starts on the account’s active chat.
- Each device keeps its own chat. Switching chats on the iPhone no longer switches the Mac (or any other device) along with it, live or when the app comes back to the foreground.
flutter (push badge, #181)
- The iOS badge no longer flicks to 1 while you are reading a reply. The app used to publish its read position from a row only it knew — the local copy of a reply still streaming — which the daemon could not resolve, so it counted the reply as unread until the app was backgrounded. The read position is now always a message the daemon knows, it is never published mid-turn, and it moves to the persisted reply as soon as the turn settles.
flutter (copying from chats, #177)
- Text in chats is copyable the way the platform does it. A long-press on iPhone or Android now selects a word with handles and the Copy / Select All menu, where before it silently copied the whole message and blocked selection. That menu — and the right-click menu on Mac — also offers “Copy message”, which copies the whole message as written. Each message is its own selection, so Select All means that message. The error banner above the chat works the same way.
daemon (outstanding count, #170)
- The iOS icon badge now shows how many agent replies you have not seen, and
goes away when you read them. The daemon computes one outstanding count per
user from the read positions the apps already publish (
count_unread_messages, migration 0023): every push carries it as the badge, and reading a chat on any device sends a badge-only push that brings every iPhone down to the new number. Before, every push set the badge to 1 and nothing ever cleared it.
Repository
android.ymlno longer fails a run whose build passed because the runner ran out of disk while saving the Gradle cache (#167): the build job first removes the preinstalled toolchains it never uses, and Gradle caching moved fromsetup-javatogradle/actions/setup-gradle, which caches selectively and prunes unused entries before saving.
daemon (push registration, #161)
POST /api/v1/user/devicesworks again: a leftover handler from the removed Swift client was mounted ahead of the contracted route and rejected every registration withdevice_token required, so no iOS device could ever be registered and no push could go out. The leftoverPOST/DELETE/GEThandlers are gone; the contract routes are the only ones. The contract test now mounts routes in the server’s order, so a shadowing route fails it.
daemon (memory extraction)
- Memory extraction no longer loses a message when the model wraps its JSON in a ```json fence with a sentence before or after it. Only a reply that was exactly one fenced block was unwrapped before; anything else failed to parse and nothing from that message reached memory.
daemon (chat attachments, #168)
- An attachment’s size is checked in storage before it is read: the presigned upload never bound the size, and the daemon used to download the whole object before comparing it with the 5 MB / 20 MB caps, so a signed-in user could make it buffer an arbitrarily large file. The library copy of a paperclip file now records the size that actually arrived, and an oversize or never-uploaded file is not copied.
- The files re-sent with a conversation are bounded as a whole (16 MB encoded across the three newest file-carrying messages), not only one by one. Beyond that a PDF goes as its text and an image as a note, instead of every turn failing on the provider’s request limit.
- A message that is only files no longer sends an empty text part to Gemini or an OpenAI-compatible provider, which refused it.
saved_to_libraryon an attachment is the plan before the message is sent and the outcome after: a copy that failed shows as not saved. A GIF is never promised a copy, as document ingest cannot read one. The copy is made before the reply streams.- The wording that said chat attachments never reach the library or memory (changelog, contract, migration and code comments) is corrected: a paperclip file does.
API contract
GET /api/v1/user/chats/{chatId}/messagesis on the contract (MessageListofChatMessage, each with itsattachments). The route and its payload existed before; theattachmentsfield was added in 0.5.1 outside the contract. Additive.
flutter
- Opening a page over the tabs on iOS and macOS (an entity, a document) no longer throws “multiple heroes share the same tag”: every tab page has a Cupertino navigation bar, and the hidden tabs’ bars now sit out the hero animation.
- The Minnet tab no longer crashes on iOS and macOS. Its kind chips and list
tiles are Material widgets, and the Cupertino page scaffold had no Material
under it;
PlatformScaffoldnow provides a transparent one there, keeping Cupertino’s text style. - The chat attachment widgets are split to the file-size rule, and a pasted image’s filename is localized (“Image …” / “Bild …”) instead of always Swedish (#168).
- iOS push registration (#19): the app asks for notification permission on
first launch after sign-in, registers its APNs token with the daemon
(
POST /devices), and retries on the next launch if the upload fails — so proactive replies can reach a fully closed app. Requires theaps-environmententitlement (added) and the deployment’sAPNS_*secrets. - The composer no longer stays disabled after a reply fails (#238). A failed
turn’s
errorevent now ends the turn in the app, so the field unlocks at once; before, it waited for adonethat a resumed stream never sends, and only a reload freed it.
API changes
GET /api/v1/admin/chats/{chatId}/messages
- endpoint added
GET /api/v1/admin/users/{id}/activity
- endpoint added
GET /api/v1/oauth/google-drive/callback
- endpoint added
GET /api/v1/user/attachments/{id}/download
- endpoint added
GET /api/v1/user/calendar/accounts
- endpoint added
POST /api/v1/user/calendar/accounts/caldav
- endpoint added
POST /api/v1/user/calendar/accounts/link
- endpoint added
DELETE /api/v1/user/calendar/accounts/{id}
- endpoint added
GET /api/v1/user/calendar/accounts/{id}/calendars
- endpoint added
PUT /api/v1/user/calendar/accounts/{id}/calendars
- endpoint added
GET /api/v1/user/calendar/oauth/{provider}/callback
- endpoint added
POST /api/v1/user/calendar/oauth/{provider}/start
- endpoint added
GET /api/v1/user/chats
- added the optional property
chats/items/unreadCountto the response with the200status
POST /api/v1/user/chats/{chatId}/attachments
- endpoint added
GET /api/v1/user/chats/{chatId}/messages
- endpoint added
GET /api/v1/user/devices
- :warning: added the new
macosenum value to thedevices/items/platformresponse property for the response status200
POST /api/v1/user/devices
- :warning: added the new
macosenum value to theplatformresponse property for the response status201 - added the new
macosenum value to the request propertyplatform
DELETE /api/v1/user/documents/{id}
- endpoint added
GET /api/v1/user/dreams
- added the optional property
dreams/items/failure_kindto the response with the200status
GET /api/v1/user/dreams/{id}
- added the optional property
failure_kindto the response with the200status
DELETE /api/v1/user/google-drive
- endpoint added
GET /api/v1/user/google-drive
- endpoint added
POST /api/v1/user/google-drive/connect
- endpoint added
GET /api/v1/user/google-drive/folders
- endpoint added
PUT /api/v1/user/google-drive/folders
- endpoint added
POST /api/v1/user/messages
- added the new optional request property
attachment_ids - added the new optional request property
timezone
GET /api/v1/user/trash
- endpoint added
POST /api/v1/user/trash/empty
- endpoint added
DELETE /api/v1/user/trash/{type}/{id}
- endpoint added
POST /api/v1/user/trash/{type}/{id}/restore
- endpoint added
[0.5.1] - 2026-10-14
Repository
deploy.ymlposts each deploy’s result to Slack — failures always, successes untilSLACK_BUILDS_SUCCESSisfalse— and stays quiet about SSH timeouts thatauto-retry-deploy.ymlreruns.android.ymlnow builds everymainpush touching the app (debug-signed, against dev.celindor.ai) to prove it still builds for Android, and posts its results to Slack likeios.yml.
daemon (resurfacing policy, #23)
- A dismissed resurfacing pick now recovers: the cooldown clock starts at the last dismissal instead of the last showing, so one dismissal means a quiet spell (14 days, doubling per further dismissal), not a permanent once-per-window rate limit. Dismissals no longer inflate the shown-day count that drives novelty decay, and open-loop urgency is monotonic in the due date — a deadline a month out no longer outscores one due in three weeks.
API contract
GETandPOST /api/v1/user/chatsare on the contract.POSTtakes an optionalincognitoflag (#62), and the list leaves incognito chats out. Additive.GET /api/v1/user/surface/todayis now on the contract: memory resurfacing’s scored picks (SurfacePickwith per-signal breakdown) are typed and reach the generated clients. The endpoint moved from the legacy handler into the contracted memory-browser routes; its response is unchanged. Additive (#44).POST /api/v1/user/surface/{memoryType}/{memoryId}/dismisslets the Today tab dismiss a pick, which cools it down before it can surface again. Additive (#44).- The dream journal (#87):
GET /api/v1/user/dreamsandGET /api/v1/user/dreams/{id}are contract routes. The list is the journal (status, trigger, counts and the dream’s own summary); the detail adds the per-extraction decisions with reasons. Additive — the routes existed before but outside the contract, and their payload moves to the contract’s snake_case shape. - Client presence (foreground awareness):
POST /user/presence({client_id, is_foreground}, device fromX-Celindor-Device-Class) records whether this client has the app foregrounded, so the daemon can skip pushing to a user who is already looking at Celindor on some device. Additive. POST /api/v1/user/messagesis on the contract, with a new optionalcaptured_at(#16): a send that waited in an offline queue says when it was actually written, the message is stored at that moment (future times clamped to arrival), and theX-Celindor-Device-Classheader rides along as the message’s capture surface — so memory extracted from the message carries the real time and place. The app’s outbox sends its queue time. Additive.- Standing instructions (#148):
GET /api/v1/user/instructionslists them,PATCH /api/v1/user/instructions/{id}changes one andDELETEremoves it. Additive. - Chat attachments (#149):
POST /api/v1/user/chats/{chatId}/attachmentsreturns a presigned upload,POST /api/v1/user/messagestakesattachment_ids(the text may then be empty), andGET /api/v1/user/attachments/{id}/downloadlinks to a sent file. The message list carries each message’s attachments. Additive.
daemon
- Observations are stamped with when the fact happened, not when the dream
worker ran (#16). Promotion (both the inbox’s approve and dreaming’s promote,
personal and org) resolves
observed_atas: the extractor’s event time, else the fact’s validity start, else the source message’screated_at(the capture moment), else the pending row’s staging time. The inbox approve path also stopped droppingvalid_from/valid_until. The firstchain_of_custodystep now records which client captured the source message (surface), so “what did I note on my phone” becomes answerable. Resurfacing anniversaries, the dream journal’s story and temporal-anomaly detection all read these dates. - Memory resurfacing now learns from engagement (#44). The Today tab records
which picks it showed and which were dismissed on
retrieval_feedback(newresurfacingcontext), and the scorer reads that back: a pick’s novelty decays after it’s shown and a dismissed pick enters an exponential cooldown, so the same memories no longer resurface every day. Migration0018_surface_resurfacing_feedback.sqlwidens the feedback context check and adds anepisode_idcolumn so episodes can be tracked too. - Push suppression now keys on foreground presence rather than mere
connectivity: a proactive reply is pushed only when the user has Celindor
backgrounded on every device. Presence lives in the session-state store
(
client_presence, migration006) and is read viaisForegroundAnywhere, replacing theliveChatsForUserheuristic that kept a connected-but- backgrounded Mac/web client from ever being notified. - Incognito chats (#62): nothing from them reaches memory. The extractor skips
them, the agent gets no memory-writing or reminder tools (it can still read
memory and is told the chat is not saved), recall records no tier access, and messages
get no embedding or classifier record. They stay out of the admin console.
Migration 0018 adds
chats.incognito; a worker deletes incognito chats idle for an hour, in case the app could not delete one itself. - One person, one entity (#150). A human is always kind
person: every write path foldsmember,family,colleagueand similar intoperson, andcompany/organizationintoorg, somember:Olleandperson:Olleno longer become two entities. Org memory follows the same rule. A role is a relationship, not a kind or a predicate: “Olle is CEO of Celindor” isworks_atwith{role: "CEO"}, andmember_ofjoins the predicate vocabulary (migration0019_people_and_roles.sql, which also rewrites the old “use ‘member’ not ‘person’” line in stored memory rules). Dreaming can now merge a pending entity into one already in memory; its MERGE used to fail on every call. - Standing instructions (#148): “when I ask what time it is, always answer
‘kuckeliku klockan är 7’” now survives into every new chat. The agent saves
such a request with
add_instruction(and lists or removes them), and a “Standing Instructions” section in every system prompt lists them. An incognito chat follows them and can list them, but cannot add or remove one. A change reaches chats that are already open from their next turn (setSystemPrompton every session type). Migration0020_agent_instructions.sql. - Chat attachments (#149): images, PDFs, Word, Excel, CSV and text files sent
with a message reach the model — Claude and Gemini read images and PDFs
natively, OpenAI-compatible providers get images and the text of the rest;
Office files go as their extracted text. They stay with the chat and are
deleted with it; a file attached with the paperclip or dropped (not pasted)
is also saved to the document library, and so memory, never from an
incognito chat. Migration
0021_chat_attachments.sql.
flutter
-
Attach files and paste images in chat (#149): the paperclip picks files, Ctrl/Cmd+V pastes an image from the clipboard, and files can be dropped on the composer on web and desktop. They upload at once and show as chips with their progress; sent files appear above the message, images as thumbnails. A large pasted screenshot is scaled down to fit the 5 MB image limit.
-
Family only shows family (#150). The group is a list of kinship predicates rather than any
is_<x>_of, so a role likeis_ceo_ofno longer puts a colleague under “Familj”. -
Settings → Instructions lists what Celindor was told to always do. Tap one to change it, or remove it after confirming (#148).
-
Line breaks survive chat rendering (#24): a single line feed in a reply is shown as a line break instead of collapsing to a space, and the user’s own messages render as Markdown too — lists, bold, links and code look the same as in replies, on the bubble’s own colors, with headings capped at body size so a stray
#doesn’t shout. Copy still yields the raw typed text, and pasted Windows line endings are normalized on send. -
Incognito chats (#62): the struck-through eye next to “New chat”, in the chat’s title bar and in the chat list, starts one. The title says “Inkognito”, a note above the field says it is not saved and is deleted when you leave, and the draft stays on the device. Leaving it (another chat, a new chat or another tab) deletes it.
-
Dream journal (#87): short-term memory links to a journal of what Celindor decided to remember overnight — each dream’s story, what was saved, let go, merged or flagged, and a path into the Inbox for flagged items.
admin
- The dreams views read the contract shape of
GET /api/v1/user/dreams, which fixes the dream statistics rendering as blanks (the old hand-rolled types never matched the daemon’s payload).
API changes
GET /api/v1/user/chats
- endpoint added
POST /api/v1/user/chats
- endpoint added
GET /api/v1/user/dreams
- endpoint added
GET /api/v1/user/dreams/{id}
- endpoint added
GET /api/v1/user/instructions
- endpoint added
DELETE /api/v1/user/instructions/{id}
- endpoint added
PATCH /api/v1/user/instructions/{id}
- endpoint added
POST /api/v1/user/messages
- endpoint added
POST /api/v1/user/presence
- endpoint added
GET /api/v1/user/surface/today
- endpoint added
POST /api/v1/user/surface/{memoryType}/{memoryId}/dismiss
- endpoint added
[0.5.0] - 2026-10-09
Repository
- One repository for the daemon, admin console, public site and Flutter app, with each former repository’s history preserved under its folder.
android.ymlbuilds the Android app and uploads it to Google Play: release tags go to internal testing against test.celindor.ai; manual runs pick dev or test and a track.ios.ymlbuilds the iOS app (#74): everymainpush touching the app gets an unsigned build against dev.celindor.ai to prove it still builds; release tags are signed and uploaded to TestFlight against test.celindor.ai. Results post to Slack (successes untilSLACK_BUILDS_SUCCESSisfalse).
API contract
- Memory browser entities and graph neighbours carry an optional
access_tier(hot,warm,cold), andGET /api/v1/admin/users/{id}/memory/tiersreturns how a user’s memories spread over the tiers (admin only). Additive (#33). - The contract lives in
daemon/packages/contractand is published ascontracts/openapi.json; its version is the train version. - Health, version,
me, sign-in/session/password/claim and push-device routes are contract routes, with contract tests. - User stream (SSE) events are part of the contract.
- Every response carries
X-Celindor-Version; clients sendX-Celindor-Client-Version, and clients below the supported floor are logged. - Recording routes are contract routes. New:
GET /recordings/{id}/transcripts,PUT /recordings/{id}/transcript(corrections from the review view),POST /recordings/{id}/summaryandPOST /recordings/{id}/transcribe.GET /recordings/{id}also returns the summary, and the recording object gainstranscriptionStatus,summaryStatus,summaryShortandspeakerCount.POST /recordings/{id}/whisperis deprecated and never re-queues a recording that is queued, processing or done. - Memory browser routes (#54), read-only and paged with an opaque cursor:
GET /memory/facets(totals, kinds, predicates and where the graph starts),GET /memory/entities(filter by kind, last mentioned, substring search; recent or A–Ö),GET /memory/entities/{id},GET /memory/entities/{id}/neighbors(each neighbor once with all its relationships to the entity, capped, with per-kind totals),GET /memory/relationships,GET /memory/observationsandGET /memory/pending(short-term memory: extractions not yet promoted). Additive. - Resumable assistant turns (#7): turn events carry an optional
turnIdand a monotonic per-turnseq, and a newGET /user/turns/{id}/stream?since=<seq>replays a reply in progress and streams it to completion. The session snapshot listsinProgressTurns. Additive.
daemon
- Memory tiers are live (#33). Recalling a memory through the agent’s
recall_entities,recall_observationsorget_entitytools records an access, in the background so answers are never slowed: observations becomehotafter 5 recalls and entities after 10, and acoldmemory comes back towarm. A daily worker steps idle memories down,hot→warmafter 7 days andwarm→coldafter 90. Migration0017_memory_tier_activity.sqlcounts a memory’s last recall, mention or creation as activity, so memories that were never recalled are no longer treated as idle since forever (which would have sent every memory tocoldon the first run). A step down also resets the access count, so a memory has to be recalled again 5 or 10 times to becomehot:hotmeans used a lot lately, not once upon a time.coldis only a label: nothing is archived, and tiers do not change search ranking. - The agent starts each conversation knowing your most-used memories: the
hotentities and observations are added to its system prompt as “Frequently Used Memories”, so it can answer about them without searching first. Reading them there does not count as a recall. - Migration
0016_memory_browser.sql: indexes for paging the memory browser andpg_trgmindexes so substring search on names and text stays fast. - Resumable assistant turns (#7): an assistant reply is now a daemon-owned turn
(
assistant_turns,messages.turn_id; migration0017) with an in-memory replay buffer, so a client can attach to a reply in progress and resume it. Turns orphaned by a restart are failed on boot. - Fixed: the realtime SSE streams (
/user/streamand the per-turn resume stream) are now primed with a ~2 KB comment on connect, so Safari/WebKit — which withholds the first ~1 KB of afetch-streamed body — flushes events immediately. Without it, the web app on Safari never received live turn events (replies stuck on “thinking”; messages from another device invisible until reload). Chrome and native clients were unaffected. archiveObservation/unarchiveObservationnow check that the observation belongs to the user. No route called them, so nothing was exposed.- Typed-decision layer (
@celindor/decision, issue #44): aDecisionProviderseam (noul/choice/score) with a Claude provider (default, teacher, fallback), a self-hosted sidecar provider, and a router that picks a provider per decision site and falls back to Claude on low confidence. The memory extraction pre-gate is wired through it in shadow mode; verdicts and observed ground truth are logged todecision_shadow_logfor distillation. Off by default — a[decisions]config section opts a deployment in.celindor decision reportshows per-site agreement/latency/coverage andcelindor decision export extraction_pregateemits the labelled training set as JSONL; the sidecar HTTP contract is documented indaemon/packages/decision/SIDECAR.md. The proactive agent-router’s speak?/where? decisions are also shadow-logged through the layer (agent_router_speak/agent_router_where), with the real router’s outcome as ground truth; the agent’s message stays generative on Claude. The utterance classifier’sshapeis shadow-logged the same way (message_shape), with the classifier’s own shape as ground truth; referents, slots, permission, threshold and cascades stay on Claude. The document ingestiondoc_typeis shadow-logged too, with the curator’s type as ground truth; curated text, title, summary, tags and measurements stay on Claude. The dream-worker’s per-extraction PROMOTE/DISMISS/MERGE/FLAG verdicts are shadow-logged (dream_review, bounded per batch) with Claude’s decision as ground truth. With[decisions.gates.dream_review] enforce = truethe local head auto-executes confident PROMOTE/DISMISS and only escalates the rest (MERGE/FLAG and low-confidence) to Claude’s batch review; with no head it escalates everything, i.e. the full Claude review, unchanged. With[decisions.gates.agent_router_speak] enforce = truethe agent-router runs live: the decision layer gates speak?/where? and Claude is called only to write the message; the classic single-call router remains the fallback whenever the layer is unavailable or under-confident.celindor decision exportnow covers the message/document/agent-router sites (the router’s input is persisted in a newdecision_shadow_log.statecolumn since it can’t be rejoined from a source table); rows are written only whenlog_for_distillationis enabled. A reference decision sidecar (daemon/decision-sidecar/) implements thePOST /decidecontract: a pluggable embedder (stdlib hashing by default, sentence-transformers optional) plus small classifier heads trained fromcelindor decision exportoutput, soprovider = "local"can answer noul/choice decisions on the operator’s own box; untrained sites 404 and fall back to Claude. An optionalJevDecisionProvider(TypeSafe System One) can answer sites routed toprovider = "jev"when[decisions] jev_api_keyis set; off by default and always behind the Claude fallback, since it is a proprietary cloud. Two more “worth mining?” pre-gates can route to Jev or a local head:document_pregateruns before entity extraction on every curated document (so every synced email), andtranscript_pregatebefore each recording transcript chunk’s extraction call. Both shadow-log against what extraction actually found; withenforce = truea confident NO skips the Claude call and leaves a processing note. Both are exportable withcelindor decision export.celindor decision probe <text> [--provider jev|local]sends one yes/no question to the configured Jev key or sidecar and prints the raw verdict, to check the key and wire format before routing a gate to it. A user without a Claude credential still gets a decision router when Jev or a sidecar is configured, so provider-only sites (surface_rerank) work for them too. Memory resurfacing (/surface/today) gains an optional local-first reranker: ascoredecision reorders the rule-ranked picks when thesurface_rerankgate is active, and falls back to the rule order otherwise (a new capability — there is no Claude reranker to shadow). /api/v1/versionreportsversion,minClientVersionandenv.- The API bind address is configurable (
CELINDOR_API_HOST) for containers. state.dblives underCELINDOR_HOMElike the rest of the daemon’s state.- Sandbox containers are named per environment.
- Documents and recordings are stored in Hetzner Object Storage (Germany)
instead of MinIO on the VPS, in versioned buckets per environment. Bucket
names, region and addressing style are configurable (
STORAGE_BUCKET_*,STORAGE_REGION,STORAGE_PATH_STYLE). - Recordings are transcribed by Gemini into sentences with speaker, language and time, in Swedish, English or a mix, then summarized (key points and quotes with time and speaker, decisions, to-dos, participants). Meetings of several hours are split into overlapping chunks so no words are lost at the seams. Engines are pluggable, so a second one can be added to cross-check.
[gemini] vertex_project/vertex_locationsend recordings to Vertex AI in a chosen region (e.g. the EU). The daemon image includes ffmpeg.- A recording left in processing by a crashed run is picked up again.
- A retry that reuses a finished transcript still mines it for memories when
the earlier run stopped before that, and never mines it twice (new
recordings.memories_extracted_at). - Saving transcript corrections returns right away; search is rebuilt in the background, and only corrected sentences are embedded again. A recording stays searchable while that runs, and a failed rebuild leaves the old segments in place.
- Corrections are only accepted on a finished transcript, so a retry can no longer overwrite them.
admin
- A user’s Memory page shows the memory tiers: how many entities and
observations are
hot,warmandcold, the average recall count and the latest recall (#33). - The former celindor-web, reduced to desk and operator tooling: surfaces now owned by the Flutter app (chat, record, documents, me, staging, profile, permissions, notifications, signup/claim/onboarding) are gone.
flutter
- The app tells the daemon when Celindor is in the foreground (on resume/pause, plus a slow heartbeat while foregrounded), so you don’t get a push on your phone for a reply you’re already watching stream in on another device.
- The title bar has rounded corners, like the message field and the bubbles.
- The memory graph fits a phone (#75):
- Neighbors are drawn 30% smaller; the center keeps its size.
- Groups fold and unfold: tap a heading (“FAMILJ · 10”) to fold it into one bubble, tap the bubble to open it again. With more than three groups all start folded, with three or fewer all start open; a corner button folds or opens all, and the choice stays as you walk the graph.
- The menu above the graph (Korttids/Långtids, search, trail, kind filters, the center’s card) folds into one line with the center’s name, a step back and any active filters. It starts folded on phones, and the app remembers your choice.
- Graf / Lista sits next to Korttids/Långtids as two icons, which saves a row.
- Minnet marks the people and things you ask about most as “Ofta använd” (“Often used”): a small flame in the lists, on the node’s icon in the graph and a label on the entity’s page (#33).
- A new “Minnet” tab (#54), read-only. Långtidsminnet lists entities, relationships and observations with search, kind filters (Personer, Platser …) and sorting (Senast nämnda, A–Ö), and loads more as you scroll. An entity’s page shows its details, aliases, observations and relationships, and each relationship opens the entity at the other end. Korttidsminnet shows what is waiting to be saved and when it will be dropped, with a shortcut to the Inbox.
- Reopening the app or reconnecting mid-reply now recovers the part of Celindor’s answer already written, instead of showing it only once the reply finishes (#7). The app attaches to the in-progress turn and replays it from where it left off, with no duplicated or missing text.
- Långtidsminnet opens on a Graf view: one entity in the middle with a card that says who or what it is, and its neighbors around it in named groups (Familj, Arbete, Studier, Platser, Ägande), colored by kind and sized by how connected they are. Each neighbor says what it is to the center (“maka”, “pappa”, “arbetsplats”, “grundat av Johan”). Tap a neighbor to move there, follow the trail back, filter by kind, pinch, scroll-zoom and pan, or “Centrera grafen”. It starts at you (or the most connected entity), search starts it elsewhere, and an entity’s page has “Visa i grafen”.
- The loading and connecting screens show the Celindor logo, a blue-to-violet loading bar and a readable message instead of large red text. Spinners and the web’s debug load bar use the brand colors.
- Settings → About shows app and server versions.
- The Android app is
ai.celindor.android1, its Google Play package name, and signs release builds with the upload key. Sign-in links from test.celindor.ai open the app as well as those from dev. - Every request reports the app’s train version.
- Record: a Review view shows the summary and who said what and when, and lets you correct sentences, change who said them and name the speakers. An audio file recorded elsewhere can be uploaded for transcription, also in the browser.