Changelog

0.16.0, 2026-11-02

Optional AI help: describe what you need in a few words, upload a workbook, and get a model and an import to review. Nothing is sent anywhere unless you set it up, and nothing changes live data without a person.

Releases are numbered by plan, not by date, so a higher number can ship before a lower one. All releases

Full changelog

Released 2026-11-02. This release was not tagged in the repository. This is the changelog's own text for the release; its links to files point at those files at the tag.

The assistant and the agents: a person says a few words, uploads a workbook and gets the model and the import, with an AI assistant that runs as a Claude Code session through the engine's own capabilities and is limited only by what they can express. An AI proposal provider (Claude or an OpenAI-compatible one) with a send level and call records, targets for unmapped values, the assistant's instruction-driven tasks that design, check and load, a change of a workspace's model with its records moved, always a way out of a failed task, agent access (/mcp with read tools, changes prepared and submitted for approval, and natural-language search), an account of a model change, an account of what changed in a period, and notification digests with following a workspace. The contract change is additive. Upgrading from 0.12: the migrations run with python -m app.cli.database_upgrade (Upgrades) or at startup; the AI and MCP settings are in OPERATIONS.

Fixed (the 0.16 release review)

  • An assistant's refresh token no longer opens the API console's management token or the engine's own pages (it is no browser session).
  • A Claude Code session never writes live data: a batch or an import naming no change set is refused 403 for it, and the write unit refuses it again. An assistant task takes a draft or a change set on its word only when its own client made it, so discarding or expiring a task never deletes a person's draft; the grants a turn leaves behind are removed first and the turn's grants end even when it fails to start; the two roles the session's grants use are checked on every turn and a changed role refuses the turn (AI_ASSISTANT_ROLE_CHANGED).
  • Correct, accept, retry, answers and run call the assistant's checks again, as creating a task does. The person's words are the last command argument after --, so none is read as an option. The engine's OpenAPI read and the session's curl verify TLS unless the API address is on this machine. A container's session runs as a separate user (assistant, through sudo) that cannot read the engine's environment or the job files (R-F45).
  • A load of a design is refused 409 AI_TASK_CHECK_STALE when the draft changed after its check. A turn's words and start stay until the turn is recorded; a load turn a stopped worker left is undone before it loads again; a session's jobs that outlive the session's time are cancelled and undone; the task row is not locked while the workbook is copied and the OpenAPI read; a stop that undid nothing no longer says it did.
  • The agent access endpoint limits registrations per address, caps a registration at ten addresses of 2,000 characters, and re-checks a self-registered address against the listed hosts on every sign-in. Every call, a failed lookup included, is recorded, and tool and resource listings count against the rate limit. create_change_set finds the change set a retry already made.
  • An account of what changed names a job's file and links the job only for a reader who may see the job; two activations of one release are two facts; a statement may cite only facts the provider was sent; a provider's error no longer carries its address.
  • Migrations: a role of the instance's own called agent is kept under the next free agent-N key with (custom) in its name; only the engine's own shared assistant client is deactivated, not an integrator's of the same name.
  • Imports: a row finding its record by a value another row writes, and the merge of repeated keys, run as one pass each. A person deleting a row the assistant marked in a change set is named in its origin.
  • The assistant's task page: a stale check hides Load and says so; Accept follows Work in change sets unless the proposal moves whole types; installing a model needs the instance-level Model release; the offers need Bulk and import and job creation; one stream per task however many parts follow it; the Apply dialog never installs twice and lists every other workspace; the Follow button's name holds its visible word.
  • The deployment's compose.yml reads its settings from env_file: ${RDM_ENV_FILE:-.env} again; a 0.16 AP1 change had broken the file name the deployment runner passes, and the runner stopped.
  • The browser stand-ins for the assistant read the person's words after -- as the prompt, as the real Claude Code does (checked once with the real one: a prompt beginning --add-dir=/etc was read as words).
  • With agent access off, the protected-resource documents and /oauth/register answer 404 in the OAuth error shape (error, error_description) instead of a bare detail; J47's route tables now hold them and AP13's /proposal (R-F46, found by the J47 cases run on the release snapshot).

