01
The features were there; the way back was not
The items in this release look unrelated to each other. A copy button on messages, task retry, conversation search, an audit log, cost estimation. But tracing why each one was needed lands in the same place every time: when something went wrong, there was nothing the user could do from the screen.
If an answer was unsatisfying, you retyped the question from scratch. If an agent task failed, the card simply said failed and stayed that way. Finding an old conversation required remembering its title, and for a signed-out user the history list looked empty from the start. None of these were missing features. They were missing entrances back.
02
Regeneration refuses to run on attachments
Completed assistant messages now carry a copy button, and the last assistant message carries a regenerate button. Copy does not hand over exactly what is on screen: it strips artifact placeholders first, and the button hides itself in insecure contexts where the clipboard API is unavailable.
Regeneration needed more care. Only the composer holds the socket, so the message component cannot resend on its own. It raises a resend request through the store, the composer rewinds the history and sends again, and if the socket has already closed and the send fails, the rewound conversation is restored.
The most important decision was where not to put the button. Messages with attachments are excluded from regeneration. The original files are not retained, so a resend would carry only the filenames as text, and the model would produce a confident answer having never seen the attachment. A missing button is better than a button that returns a wrong answer.
03
We added a retry button and it cancelled itself immediately
Rerunning a failed or cancelled agent task from the beginning did not require a new execution path. The existing execute API already had a fresh-start route, so the list card only had to call it again.
Measured against a real task, though, the retry aborted the moment it started. The cause was the start guard. On entering execution it reads the state left in the database, and it was reading the cancellation left behind by the previous run as a cancellation request against the new one. Resetting the state to pending before re-entering execution resolved it.
A few things were fixed alongside. A fresh rerun now clears the previous checkpoint; leaving it makes the task look resumable when what remains is a leftover from the last run. An optimistic pending update closes the double-click window right after the button is pressed, and tasks that use a local executor check the device connection first, symmetrically with task creation.
Applying the review feedback pushed the route file to 618 lines, past the 600-line CI guard. Rather than cutting behaviour, the owned-task lookup and the public-shape conversion moved into a helper module, bringing it back to 596.
04
Guest conversations were being saved
There was an impression that conversations from signed-out users were not kept in history. They were, from the beginning. They landed in the database under an anonymous session identifier. The problem was that the recent-conversations list in the sidebar never invalidated its cache when a chat ended. A refresh brought them back.
Signed-in users already had several conversations in the list, so one arriving late was hard to notice. For guests the list started empty, so it read as nothing being saved at all. Invalidating the cache at the common end-of-stream point — the one that normal completion, abort, and error all pass through — closed it.
Finding things was still a problem. Search only scanned titles, so a conversation whose subject you remembered but whose title you did not was unreachable. Search now covers titles and message bodies together and shows an excerpt around the match. Input goes to the server on a 300 ms debounce, and changing the query discards the previous results immediately so a late response cannot paint over the screen. Pattern metacharacters in the query are escaped, and the owner condition is bound as a value.
- The non-search response shape is unchanged, so the sidebar and personal history were untouched
- Sessions that match outside the top 100 are merged into the results
- In the admin all-user view, a title filter applies instead of body search
05
Paging instead of raising the ceiling
The admin all-conversations screen read 200 records in a single query. Anything older than that, guest conversations included, could not be opened at all. Raising the number was an option, but it only defers the same problem, so the screen moved to server-side offset pagination and can now walk the entire set page by page.
The response carries a total count, but only in the all-scope view. The sidebar and personal history use the same API, so adding the field to every response would change the contract for existing consumers. The screen pages in blocks of fifty with previous and next controls, alongside the total count and the current page.
06
Every assignment vanished and there was no history
On August 8, model-role assignments went to zero rows for both users and the global scope. Restoring them was not difficult, but why they disappeared was never established. User-side assignment changes wrote no audit record at all, and the admin side wrote one without the value being replaced. The goal of this change was that the next time this happens, someone can read who changed what, and from which value to which.
The first implementation had defects, and code review caught four. The worst was the lookup added to fetch the previous value. Its exception propagated, so a momentary database problem turned an assignment that would otherwise have succeeded into a 500. Trying to record the change killed the thing being recorded.
Making the previous value a by-product of the write, rather than a separate lookup, removed that path entirely. The update opens a transaction, takes a per-key advisory lock, and performs the read and the insert as one unit; the delete returns the removed value as part of the deletion. Two concurrent requests changing the same role can no longer produce an audit entry whose previous value disagrees with what was actually there.
An earlier attempt put the lock inside a common table expression. Measurement showed it conflicting with the insert in the same statement and returning an empty previous value, so it was discarded. Audit writes are now awaited so they commit before the response, and a failed audit does not kill the assignment — but it is not swallowed either; it is logged as an error with the previous value included.
07
Which way should a guard fail
Three guards went into this release, and each fails in a different direction.
A June notice moved NAVER's search API to a new gateway. When the new key pair is configured the new route is used; otherwise the legacy one. The two are never called together — one search means one call — and removing the environment variables rolls it back. When the daily free allowance is reached, the call is stopped before it is made; but if the counter store is unresponsive, search passes through instead. Exceeding the quota gets rejected upstream anyway, so losing search to our own broken counter is the worse outcome.
The audit trail fails the other way: the assignment goes through, and the failure is recorded. Recording is the whole point, so a failed record must not stop the feature — and if it disappears quietly, it was never an audit trail to begin with.
HTTPS enforcement is the third. The backend policy header never reached the HTML routes Next serves directly, and the header announcing the framework was going out untouched. The value now matches the backend, without the option that preloads the domain into browser lists. That option is extremely hard to undo once it is in.
08
The cost figure is not an invoice
The usage screen now shows daily, monthly, and yearly cost estimates. This is self-hosted, so nothing is actually billed. The number exists to answer what the same usage would have cost through a commercial API.
Agent-task tokens are included alongside conversation tokens. Counting conversations alone understates the total badly — task tokens dominate in practice. The reference rates are configurable through environment variables, and the defaults use the published Qwen3.8-Max pricing of two dollars per million input tokens and six per million output. The first defaults were 30B-class rates, replaced once it was clear what class of model actually runs here. Measured against a real account, 7.1 million cumulative tokens came to roughly $4.26, about ₩5,970.
More care went into preventing a misreading than into the number itself. Token records only exist from July 2026 onward, because conversations are deleted on a 30-day rolling window and token persistence began then. So the response carries the earliest recorded date and the screen states the coverage window in a footnote. If this figure reads as complete lifetime usage, that is worse than the number being imprecise.
09
Next
What exists now are entrances that open after a failure: regenerate, rerun, search again, and read later what changed. What does not exist is regeneration for messages with attachments. That door stays shut as long as original files are not retained, and retention is a decision that belongs with the storage policy.
The audit side is the same. What closed here is one setting: model-role assignment. Asking the same question of other settings will mostly give the same answer, and rather than learning which to close by watching them disappear, the plan is to map the blast radius per setting first.
Source evidence
OpenMake