Versionsanteckningar
Celindor släpps som ett gemensamt versionståg: apparna, servern och den här webbplatsen delar versionsnummer.
Versionsanteckningarna skrivs på engelska.
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.
[Unreleased]
[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
API Changelog 0.6.1 vs. 0.6.2
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
API Changelog 0.6.0 vs. 0.6.1
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
API Changelog 0.5.1 vs. 0.6.0
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
API Changelog 0.5.0 vs. 0.5.1
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.