Added (notification digests and following a workspace, AO3)

  • A digest instead of many mails (0.16 AO3). On Settings › Notifications each kind of notice is mailed at once (what every kind stays until changed), in the digest or not at all, and the digest comes every morning or on Mondays at 07:00 in the time zone the browser reports; the page says when the next is. One message says in a sentence how many notices of each kind wait ("3 change sets wait for your approval; 1 job you started failed"), then lists up to 50 newest first with their kind, time and a link that opens the notice, and "and N more"; a job's line adds its kind and the rows applied and rejected while the job is kept. It names no record, change set, workspace, person or file (D432); there is no AI in it (AO-D32). Nothing new, or everything read before then, makes no mail; the sender's round makes the digest with the person's row locked and the next time moved in one commit, so two processes make one. GET/PUT /me/notification-mail take kinds as now, digest or off and a digest of every and timeZone (422 UNKNOWN_TIME_ZONE, 422 DIGEST_NOT_SET) and answer delivery per kind and digest.nextAt (pre-1.0: enabled per kind is replaced by delivery). Migration 20261123_01.
  • Follow a workspace (0.16 AO3, AO-D38). A Follow / Following toggle on each workspace card on Home (following on GET /me/workspaces) and Workspaces I follow, with Stop following, on Settings › Notifications; GET /me/followed-workspaces, PUT and DELETE /me/followed-workspaces/{workspaceKey} (404 for a workspace the person cannot browse). A change set published there, from an approved request or directly, puts a change-set.published notice ("A change set was published in a workspace you follow") in each follower's inbox who still holds Browse there, not the publisher's, written in the publication's transaction by one INSERT … SELECT. It is a kind like the others: at once, in the digest or no mail.
  • A digest's link opens the notice. /notifications?notice=<id> opens the notice's subject as a click does (a followed publication's notice opens the publication, while it is the active one) and marks it read; GET /me/notifications/{id} reads one of the caller's notices.

Added (always a way out, AP13)

  • A task is never left with only Back (0.16 AP13). A turn that failed shows its reason on the page (the task's own notes) and takes the person's words: POST /ai/tasks/{id}/model now works in every state a task can go on in (a proposal, a failed or stopped task, a saved layout's load stage, a loaded change set not yet published), and POST /ai/tasks/{id}/retry takes optional words. The same session goes on, or starts again on the same task, file and words when it never ran. The page's words box is one component (TellAssistant) wherever it appears.
  • Stop ends the turn, not the task. POST /ai/tasks/{id}/cancel now also works while a load runs. A stopped correction returns the task to its last proposal with a note (and a warning when the turn had already changed the draft); a stopped load discards the change set it was making, stops the jobs the session started and returns to the accepted proposal; a turn stopped before any proposal leaves a stopped task (a new state, migration 20261122_01) that takes words or Discard. POST /ai/tasks/{id}/discard is the one way to end a task and now also ends a failed, stopped or loaded one (a loaded task's change set goes with it).
  • A load that is wrong takes words. Words on a loaded task discard its change set (the page says first that any edits made in it go) and the same session loads again with them. The session's folder, transcript and client now stay until the change set is published or discarded, the migration confirmed, or the task discarded or expired (revises AP-D37's timing); the next read of the task ends the session, records a publication (result.published, result.publicationId) and ends a task whose change set was discarded elsewhere.
  • A published task points onward with Change this model… and Ask about these records…, the same offers as the workspace menu (one hook, useAssistantOffers, for both). An offer, and the Accept and Load buttons, appear only for a person who holds every group the steps need, Work in change sets included; POST /ai/tasks/{id}/accept refuses without it (403 FORBIDDEN) unless the proposal only moves whole types, and a person who lost it is told who can load, with the task's link to copy.
  • The smaller states. The check before a saved layout's load has Back to the proposal (POST /ai/tasks/{id}/proposal) and the words box; a saved layout that no longer fits offers Read it with the assistant; a migration the engine refused is shown on the task with the words box; a task that expired or never existed says so and leads to the workspaces. canTell on a task says whether the assistant can be told something now.
  • A refused step of a reshape is shown on the task. The next step that activates a release is checked with the release page's own check, and the engine's reasons appear above the words box. A loaded task recorded before AP13, whose folder went at its load, ends its session at its next read (canTell false) and offers Discard and the ways onward. The journey bar no longer offers Accept or Load to a person without Work in change sets.
  • A column no source reads is said before the proposal is accepted (found on the real walk-through). The proposal's checks name the filled cells of the tables a design reads that none of its sources covers (cellsLeftOut, e.g. a column under an empty header): the walk-through's workbook lost the Spanish names of two lists' entries without a word. design.checks[].kind gains cellsLeftOut.
  • A task's notes show the newest three and fold the earlier ones, and a task that goes on from a failure or a stop clears the reasons it stood on.

Added (a job's page says what it did)

  • A move's and a migration's job page (0.16). Settings › Jobs › one job shows a move's sentence and four counts, its record and relationship types (made, refused, not moved), and its items filtered by outcome and reason, refused first; names link to the records in the source workspace and a refusal by a model rule links to the model. A migration shows its counts and items by outcome. A job with nothing to list (an assistant task, a model draft's inference or check, a mapping proposal, an import profile's check) says what it belongs to and links to it only where the caller can open it; a mapping proposal job shows its tally. The move editor's report draws its rows with the same table (columns Item, What, Why, Fix); a move or migration job is titled "Copy into X", "Move into X" or "Migration" instead of "Batch". No API or database change: GET /jobs/{id} already carried the move and migration summaries.

Added (change the model from the workspace, and move the records with it, AP12)

  • "Change this model…" on a workspace's ⋯ menu (0.16 AP12). Where the assistant is on and the person holds Use AI proposals, Model authoring and Model release in the workspace, the menu opens the same dialog as Load a workbook with the workspace fixed, the words first and a workbook optional. The task page's title and crumb no longer say "Load a workbook" for a task with no file.
  • Two ways to move records between types. A whole type that becomes another (a rename, a merge, into another application) goes by one migration: the proposal's moves are checked against the draft with the migration's own plan, shown as a table of each type, what it becomes and the records it holds, and the task ends with result.migration instead of a change set; Preview the migration opens the migration page with those choices and Back to the proposal returns. Part of a type goes by the 0.8 restructuring in a change set on a release holding both shapes, and the task ends with result.changeSetId and result.thenReleaseId; the task page shows the three steps (activate both shapes, publish, activate the final release) one at a time. Records keep their IDs, values, relationships and history either way.
  • Prompt agent/5 tells a session that a record changes type and keeps its ID through a restructuring or a proposal's moves, and the endings to use. design.moves and result.migration and result.thenReleaseId are new in the task; result.changeSetId may be absent. A release named by result.migration or thenReleaseId is not retired with the task. No migration of the database.
  • A merge the consumers would notice no longer ends in a refused preview (0.16 AP12, fixed after the real try). The proposal is checked against the consumer contract as the migration preview and an activation check it: a draft that breaks a consumer API without raising its contractVersion goes back to the session (twice at most) and fails the task naming the API if it still does; the final release of a part-of-a-type reshaping and the installed release are re-checked likewise. The proposal says, for each consumer API whose version it raises, its name, the old and new version and what its consumers stop reading (design.consumerChanges, from the engine's diff). Prompt agent/6.
  • A merge the records would be refused for no longer ends in a refused preview (0.16 AP12, fixed after the second real try). The proposal's moves are checked against the workspace's live records by the migration preview's own check (every record, set work in PostgreSQL, nothing written): a draft that leaves the moved records no place under their parents goes back to the session with the engine's code, message, count per type and example names (twice at most), and fails the task with that reason if it is still refused; the installed release is re-checked on accept. The proposal shows what each moved type would lose (design.moves[].lost). Prompt agent/7.
  • A reshaping that keeps the old type no longer shows a step with nothing to do (0.16 AP12, fixed after the third real try). When the session's final release is the change set's own release (a split that leaves the old type beside the new), the task page shows "Two steps are yours" and no "Remove the emptied types" step; after publishing it says the workspace runs the new model, whenever the page is opened: the result carries changeSetReleaseId next to thenReleaseId, and the page compares the two.

Fixed (a removal in the model editor leaves nothing dangling, and every broken page is named)

  • Removing an application, a type or a relation binding takes the panels and views that depended on it. A relations panel on another application's page that listed a relation its page's type no longer has loses that relation (and the columns only the dropped types gave it), and goes when none is left; a mapping grid or relation matrix over a gone binding goes, with the navigation entry it leaves empty. The relation types and attribute types a removal leaves without a binding go too, released ones included, unless data holds them; one still bound stays even with no data (D31, the same rule for every removal, merge and parent change). One undo restores all of it; adding a relation is now undone by restoring what it changed.
  • The compiler reports every broken view and page problem at once (a view, a detail page header, each panel) instead of the first; other rules still stop at the first.
  • A panel issue leads somewhere. diagnostics[].element of the draft's outline names applicationKey, detail and panel; the issue links to the page's type and offers Remove from page, which works while the draft is invalid (Edit layout stays closed). The message is plain: "The Stock panel on the Bin page lists a relation Bin no longer has (…). Remove the panel or restore the relation."
  • A step that leaves the draft with more errors says so in a popup with the count and Show issues, whatever caused it (a step, undo, redo, a JSON edit, an applied proposal); opening an invalid draft does not. The Apply and Install dialogs' "Not valid" notice links to the Issues list.

Added (what changed in a period, AO2)

  • "What changed" above a workspace's Activity and on a record's History (0.16 AO2). Where a provider is set and the person may browse and holds Use AI proposals, Explain this period puts the changes of a period (7 days on a workspace, 30 on a record, or dates) into a few plain statements, each followed by numbers that lead to the facts it rests on. The engine counts the facts in the database over every change to live data: records per type and action, fields, relationships, sources (a job, a publication with who prepared and published it, or direct), people, change sets, withdrawals and model activations. Show them narrows the Activity, or the record's timeline, to the changes a fact counted; a fact also opens its job or change set. Names go to the provider only at send level values.
  • POST /workspaces/{key}/history/account and POST /objects/{type}/{id}/history/account, the purpose historyAccount (offered at every send level) and HISTORY_PERIOD_INVALID. GET /changes gains objectTypeKey, relationTypeKey, causeId, action, attributeKey and live, and a record's history gains live. The shared parts of the account moved to summaries.py and ui/AiAccount; the model change's account behaves as before.
  • history_facts, an assistant's tool (0.16 AO2, AO-D28). With Browse only, an assistant asks for the engine's facts of a workspace, or of one record (objectType and id), over [since, until) and an optional timeZone, and words them itself. It runs the account's own scope, period and facts, with no provider call, no ai_summaries row and no call record, and is refused as the route is: a workspace it cannot read, a missing record, HISTORY_PERIOD_INVALID. The agent ceiling is unchanged.

Added (an account of a model change, AO1)

  • "What this change does" in the activation dialog and in Apply (0.16 AO1). Where a provider is set and the person may activate releases and holds Use AI proposals in the workspace, pressing Explain this change puts the change into a few plain statements, each followed by numbers that lead to the facts it rests on. The engine writes the facts (each difference between the models, each rule the workspace's data would break with its count, what the consumer APIs would notice, whether activation is refused, the workspace's records per touched type, counted over the whole set); the provider only words them, is sent at most 40 of a kind with each kind's true total and no record, and is told never to judge. A statement with no source is dropped and counted; when the provider fails the facts stand alone with Try again. The same facts asked within the hour are answered without a second call. Nothing is sent until the person asks.
  • POST /model-releases/{id}/impact-summary and POST /model-drafts/{id}/impact-summary (the second where model drafts are), the purpose impactSummary (offered at every send level), the table ai_summaries (migration 20261121_01, an hour's cache) and IMPACT_SUMMARY_DRAFT_INVALID. Use AI proposals now also covers summaries.

Changed (a hint beside the relationship panel's heading)

  • Why a record is not offered a relationship ("Only … can have this relationship.") is no longer text under the panel's toolbar: a small blue circled "i" beside the panel's heading shows it on hover, on keyboard focus and on a tap, one reason per line. The hint is a shared InfoHint; it appears only when the reason applies to that record.

Changed (the model's language in the API, AP11)

  • The API's OpenAPI document now describes the model: the model field of model requests and responses names EngineModel and its definitions. Validation is unchanged, so a wrong model still gets the compiler's diagnostics.
  • An assistant session's log is kept beside its folder rather than inside it, and goes with the folder.

Changed (one path for the assistant, AP10)

  • A session builds everything the words ask for, as they place it and also with no data in the file; a question no longer replaces building.

  • Switching workspace from a page the other workspace's model lacks now writes the start page it opens to the address, so a reload no longer says the page is not in the model.

  • A session no longer stops with an internal error when its stream carries a system event whose message is text, as a refused permission does.

  • An assistant check counts a row skipped only when a value on it is lost: an empty cell in a list's column no longer reads as a skipped row, and unchanged counts records, not links.

  • AI_AGENT_COMMAND alone turns the assistant on. POST /ai/tasks is 409 AI_NOT_CONFIGURED without it and no longer needs AI_PROVIDER, which serves mapping targets and later features only. GET /ai/status answers assistant; Settings › AI says what each setting gives and opens if either is set.

  • The rules-only task path is removed: POST /ai/tasks/{id}/plan, plan on a task, table on GET /ai/tasks/{id}/changes, the relationship rules (design.links, check.links, result.linksJobId), AI_MAX_STEPS and the provider purpose workbookLoad, with their Reading and Check pages. A saved layout that still fits takes the engine's path with no AI.

  • A load leaves a saved profile. The engine saves it from the change set's imports (result.profileKey), checked against the change set's release, or says why it did not (result.profileNotSaved): a source with no key, a change made only through batches, a profile already saved.

  • A proposal warns of new records beside held ones (design.checks kind besideHeld).

  • A check tells skipped rows and an old check apart: check.summary.skipped no longer counts as refused, and check.draftChanged says the draft moved after that dry run.

  • A saved layout fits a workbook with sheets its profile left out, if the earlier file had them too; its summary gives the profile's own figures.

  • A session is refused a draft another task holds (409 AI_TASK_DRAFT_HELD), and unused releases a task installed are retired when it loads or is discarded.

Added (a key of several columns, AP10)

  • SourceKey.attributeTypeKeys in an import profile: two or more attributes, each bound by a column, whose values together find one record, live and in a change set. A combination two records hold refuses the row (KEY_AMBIGUOUS); an empty cell is a row without a key (MISSING_RECORD_KEY, or rowsWithoutKey); an unbound attribute is KEY_NOT_BOUND. The assistant gives each source such a key. Batch upsert keys are unchanged.

Added (who did the assistant's changes, AP9)

  • Provenance on a session's changes: a record or relationship the assistant proposed carries origin (who asked, who edited it afterwards, the task) in the change set and, once published, in history, removals included. ProposalOrigin gains editedBy, editedByName, proposedByName and taskId.
  • One client per task: ai_tasks.client_id, client kind session, left out of GET /clients unless kind=session; 409 CLIENT_HAS_NO_SECRETS.
  • The call record holds a session's runs: purpose assistantSession, costUsd, taskId, failure kinds timeout, cancelled and interrupted; Settings › AI shows them with their cost.
  • Retention removes a task's folder and Claude Code's transcript with the task.

Added (changing a workspace's model)

  • Edit model from the workspace: a ⋯ menu beside the workspace selector, and a button on the workspace's page in Settings › Workspaces, open the model editor on that workspace's draft. Apply to this workspace says what changes and what happens to the records, installs, then activates or migrates there alone, and returns to the workspace. POST /workspaces/{key}/model/draft; a draft's workspaceKey. Model release in a draft's workspace now allows installing that draft.

Changed (Import and Export)

  • Import and Export moved from the collection's header into the workspace's ⋯ menu. Export still exports what the page shows.

Fixed (the assistant's pages)

  • Load a workbook is offered only where a session is set up (ai.assistant on GET /system, AI_AGENT_COMMAND), and says what the session is given. With a provider alone, Import is the ordinary dialog.
  • The proposal's correction box is three lines, not 440px. The loaded page no longer says a session's changes note the assistant, which they do not yet.

Added (asking a list in words)

  • Ask a table list in words (0.16 AG4). Where a provider is set and the person holds Use AI proposals, a Search | Ask switch sits at a table list's search box. Pressing Enter on a question sends it (nothing is sent as you type; Cancel stops it); the list shows the filters it became as ordinary pills, with "Your question became these filters. Change or remove any of them." A part the list cannot express (an order, who changed a record, a record that is not found or is named by several) is named and nothing is applied: the proposed filters are shown as dashed pills with Use these filters. A failure is one sentence and the words stay. The provider is sent the question, today's date and the list's fields, every relationship the type has (declared by the view or not), and, at send level samples or values, the record names of a relationship's types when they hold at most 50 records, so it can choose records by meaning ("above 100 km" from "120 km" as one eq filter each); at structure no record is sent. A filter on a relationship the view does not declare is drawn as an ordinary pill and kept in the page address. POST /workspaces/{key}/query-translations answers directly; new codes QUERY_TRANSLATION_FAILED, QUERY_TRANSLATION_VIEW_UNSUPPORTED and VIEW_NOT_FOUND; no new setting.
  • List filters: under an ancestor, and the stamps. filter.related:<key>.under=<record ID> and .notUnder=<record ID> keep the records with some, or none, of their related records below a record at any depth (live, in a change set, in a publication; the query and an export say the same with ancestor:<type> inside a related hop). The filter bar's relationship editor offers "has none under" and "has any under". createdAt and updatedAt filter every live list without being declared, and a field with two conditions (a date range) shows a pill for each.

Added (agent access)

  • An assistant can prepare a change (0.16 AG3). With Work in change sets, an assistant creates a change set, adds and changes records and relationships, sets a field's value, reads the validation and submits it for approval, in change sets its own person or client created and never a colleague's. It cannot remove or clear anything, withdraw, discard, batch, restructure, approve or publish; change reaches live data only when a person approves it. A refused call tells the assistant the engine's code and details. Its rows say "Prepared by an assistant" with the name it gave itself (origin of kind agent), and channel mcp is on GET /changes and a record's history for what was done through an assistant. changeSetId joins find_records, read_record and describe_type. No new setting.
  • An assistant can read (0.16 AG2). A signed-in assistant or agent client can look up a value through a tool generated from each consumer API, list a mapping view's unmapped values, find and read records with their relationships and history, read a workspace's changes, and read the model as tools and as resources. New setting: MCP_ANSWER_BYTES.
  • An assistant can reach refiroo (0.16 AG1). With MCP_ENABLED, an assistant that speaks MCP connects at /mcp. A person adds the address and signs in on refiroo's own page, which says who asks and where it returns; a system uses an agent client. It works within what its person or client may do, cut down to reading and preparing changes, and can never approve or publish. Two tools so far: who it acts as, and the workspaces it may read.
  • Settings › Security › Agent access shows how to connect, the agent clients, the assistants people sign in with and each call, with argument names and never their values. Blocking an assistant or deactivating a client stops it on its next call. People see their assistant among their sessions.
  • API: /agent-access and its lists, kind on clients, application on sessions, mcp on GET /system, /.well-known/oauth-protected-resource, POST /oauth/register. Settings: MCP_ENABLED, MCP_RATE_PER_MINUTE, MCP_OPEN_REGISTRATION, MCP_REDIRECT_HOSTS, MCP_CALLS_DAYS.

Fixed (the AI import)

  • The same words give the same kind of model: a session separates what a workbook repeats into types of its own and raises it as a question if it keeps it flat; what the workspace holds decides whether it adds to the model or may start anew. The proposal shows attributes and records per type from the engine's figures, counts relationships apart from records, and warns when a model leaves out types holding records.
  • A session's own jobs run: an assistant's session held the only job worker while it worked, so a job it queued, such as profiling its workbook, waited until the session ended and then failed because the session's grants had ended too. Sessions now run on an assistant worker of their own (reported as assistantWorker among a process's threads), and their grants stay until the jobs they queued are done.

Fixed (opening a record from another application)

  • A record opens where it stands: opening a record from an application that lists its type but does not hold it, such as from a matrix row, now goes to the record's own application. It said "This object belongs to another application" before.
  • The session's checklist shows what happened: a step was ticked as soon as a command mentioned its address, so "Model changed" and "Every row checked" appeared before either happened, and a refused load counted as loaded. Steps now come from what the engine recorded: a check counts until the model changes again, a running one shows as in progress, and a turn lists only what it can reach, the model and its check first and the release and the load after acceptance.

Fixed (relation matrix cells)

  • A matrix cell shows the view's cell fields: a cell now reads the first of "Fields in a cell" with a text or number value, and the record's name only when none has one. It always showed the name before, so records named by a code showed the code.

Added (model editor, relation matrices)

  • Add a relation matrix in the editor: an application's panel has a Views section listing its views. "Relation matrix" there opens a dialog offering only rows, cells, columns and actions that can work, with a preview of the shape chosen. The types the matrix needs are listed in the application in the same step; their records stay where they stand. A matrix can be changed, renamed and removed, and a removal takes along the types only it needed.
  • List a type in another application: listObjectType and unlistObjectType, and "List an existing type" in the editor. A released listing is not taken away without a migration. removeView takes unlist; the draft's outline gives each application's views and unlistable.

Changed (model editor, released elements)

  • A released element nothing uses can be changed: a removal or other draft-only change applies when it would affect 0 records, values and relationships in the workspaces running the model and their change sets; above 0 it is refused with its impact, and activating the release checks the data again. A released key stays fixed whatever the data, and the refusal now says so.

Changed (model editor, choosing a relation)

  • Types and relation types sharing a name can be told apart: in the model editor's pickers, a type whose name another type shares shows the applications listing it, and its key where those match too, with a note that the two hold separate records. A relation type shows the types it stands between; those already reaching the chosen type come first, and a note says how it reads in reverse, which pairs use it with their rules, and what choosing a new one leaves out. The draft's outline gives a relation's uniquePer where it has one.

Added (0.16 AP6, the assistant designs and loads)

  • The assistant is a Claude Code session: with AI_AGENT_COMMAND set, the person's words, the workbook, the model and the OpenAPI document go to one session working through the API. It stops at a model draft with every row checked; the person opens it in the model editor, answers or says what to change, and the same session goes on. Accepting installs one release, unactivated, and loads a change set; activation stays with publishing. The page shows what the session has reached while it works, and has Discard and Close. The turn loop, its answer format and unpivots are gone. A saved layout that still fits needs no AI; key.storedUnique keys a source by an attribute whose stored values are unique.
  • Guards on the assistant's session, for sessions refiroo starts only: the operating system's sandbox holds it to its folder and this engine, it has only file and shell tools and none of the server's secrets, and its client cannot publish, activate or reach another workspace. A stopped session can be tried again. The engine image holds Claude Code (AI_AGENT_SANDBOX=container).
  • Imports reach every column of a repeated heading: where a header repeats a name, the first column has the name and each later one the name with its place, Currency #2.
  • Words alone start a session: a task without a file no longer stops with an internal error.
  • An empty change set is no load: the task fails with what the session said, and can be tried again.
  • An import's request sent as a file part is checked against the workspace it names, not the default one.
  • One session per task: a session left running by a worker that stopped is ended before its job goes on, so two no longer work in one folder.
  • Conflicting values are kept: the session is told that a workbook's conflicting names or values are intended: it keeps them as they are and may raise them as a question.
  • Switching workspace: a page the other workspace's model lacks no longer shows "not in this workspace's model"; its start opens. An address asking for that page still says so.
  • The assistant designs the load: with AI proposals offered, a task's assistant decides how the workbook lives in the workspace — an update, additions, a new relation, something new but related, something totally new — and changes the model where the data needs it (attributes, types, applications, relations) in a model draft of the task's own (draftId). It reads cells, values, stored records and model-aware queries; writes data sheets of its own for rows the workbook's shape cannot load; and asks questions with an assumed option (POST /ai/tasks/{id}/answers). Every row of the working copy is checked in a dry run against the draft, and refused rows go back to it within AI_MAX_STEPS calls. POST /ai/tasks/{id}/run installs and activates the model change first (needs Model release), then imports into one change set and saves the profile (result.releaseId).
  • Words alone: POST /ai/tasks without a file takes an instruction; relationships where a condition holds (design.links) are counted in the database and written as one background batch into the change set (result.linksJobId).
  • GET /ai/tasks/{id}/changes?source= lists a design sheet's changed records; the design check shows them behind a button per sheet. The check counts rows skipped for an empty key (skipped, refused as ROWS_SKIPPED when most of a source's rows are).
  • CARDINALITY_VIOLATION names the relation and the end that already holds one (message and details).
  • Import profiles: a related record's parent read from a column may name a valueMap (parent.valueMap), so a language word in a translations sheet finds the language record. The assistant's working copy keeps the formulas' last values.
  • Imports: a record: "existing" source finds a record by the value another source of the file writes into its key attribute, whatever that source is keyed by. The assistant is given every model-draft operation kind (such as setAttributeTypeFeatures to make a stored attribute a key).
  • Loading the same file again with "save the import" no longer fails with IMPORT_PROFILE_EXISTS: a key holding a profile of the same model gets its next version. A profile that cannot be saved is a warning note on the task; the load, already committed, stands.
  • A record edited in a change set (an import's update) no longer re-checks the relationships it already holds against their candidate criteria: as live, an existing relationship is kept whatever the criteria say now (D175); the dry run and the load now agree.
  • A design whose dry run cannot run (the file does not fit the profile, such as a header on row 2 under a row of group headings) no longer fails the task with "The reading failed": the problems go back to the assistant to fix. Each round's refused rows by code go with the design so far.
  • Imports: rows repeating a key no longer conflict where one leaves a cell empty that another fills (the record takes the filled values); rows of two sheets keyed alike by an attribute are one new record instead of two that break the unique rule.
  • The assistant's loaded page lists only the sheets and columns the design leaves out (no longer the rules' first reading as "something new, not loaded"); the shown model is read again once a load activated a model change; the release it installs is numbered after the newest one.
  • The assistant proposes the model first (AP6r): a task stops at a proposal — the model in plain sentences per type, the engine's design checks of any workbook (stored records to reuse, columns that group the rows, translations left out, relationships one end cannot show) and the questions — before anything loads. The steward corrects it in words (POST /ai/tasks/{id}/model) or accepts it (POST /ai/tasks/{id}/accept); the load stage then maps the sheets and checks every row. The assistant may use every model-draft operation, the page kinds included.
  • The assistant describes how a wide sheet (one group of columns per brand, list or period) becomes rows (unpivots) and the engine writes them into the working copy.
  • The assistant's design lists each column no source of its sheet loads with the reason it gave (design.leftOut; the loaded page shows it), and is told to reuse the records the workspace holds, to put rows under the column that groups them, and to load every translated column (workbook-design/5).
  • The import's journey bar: below the top bar, an assistant's load shows its steps (file and words, the design, the model change, questions, the check, the load, review and publish), what to do now and the next action, on the task's page, in the model editor opened from it and on its change set (?journey=ai-task:<id>). Close in the editor returns to the task; Stop, Start again and Open the change set are on the bar.

Removed (0.16 AP6, the reading turns)

  • The assistant's read, match and fix turns of AP6a and the task phases asking, matching and fixing: with the assistant offered a task is designed; without it, read by the rules and planned by the person as before.

Added (0.16 AP6a, loading a workbook with the assistant)

  • Import with the assistant: with AI proposals offered, Import opens the assistant first ("Import without the assistant…" opens the manual import). POST /ai/tasks takes a workbook and a workspace; a job (kind aiTask) reads it: a stored import profile that fits the file is followed, other tables are read by the rules (an update of a type when most of a column's values are its records' keys or names), note and title rows are set apart, and the assistant checks the reading and matches values by meaning. The task page shows what needs the person (new keys, names that differ, missing records), the rest grouped by the model; POST /ai/tasks/{id}/plan checks every row in a dry run, the assistant proposes value maps for refused values, and POST /ai/tasks/{id}/run loads into a change set, optionally saving the import profile. Visible to its requester and the workspace's administrators; kept AI_TASK_RETENTION_DAYS; at most AI_MAX_STEPS provider calls a job.
  • A list of records the workspace does not hold yet is read as new records of its type, keyed by the column the assistant names (prompt workbook-read/2). An answer that only repeats an operation the engine refused ends the assistant's turns. The task keeps only the assistant's last note, and refusals name the sheet and the column's header.

Removed (0.16 AP6a, the AI naming helper)

  • The AI naming helper of New from a workbook and Propose again is removed ("Also ask the AI assistant for clearer names", the inference's assistant option and read fields, phase and assistantOutcome, the purpose modelNames and the proposal pattern assistant): the inference runs on its rules; the workbook assistant replaces it.

Changed (0.16 AP1, the naming helper sees the whole file)

  • The inference's AI assistant no longer sends each sheet's first 50 rows. It sends each table's profile, computed in the database over every row: per column the counts, value type, candidate key, delimiters, placeholders, overlaps with other columns and, from samples up, the most frequent values with counts (at most AI_SAMPLE_VALUES at samples); structure sends no value. A request over half of AI_CONTEXT_TOKENS drops the values of the columns with the most distinct values first. Prompt version model-names/2.

Added (0.16 AP2, AI targets for unmapped values)

  • Propose targets in a mapping grid: POST /workspaces/{key}/mapping-proposals (a job of kind aiProposal) asks the provider for a target for each unmapped source of a view, among the targets the grid would offer (all up to 100, else each source's 20 closest), and writes them into a new or open change set; never into live data. The job counts proposed, unmatched, changed, refused and not-answered sources. Needs Use AI proposals and send level values.
  • Provenance on change-set changes: origin on change_set_relations and change_set_objects (migration 20261118_01), on coverage target summaries, copied into the published relation's audit event and answered by record history. Coverage takes proposed=true in a change set; the grid's Proposed filter and the "Proposed by the AI" mark with its reason.
  • sendLevel on a feature's request, a lower level than AI_SEND_LEVEL for that request (AP-D3).
  • AI_CONTEXT_TOKENS, a small model's context window: mapping-target calls are sized to half of it. A source with no candidate is not sent; fields repeating the name or holding a generated identifier are left out of what is sent. GET /jobs filters by kind=aiProposal.

Added (0.16 AP1, the AI proposal provider)

  • An AI provider for proposals, set in the environment: AI_PROVIDER (claude, openai-compatible or a factory), AI_MODEL, AI_API_KEY, AI_BASE_URL, and how much may leave the instance, AI_SEND_LEVEL (structure, samples, values, default values) with AI_SAMPLE_VALUES; AI_MIN_CONFIDENCE, AI_LOG_CONTENT, AI_LOG_CONTENT_DAYS, AI_TIMEOUT_SECONDS. A setting that cannot work refuses the start. Without a provider nothing is sent anywhere.
  • Settings › Administration › AI and GET /ai/status, GET /ai/calls, POST /ai/test (Operations): the provider, the send level, the features offered, a test request and every call with who asked, the sizes, the tokens and the outcome (table ai_calls, migration 20261117_01). GET /system answers ai.
  • The action group Use AI proposals (ai-proposals), held by the Data steward, Data owner, Model administrator, Workspace administrator and Integration administrator roles.
  • The inference's assistant runs on the provider: POST /model-drafts/{id}/inference takes assistant (a checkbox in the dialog); its clearer names are proposals of pattern assistant carrying evidence.origin (kind: ai, model, confidence, reason), copied as origin.proposedBy into the step that applies one, and shown in the editor's "Proposed because".

Changed (0.16 AP1)

  • INFERENCE_ASSISTANT and INFERENCE_ASSISTANT_ROWS are retired; the start is refused while either is set, naming AI_PROVIDER or AI_SAMPLE_VALUES. httpx is now a runtime dependency (both adapters use it).

  • Every setting in both settings files: python -m app.cli.settings_template writes every engine setting from the settings catalogue, commented out at its default with its description. scripts/dev.sh settings uses it for a new .dev/settings.env and, run again, adds what an existing file misses and comments out a retired setting; the engine part of .env.example is generated from it and a unit test keeps it so. compose.yml passes all of .env to the engine (env_file) instead of a hand-kept list, which had left out every setting but four capability groups.

  • AI_REASONING_EFFORT (openai-compatible): sends reasoning_effort, so a local thinking model answers without reasoning for minutes. The adapter also shows the answer's shape in the instructions as an example to fill in, so a model on an endpoint that does not enforce the schema (Ollama's MLX builds) answers in shape.

Fixed (0.16 AP2)

  • Mapping sources ignored the binding's criteria: a mapping grid applied candidates only to targets, so coverage, its counts and state filters, the Proposed filter and the AI proposal job's unmapped sources included records that can never be mapped there. Sources are now the source-type records meeting the criteria at the source end, plus any that already holds a relationship of the binding (kept, D175).

Fixed (0.16 AP1)

  • A burst of requests froze the engine: a page asking for many lists at once (a sidebar of 101 types) left the requests holding database connections without a worker thread to finish on, while the rest held every thread waiting for a connection; everything waited for DATABASE_POOL_TIMEOUT and answered 503. At most as many requests as the pool has connections are now inside the application; the rest wait for their turn without a thread (503 poolExhausted only after DATABASE_POOL_TIMEOUT).
  • Handled errors were also logged as unhandled: the catch-all handler was registered where Starlette raises the exception again after the answer, so every 503 or 500 also printed an "Exception in ASGI application" traceback. The last-line handler now answers and raises nothing further; J12.11 fails on any escaping error.
  • A slow AI provider no longer breaks the inference: the inference held its database transaction open while waiting for the answer, so a model taking longer than DATABASE_IDLE_TRANSACTION_TIMEOUT (a local model can) had its connection ended. It now ends the transaction first and marks the job's phase assistant.
  • The editor waits for the proposals, visibly: while the inference runs it says what it is doing ("Asking the AI assistant (model) for clearer names…"), pauses editing so a change cannot discard the proposals, and offers Stop; afterwards it says what the AI did, including "found the names good as they are". The inference read answers phase and assistantOutcome; GET /system's ai names the model.
  • A draft profile key taken by another model's profile was refused only at installation or loading, and the editor had no way to change the key. It is now an error in Issues at the profile's key, with a free key (freeKey on the draft's profile and in the refusal's details) offered as "Use '…'" there and in the Load and Install dialogs; the source pane shows and edits the profile's key and name.
  • tests/test_startup.py pinned the schema head at 20261114_01, two migrations behind 0.12.0; POST /me/notifications/removal was missing from the API journey inventory.

Let us run it for you.

Backups, upgrades, TLS and monitoring are ours. Your data stays exportable any time.